Skip to content

Commit a6245ab

Browse files
authored
GH-804: Prepend JDBC FlightSQL version to user agent (#806)
## What's Changed * Driver version is passed on to NettyBuilderClient to append it to the user-agent header * NettyBuilderClient now prepends `JDBC Flight SQL Client <version>` to the header (e.g. `JDBC Flight SQL Client 19.0.0-SNAPSHOT grpc-java-netty/1.73.0`) Closes #804.
1 parent e4f6426 commit a6245ab

File tree

6 files changed

+107
-13
lines changed

6 files changed

+107
-13
lines changed

flight/flight-sql-jdbc-core/src/main/java/org/apache/arrow/driver/jdbc/ArrowFlightConnection.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import org.apache.arrow.util.Preconditions;
3333
import org.apache.calcite.avatica.AvaticaConnection;
3434
import org.apache.calcite.avatica.AvaticaFactory;
35+
import org.apache.calcite.avatica.DriverVersion;
3536

3637
/** Connection to the Arrow Flight server. */
3738
public final class ArrowFlightConnection extends AvaticaConnection {
@@ -86,13 +87,16 @@ static ArrowFlightConnection createNewConnection(
8687
throws SQLException {
8788
url = replaceSemiColons(url);
8889
final ArrowFlightConnectionConfigImpl config = new ArrowFlightConnectionConfigImpl(properties);
89-
final ArrowFlightSqlClientHandler clientHandler = createNewClientHandler(config, allocator);
90+
final ArrowFlightSqlClientHandler clientHandler =
91+
createNewClientHandler(config, allocator, driver.getDriverVersion());
9092
return new ArrowFlightConnection(
9193
driver, factory, url, properties, config, allocator, clientHandler);
9294
}
9395

9496
private static ArrowFlightSqlClientHandler createNewClientHandler(
95-
final ArrowFlightConnectionConfigImpl config, final BufferAllocator allocator)
97+
final ArrowFlightConnectionConfigImpl config,
98+
final BufferAllocator allocator,
99+
final DriverVersion driverVersion)
96100
throws SQLException {
97101
try {
98102
return new ArrowFlightSqlClientHandler.Builder()
@@ -116,6 +120,7 @@ private static ArrowFlightSqlClientHandler createNewClientHandler(
116120
.withCatalog(config.getCatalog())
117121
.withClientCache(config.useClientCache() ? new FlightClientCache() : null)
118122
.withConnectTimeout(config.getConnectTimeout())
123+
.withDriverVersion(driverVersion)
119124
.build();
120125
} catch (final SQLException e) {
121126
try {

flight/flight-sql-jdbc-core/src/main/java/org/apache/arrow/driver/jdbc/client/ArrowFlightSqlClientHandler.java

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@
6666
import org.apache.arrow.util.VisibleForTesting;
6767
import org.apache.arrow.vector.VectorSchemaRoot;
6868
import org.apache.arrow.vector.types.pojo.Schema;
69+
import org.apache.calcite.avatica.DriverVersion;
6970
import org.apache.calcite.avatica.Meta.StatementType;
7071
import org.checkerframework.checker.nullness.qual.Nullable;
7172
import org.slf4j.Logger;
@@ -548,6 +549,9 @@ public FlightInfo getCrossReference(
548549

549550
/** Builder for {@link ArrowFlightSqlClientHandler}. */
550551
public static final class Builder {
552+
static final String USER_AGENT_TEMPLATE = "JDBC Flight SQL Driver %s";
553+
static final String DEFAULT_VERSION = "(unknown or development build)";
554+
551555
private final Set<FlightClientMiddleware.Factory> middlewareFactories = new HashSet<>();
552556
private final Set<CallOption> options = new HashSet<>();
553557
private String host;
@@ -597,6 +601,8 @@ public static final class Builder {
597601
@VisibleForTesting
598602
ClientCookieMiddleware.Factory cookieFactory = new ClientCookieMiddleware.Factory();
599603

604+
DriverVersion driverVersion;
605+
600606
public Builder() {}
601607

602608
/**
@@ -631,6 +637,8 @@ public Builder() {}
631637
if (original.retainAuth) {
632638
this.authFactory = original.authFactory;
633639
}
640+
641+
this.driverVersion = original.driverVersion;
634642
}
635643

636644
/**
@@ -879,6 +887,17 @@ public Builder withConnectTimeout(Duration connectTimeout) {
879887
return this;
880888
}
881889

890+
/**
891+
* Sets the driver version for this handler.
892+
*
893+
* @param driverVersion the driver version to set
894+
* @return this builder instance
895+
*/
896+
public Builder withDriverVersion(DriverVersion driverVersion) {
897+
this.driverVersion = driverVersion;
898+
return this;
899+
}
900+
882901
public String getCacheKey() {
883902
return getLocation().toString();
884903
}
@@ -914,6 +933,11 @@ public ArrowFlightSqlClientHandler build() throws SQLException {
914933
final NettyClientBuilder clientBuilder = new NettyClientBuilder();
915934
clientBuilder.allocator(allocator);
916935

936+
String userAgent = String.format(USER_AGENT_TEMPLATE, DEFAULT_VERSION);
937+
if (driverVersion != null && driverVersion.versionString != null) {
938+
userAgent = String.format(USER_AGENT_TEMPLATE, driverVersion.versionString);
939+
}
940+
917941
buildTimeMiddlewareFactories.add(new ClientCookieMiddleware.Factory());
918942
buildTimeMiddlewareFactories.forEach(clientBuilder::intercept);
919943
if (useEncryption) {
@@ -948,6 +972,9 @@ public ArrowFlightSqlClientHandler build() throws SQLException {
948972
}
949973

950974
NettyChannelBuilder channelBuilder = clientBuilder.build();
975+
976+
channelBuilder.userAgent(userAgent);
977+
951978
if (connectTimeout != null) {
952979
channelBuilder.withOption(
953980
ChannelOption.CONNECT_TIMEOUT_MILLIS, (int) connectTimeout.toMillis());

flight/flight-sql-jdbc-core/src/test/java/org/apache/arrow/driver/jdbc/ArrowFlightJdbcConnectionCookieTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,11 @@ public void testCookies() throws SQLException {
3939
Statement statement = connection.createStatement()) {
4040

4141
// Expect client didn't receive cookies before any operation
42-
assertNull(FLIGHT_SERVER_TEST_EXTENSION.getMiddlewareCookieFactory().getCookie());
42+
assertNull(FLIGHT_SERVER_TEST_EXTENSION.getInterceptorFactory().getCookie());
4343

4444
// Run another action for check if the cookies was sent by the server.
4545
statement.execute(CoreMockedSqlProducers.LEGACY_REGULAR_SQL_CMD);
46-
assertEquals("k=v", FLIGHT_SERVER_TEST_EXTENSION.getMiddlewareCookieFactory().getCookie());
46+
assertEquals("k=v", FLIGHT_SERVER_TEST_EXTENSION.getInterceptorFactory().getCookie());
4747
}
4848
}
4949
}

flight/flight-sql-jdbc-core/src/test/java/org/apache/arrow/driver/jdbc/ConnectionTest.java

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import org.apache.arrow.driver.jdbc.client.ArrowFlightSqlClientHandler;
3232
import org.apache.arrow.driver.jdbc.utils.ArrowFlightConnectionConfigImpl.ArrowFlightConnectionProperty;
3333
import org.apache.arrow.driver.jdbc.utils.MockFlightSqlProducer;
34+
import org.apache.arrow.flight.FlightMethod;
3435
import org.apache.arrow.memory.BufferAllocator;
3536
import org.apache.arrow.memory.RootAllocator;
3637
import org.apache.arrow.util.AutoCloseables;
@@ -576,4 +577,49 @@ public void testPasswordConnectionPropertyIntegerCorrectCastUrlWithDriverManager
576577
assertTrue(connection.isValid(0));
577578
}
578579
}
580+
581+
/**
582+
* Test that the JDBC driver properly integrates driver version into client handler.
583+
*
584+
* @throws Exception on error.
585+
*/
586+
@Test
587+
public void testJdbcDriverVersionIntegration() throws Exception {
588+
final Properties properties = new Properties();
589+
properties.put(
590+
ArrowFlightConnectionProperty.HOST.camelName(), FLIGHT_SERVER_TEST_EXTENSION.getHost());
591+
properties.put(
592+
ArrowFlightConnectionProperty.PORT.camelName(), FLIGHT_SERVER_TEST_EXTENSION.getPort());
593+
properties.put(ArrowFlightConnectionProperty.USER.camelName(), userTest);
594+
properties.put(ArrowFlightConnectionProperty.PASSWORD.camelName(), passTest);
595+
properties.put(ArrowFlightConnectionProperty.USE_ENCRYPTION.camelName(), false);
596+
597+
// Create a driver instance and connect
598+
ArrowFlightJdbcDriver driverVersion = new ArrowFlightJdbcDriver();
599+
600+
try (Connection connection =
601+
ArrowFlightConnection.createNewConnection(
602+
driverVersion,
603+
new ArrowFlightJdbcFactory(),
604+
"jdbc:arrow-flight-sql://localhost:" + FLIGHT_SERVER_TEST_EXTENSION.getPort(),
605+
properties,
606+
allocator)) {
607+
608+
assertTrue(connection.isValid(0));
609+
610+
var actualUserAgent =
611+
FLIGHT_SERVER_TEST_EXTENSION
612+
.getInterceptorFactory()
613+
.getHeader(FlightMethod.HANDSHAKE, "user-agent");
614+
615+
var expectedUserAgent =
616+
"JDBC Flight SQL Driver " + driverVersion.getDriverVersion().versionString;
617+
// Driver appends version to grpc user-agent header. Assert the header starts with the
618+
// expected
619+
// value and ignored grpc version.
620+
assertTrue(
621+
actualUserAgent.startsWith(expectedUserAgent),
622+
"Expected: " + expectedUserAgent + " but found: " + actualUserAgent);
623+
}
624+
}
579625
}

flight/flight-sql-jdbc-core/src/test/java/org/apache/arrow/driver/jdbc/FlightServerTestExtension.java

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
import java.sql.SQLException;
2626
import java.util.ArrayDeque;
2727
import java.util.Deque;
28+
import java.util.HashMap;
29+
import java.util.Map;
2830
import java.util.Properties;
2931
import org.apache.arrow.driver.jdbc.authentication.Authentication;
3032
import org.apache.arrow.driver.jdbc.authentication.TokenAuthentication;
@@ -33,6 +35,7 @@
3335
import org.apache.arrow.flight.CallHeaders;
3436
import org.apache.arrow.flight.CallInfo;
3537
import org.apache.arrow.flight.CallStatus;
38+
import org.apache.arrow.flight.FlightMethod;
3639
import org.apache.arrow.flight.FlightServer;
3740
import org.apache.arrow.flight.FlightServerMiddleware;
3841
import org.apache.arrow.flight.Location;
@@ -67,7 +70,8 @@ public class FlightServerTestExtension
6770
private final CertKeyPair certKeyPair;
6871
private final File mTlsCACert;
6972

70-
private final MiddlewareCookie.Factory middlewareCookieFactory = new MiddlewareCookie.Factory();
73+
private final InterceptorMiddleware.Factory interceptorFactory =
74+
new InterceptorMiddleware.Factory();
7175

7276
private FlightServerTestExtension(
7377
final Properties properties,
@@ -130,8 +134,8 @@ private void setUseEncryption(boolean useEncryption) {
130134
properties.put("useEncryption", useEncryption);
131135
}
132136

133-
public MiddlewareCookie.Factory getMiddlewareCookieFactory() {
134-
return middlewareCookieFactory;
137+
public InterceptorMiddleware.Factory getInterceptorFactory() {
138+
return interceptorFactory;
135139
}
136140

137141
@FunctionalInterface
@@ -143,7 +147,7 @@ private FlightServer initiateServer(Location location) throws IOException {
143147
FlightServer.Builder builder =
144148
FlightServer.builder(allocator, location, producer)
145149
.headerAuthenticator(authentication.authenticate())
146-
.middleware(FlightServerMiddleware.Key.of("KEY"), middlewareCookieFactory);
150+
.middleware(FlightServerMiddleware.Key.of("KEY"), interceptorFactory);
147151
if (certKeyPair != null) {
148152
builder.useTls(certKeyPair.cert, certKeyPair.key);
149153
}
@@ -301,11 +305,11 @@ public FlightServerTestExtension build() {
301305
* A middleware to handle with the cookies in the server. It is used to test if cookies are being
302306
* sent properly.
303307
*/
304-
static class MiddlewareCookie implements FlightServerMiddleware {
308+
static class InterceptorMiddleware implements FlightServerMiddleware {
305309

306310
private final Factory factory;
307311

308-
public MiddlewareCookie(Factory factory) {
312+
public InterceptorMiddleware(Factory factory) {
309313
this.factory = factory;
310314
}
311315

@@ -323,22 +327,33 @@ public void onCallCompleted(CallStatus callStatus) {}
323327
public void onCallErrored(Throwable throwable) {}
324328

325329
/** A factory for the MiddlewareCookie. */
326-
static class Factory implements FlightServerMiddleware.Factory<MiddlewareCookie> {
330+
static class Factory implements FlightServerMiddleware.Factory<InterceptorMiddleware> {
327331

332+
private final Map<FlightMethod, CallHeaders> receivedCallHeaders = new HashMap<>();
328333
private boolean receivedCookieHeader = false;
329334
private String cookie;
330335

331336
@Override
332-
public MiddlewareCookie onCallStarted(
337+
public InterceptorMiddleware onCallStarted(
333338
CallInfo callInfo, CallHeaders callHeaders, RequestContext requestContext) {
334339
cookie = callHeaders.get("Cookie");
335340
receivedCookieHeader = null != cookie;
336-
return new MiddlewareCookie(this);
341+
342+
receivedCallHeaders.put(callInfo.method(), callHeaders);
343+
return new InterceptorMiddleware(this);
337344
}
338345

339346
public String getCookie() {
340347
return cookie;
341348
}
349+
350+
public String getHeader(FlightMethod method, String key) {
351+
CallHeaders headers = receivedCallHeaders.get(method);
352+
if (headers == null) {
353+
return null;
354+
}
355+
return headers.get(key);
356+
}
342357
}
343358
}
344359
}

flight/flight-sql-jdbc-core/src/test/java/org/apache/arrow/driver/jdbc/client/ArrowFlightSqlClientHandlerBuilderTest.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ public void testDefaults() {
149149
assertEquals(Optional.empty(), builder.catalog);
150150
assertNull(builder.flightClientCache);
151151
assertNull(builder.connectTimeout);
152+
assertNull(builder.driverVersion);
152153
}
153154

154155
@Test

0 commit comments

Comments
 (0)