diff --git a/adapter/avro/pom.xml b/adapter/avro/pom.xml index 827d19f2a2..4cb437703b 100644 --- a/adapter/avro/pom.xml +++ b/adapter/avro/pom.xml @@ -23,7 +23,7 @@ under the License. org.apache.arrow arrow-java-root - 19.0.0-SNAPSHOT + 19.0.0-iomete.1 ../../pom.xml diff --git a/adapter/jdbc/pom.xml b/adapter/jdbc/pom.xml index 2f621d7a05..a5baf315d4 100644 --- a/adapter/jdbc/pom.xml +++ b/adapter/jdbc/pom.xml @@ -23,7 +23,7 @@ under the License. org.apache.arrow arrow-java-root - 19.0.0-SNAPSHOT + 19.0.0-iomete.1 ../../pom.xml diff --git a/adapter/orc/pom.xml b/adapter/orc/pom.xml index c96ab36119..0821b03523 100644 --- a/adapter/orc/pom.xml +++ b/adapter/orc/pom.xml @@ -23,7 +23,7 @@ under the License. org.apache.arrow arrow-java-root - 19.0.0-SNAPSHOT + 19.0.0-iomete.1 ../../pom.xml diff --git a/algorithm/pom.xml b/algorithm/pom.xml index 898c2605b6..9b6d95c37a 100644 --- a/algorithm/pom.xml +++ b/algorithm/pom.xml @@ -22,7 +22,7 @@ under the License. org.apache.arrow arrow-java-root - 19.0.0-SNAPSHOT + 19.0.0-iomete.1 arrow-algorithm Arrow Algorithms diff --git a/arrow-variant/pom.xml b/arrow-variant/pom.xml index 3a842178a4..b4d5608b54 100644 --- a/arrow-variant/pom.xml +++ b/arrow-variant/pom.xml @@ -22,7 +22,7 @@ under the License. org.apache.arrow arrow-java-root - 19.0.0-SNAPSHOT + 19.0.0-iomete.1 arrow-variant Arrow Variant diff --git a/bom/pom.xml b/bom/pom.xml index 0de43a1217..5af49b1ef4 100644 --- a/bom/pom.xml +++ b/bom/pom.xml @@ -29,7 +29,7 @@ under the License. org.apache.arrow arrow-bom - 19.0.0-SNAPSHOT + 19.0.0-iomete.1 pom Arrow Bill of Materials diff --git a/c/pom.xml b/c/pom.xml index c90b6dc0ef..5d7c50bc1b 100644 --- a/c/pom.xml +++ b/c/pom.xml @@ -22,7 +22,7 @@ under the License. org.apache.arrow arrow-java-root - 19.0.0-SNAPSHOT + 19.0.0-iomete.1 arrow-c-data diff --git a/compression/pom.xml b/compression/pom.xml index 29f8b41788..7d932706dd 100644 --- a/compression/pom.xml +++ b/compression/pom.xml @@ -22,7 +22,7 @@ under the License. org.apache.arrow arrow-java-root - 19.0.0-SNAPSHOT + 19.0.0-iomete.1 arrow-compression Arrow Compression diff --git a/dataset/pom.xml b/dataset/pom.xml index 1852c6eddc..688229fddf 100644 --- a/dataset/pom.xml +++ b/dataset/pom.xml @@ -22,7 +22,7 @@ under the License. org.apache.arrow arrow-java-root - 19.0.0-SNAPSHOT + 19.0.0-iomete.1 arrow-dataset diff --git a/flight/flight-core/pom.xml b/flight/flight-core/pom.xml index f1d58a0cad..597ac3aaa2 100644 --- a/flight/flight-core/pom.xml +++ b/flight/flight-core/pom.xml @@ -22,7 +22,7 @@ under the License. org.apache.arrow arrow-flight - 19.0.0-SNAPSHOT + 19.0.0-iomete.1 flight-core diff --git a/flight/flight-core/src/main/java/org/apache/arrow/flight/grpc/NettyClientBuilder.java b/flight/flight-core/src/main/java/org/apache/arrow/flight/grpc/NettyClientBuilder.java index 42cdaac016..d688b26689 100644 --- a/flight/flight-core/src/main/java/org/apache/arrow/flight/grpc/NettyClientBuilder.java +++ b/flight/flight-core/src/main/java/org/apache/arrow/flight/grpc/NettyClientBuilder.java @@ -185,6 +185,28 @@ public NettyChannelBuilder build() { "Scheme is not supported: " + location.getUri().getScheme()); } + try { + configureChannel(builder); + } catch (SSLException e) { + throw new RuntimeException(e); + } + return builder; + } + + /** + * Build a {@link NettyChannelBuilder} using {@code forTarget()} instead of {@code forAddress()}. + * + *

This is required for proxy support: only the DNS resolver path ({@code dns:///host:port}) + * invokes the {@link io.grpc.ProxyDetector}. {@code forAddress()} bypasses it entirely. + */ + public NettyChannelBuilder buildForTarget(String target) throws SSLException { + final NettyChannelBuilder builder = NettyChannelBuilder.forTarget(target); + configureChannel(builder); + return builder; + } + + /** Apply TLS and message-size configuration to an already-created {@link NettyChannelBuilder}. */ + protected void configureChannel(NettyChannelBuilder builder) throws SSLException { if (this.forceTls || LocationSchemes.GRPC_TLS.equals(location.getUri().getScheme())) { builder.useTransportSecurity(); @@ -210,11 +232,8 @@ public NettyChannelBuilder build() { sslContextBuilder.keyManager(this.clientCertificate, this.clientKey); } } - try { - builder.sslContext(sslContextBuilder.build()); - } catch (SSLException e) { - throw new RuntimeException(e); - } + + builder.sslContext(sslContextBuilder.build()); if (this.overrideHostname != null) { builder.overrideAuthority(this.overrideHostname); @@ -227,6 +246,5 @@ public NettyChannelBuilder build() { .maxTraceEvents(MAX_CHANNEL_TRACE_EVENTS) .maxInboundMessageSize(maxInboundMessageSize) .maxInboundMetadataSize(maxInboundMessageSize); - return builder; } } diff --git a/flight/flight-integration-tests/pom.xml b/flight/flight-integration-tests/pom.xml index f0f10ada43..f525160e8a 100644 --- a/flight/flight-integration-tests/pom.xml +++ b/flight/flight-integration-tests/pom.xml @@ -22,7 +22,7 @@ under the License. org.apache.arrow arrow-flight - 19.0.0-SNAPSHOT + 19.0.0-iomete.1 flight-integration-tests diff --git a/flight/flight-sql-jdbc-core/pom.xml b/flight/flight-sql-jdbc-core/pom.xml index da00baf32a..4e910eae9a 100644 --- a/flight/flight-sql-jdbc-core/pom.xml +++ b/flight/flight-sql-jdbc-core/pom.xml @@ -22,7 +22,7 @@ under the License. org.apache.arrow arrow-flight - 19.0.0-SNAPSHOT + 19.0.0-iomete.1 flight-sql-jdbc-core diff --git a/flight/flight-sql-jdbc-core/src/main/java/org/apache/arrow/driver/jdbc/ArrowFlightConnection.java b/flight/flight-sql-jdbc-core/src/main/java/org/apache/arrow/driver/jdbc/ArrowFlightConnection.java index 623c2b81be..c7c6e65d3c 100644 --- a/flight/flight-sql-jdbc-core/src/main/java/org/apache/arrow/driver/jdbc/ArrowFlightConnection.java +++ b/flight/flight-sql-jdbc-core/src/main/java/org/apache/arrow/driver/jdbc/ArrowFlightConnection.java @@ -128,6 +128,9 @@ private static ArrowFlightSqlClientHandler createNewClientHandler( .withConnectTimeout(config.getConnectTimeout()) .withDriverVersion(driverVersion) .withOAuthConfiguration(config.getOauthConfiguration()) + .withProxySettings(config.getProxyHost(), config.getProxyPort()) + .withProxyBypassPattern(config.getProxyBypassPattern()) + .withProxyDisable(config.getProxyDisable()) .build(); } catch (final SQLException e) { try { diff --git a/flight/flight-sql-jdbc-core/src/main/java/org/apache/arrow/driver/jdbc/client/ArrowFlightSqlClientHandler.java b/flight/flight-sql-jdbc-core/src/main/java/org/apache/arrow/driver/jdbc/client/ArrowFlightSqlClientHandler.java index f0ea284239..60dc7e57e0 100644 --- a/flight/flight-sql-jdbc-core/src/main/java/org/apache/arrow/driver/jdbc/client/ArrowFlightSqlClientHandler.java +++ b/flight/flight-sql-jdbc-core/src/main/java/org/apache/arrow/driver/jdbc/client/ArrowFlightSqlClientHandler.java @@ -38,6 +38,7 @@ import org.apache.arrow.driver.jdbc.client.utils.ClientAuthenticationUtils; import org.apache.arrow.driver.jdbc.client.utils.FlightClientCache; import org.apache.arrow.driver.jdbc.client.utils.FlightLocationQueue; +import org.apache.arrow.driver.jdbc.utils.ArrowFlightProxyDetector; import org.apache.arrow.flight.CallOption; import org.apache.arrow.flight.CallStatus; import org.apache.arrow.flight.CloseSessionRequest; @@ -693,6 +694,14 @@ public static final class Builder { DriverVersion driverVersion; + @VisibleForTesting String proxyHost; + + @VisibleForTesting Integer proxyPort; + + @VisibleForTesting String proxyBypassPattern; + + @VisibleForTesting String proxyDisable; + public Builder() {} /** @@ -1000,6 +1009,63 @@ public Builder withOAuthConfiguration(final OAuthConfiguration oauthConfig) { return this; } + /** + * Sets the explicit proxy host and port for this connection. + * + * @param proxyHost the proxy hostname or IP, or {@code null} to clear + * @param proxyPort the proxy port, or {@code null} to clear + * @return this builder instance + */ + public Builder withProxySettings( + @Nullable final String proxyHost, @Nullable final Integer proxyPort) { + this.proxyHost = proxyHost; + this.proxyPort = proxyPort; + return this; + } + + /** + * Sets the proxy bypass pattern in {@code http.nonProxyHosts} format ({@code |}-separated glob + * patterns). + * + * @param proxyBypassPattern bypass pattern, or {@code null} to clear + * @return this builder instance + */ + public Builder withProxyBypassPattern(@Nullable final String proxyBypassPattern) { + this.proxyBypassPattern = proxyBypassPattern; + return this; + } + + /** + * Sets the proxy disable flag. Pass {@code "force"} to disable proxy even when system + * properties or environment variables configure one. + * + * @param proxyDisable disable flag value, or {@code null} to clear + * @return this builder instance + */ + public Builder withProxyDisable(@Nullable final String proxyDisable) { + this.proxyDisable = proxyDisable; + return this; + } + + private String getDnsTarget() { + // Validate port is in valid range (0-65535) before passing to gRPC. + // gRPC's DNS resolver validates the port on a background thread, but background thread + // exceptions do not propagate to the caller and prevent proper error handling. + if (port < 0 || port > 65535) { + throw new IllegalArgumentException("port out of range: " + port); + } + // IPv6 addresses need brackets in the URI authority component + String normalizedHost = host.contains(":") && !host.startsWith("[") ? "[" + host + "]" : host; + return "dns:///" + normalizedHost + ":" + port; + } + + private NettyChannelBuilder getChannelBuilder(final NettyClientBuilder clientBuilder) + throws IOException { + // forTarget() ensures the DNS resolver is used, which invokes ProxyDetector. + // forAddress() bypasses it entirely. + return clientBuilder.buildForTarget(getDnsTarget()); + } + public String getCacheKey() { return getLocation().toString(); } @@ -1075,9 +1141,12 @@ public ArrowFlightSqlClientHandler build() throws SQLException { } } - NettyChannelBuilder channelBuilder = clientBuilder.build(); + NettyChannelBuilder channelBuilder = getChannelBuilder(clientBuilder); channelBuilder.userAgent(userAgent); + // Always install — returns null when no proxy is applicable, safe for all connections + channelBuilder.proxyDetector( + new ArrowFlightProxyDetector(proxyHost, proxyPort, proxyBypassPattern, proxyDisable)); if (connectTimeout != null) { channelBuilder.withOption( diff --git a/flight/flight-sql-jdbc-core/src/main/java/org/apache/arrow/driver/jdbc/utils/ArrowFlightConnectionConfigImpl.java b/flight/flight-sql-jdbc-core/src/main/java/org/apache/arrow/driver/jdbc/utils/ArrowFlightConnectionConfigImpl.java index d0ba74dbcc..cf35bb1dd0 100644 --- a/flight/flight-sql-jdbc-core/src/main/java/org/apache/arrow/driver/jdbc/utils/ArrowFlightConnectionConfigImpl.java +++ b/flight/flight-sql-jdbc-core/src/main/java/org/apache/arrow/driver/jdbc/utils/ArrowFlightConnectionConfigImpl.java @@ -182,6 +182,22 @@ public boolean useClientCache() { return ArrowFlightConnectionProperty.USE_CLIENT_CACHE.getBoolean(properties); } + public String getProxyHost() { + return ArrowFlightConnectionProperty.PROXY_HOST.getString(properties); + } + + public Integer getProxyPort() { + return ArrowFlightConnectionProperty.PROXY_PORT.getInteger(properties); + } + + public String getProxyBypassPattern() { + return ArrowFlightConnectionProperty.PROXY_BYPASS_PATTERN.getString(properties); + } + + public String getProxyDisable() { + return ArrowFlightConnectionProperty.PROXY_DISABLE.getString(properties); + } + /** * Gets the {@link CallOption}s from this {@link ConnectionConfig}. * @@ -284,6 +300,10 @@ public enum ArrowFlightConnectionProperty implements ConnectionProperty { OAUTH_EXCHANGE_AUDIENCE("oauth.exchange.aud", null, Type.STRING, false), OAUTH_EXCHANGE_REQUESTED_TOKEN_TYPE( "oauth.exchange.requestedTokenType", null, Type.STRING, false), + PROXY_HOST("proxyHost", null, Type.STRING, false), + PROXY_PORT("proxyPort", null, Type.NUMBER, false), + PROXY_BYPASS_PATTERN("proxyBypassPattern", null, Type.STRING, false), + PROXY_DISABLE("proxyDisable", null, Type.STRING, false), ; private final String camelName; diff --git a/flight/flight-sql-jdbc-core/src/main/java/org/apache/arrow/driver/jdbc/utils/ArrowFlightProxyDetector.java b/flight/flight-sql-jdbc-core/src/main/java/org/apache/arrow/driver/jdbc/utils/ArrowFlightProxyDetector.java new file mode 100644 index 0000000000..baadde45ac --- /dev/null +++ b/flight/flight-sql-jdbc-core/src/main/java/org/apache/arrow/driver/jdbc/utils/ArrowFlightProxyDetector.java @@ -0,0 +1,155 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.arrow.driver.jdbc.utils; + +import io.grpc.HttpConnectProxiedSocketAddress; +import io.grpc.ProxiedSocketAddress; +import io.grpc.ProxyDetector; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.Proxy; +import java.net.ProxySelector; +import java.net.SocketAddress; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.List; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * gRPC {@link ProxyDetector} for Arrow Flight JDBC connections. + * + *

Resolution priority: + * + *

    + *
  1. {@code proxyDisable=force} → no proxy (always) + *
  2. Target host matches {@code proxyBypassPattern} → no proxy + *
  3. Explicit {@code proxyHost} + {@code proxyPort} → use that proxy + *
  4. {@link ProxySelector} default → use first non-DIRECT proxy found + *
  5. No proxy + *
+ * + *

{@code proxyBypassPattern} follows the {@code http.nonProxyHosts} format: {@code |}-separated + * glob patterns where {@code *} matches any sequence of characters. Matching is case-insensitive. + */ +public final class ArrowFlightProxyDetector implements ProxyDetector { + + @Nullable private final String proxyHost; + @Nullable private final Integer proxyPort; + @Nullable private final String bypassPattern; + private final boolean forceDisabled; + @Nullable private final ProxySelector proxySelector; + + /** Production constructor — falls back to {@link ProxySelector#getDefault()}. */ + public ArrowFlightProxyDetector( + @Nullable String proxyHost, + @Nullable Integer proxyPort, + @Nullable String bypassPattern, + @Nullable String proxyDisable) { + this(proxyHost, proxyPort, bypassPattern, proxyDisable, ProxySelector.getDefault()); + } + + /** Package-private constructor for testing — accepts an explicit {@link ProxySelector}. */ + ArrowFlightProxyDetector( + @Nullable String proxyHost, + @Nullable Integer proxyPort, + @Nullable String bypassPattern, + @Nullable String proxyDisable, + @Nullable ProxySelector proxySelector) { + this.proxyHost = proxyHost; + this.proxyPort = proxyPort; + this.bypassPattern = bypassPattern; + this.forceDisabled = "force".equalsIgnoreCase(proxyDisable); + this.proxySelector = proxySelector; + } + + @Override + @Nullable + public ProxiedSocketAddress proxyFor(SocketAddress targetAddress) throws IOException { + if (forceDisabled) { + return null; + } + InetSocketAddress inetTarget = (InetSocketAddress) targetAddress; + if (matchesBypass(inetTarget.getHostString())) { + return null; + } + InetSocketAddress proxyAddr = resolveProxy(inetTarget); + if (proxyAddr == null) { + return null; + } + return HttpConnectProxiedSocketAddress.newBuilder() + .setTargetAddress(inetTarget) + .setProxyAddress(proxyAddr) + .build(); + } + + /** + * Returns true if {@code host} matches any pattern in the bypass list. + * + *

Patterns use {@code http.nonProxyHosts} format: {@code |}-separated globs, where {@code *} + * is a wildcard for any sequence of characters. + */ + private boolean matchesBypass(String host) { + if (bypassPattern == null || bypassPattern.isEmpty()) { + return false; + } + for (String pattern : bypassPattern.split("\\|")) { + String trimmed = pattern.trim(); + if (trimmed.isEmpty()) { + continue; + } + // Convert glob to regex: escape dots, replace * with .* + String regex = trimmed.replace(".", "\\.").replace("*", ".*"); + if (host.matches("(?i)" + regex)) { + return true; + } + } + return false; + } + + /** + * Resolves which proxy address to use for the given target. + * + *

Tries explicit proxy first, then {@link ProxySelector}. + * + * @return resolved proxy address, or {@code null} if no proxy should be used + */ + @Nullable + private InetSocketAddress resolveProxy(InetSocketAddress target) { + if (proxyHost != null && proxyPort != null) { + // new InetSocketAddress(String, int) resolves IP literals without DNS; + // for hostnames it attempts DNS — required since HttpConnectProxiedSocketAddress + // validates the proxy address is resolved. + return new InetSocketAddress(proxyHost, proxyPort); + } + if (proxySelector == null) { + return null; + } + URI uri; + try { + uri = new URI("https", null, target.getHostString(), target.getPort(), null, null, null); + } catch (URISyntaxException e) { + return null; + } + List proxies = proxySelector.select(uri); + for (Proxy proxy : proxies) { + if (proxy.type() != Proxy.Type.DIRECT && proxy.address() instanceof InetSocketAddress) { + return (InetSocketAddress) proxy.address(); + } + } + return null; + } +} diff --git a/flight/flight-sql-jdbc-core/src/test/java/org/apache/arrow/driver/jdbc/client/ArrowFlightSqlClientHandlerBuilderTest.java b/flight/flight-sql-jdbc-core/src/test/java/org/apache/arrow/driver/jdbc/client/ArrowFlightSqlClientHandlerBuilderTest.java index a60a71f23d..d857429f1b 100644 --- a/flight/flight-sql-jdbc-core/src/test/java/org/apache/arrow/driver/jdbc/client/ArrowFlightSqlClientHandlerBuilderTest.java +++ b/flight/flight-sql-jdbc-core/src/test/java/org/apache/arrow/driver/jdbc/client/ArrowFlightSqlClientHandlerBuilderTest.java @@ -150,6 +150,12 @@ public void testDefaults() { assertNull(builder.flightClientCache); assertNull(builder.connectTimeout); assertNull(builder.driverVersion); + + // Proxy fields default to null + assertNull(builder.proxyHost); + assertNull(builder.proxyPort); + assertNull(builder.proxyBypassPattern); + assertNull(builder.proxyDisable); } @Test @@ -175,4 +181,34 @@ public void testCatalog() { assertTrue(rootBuilder.catalog.isPresent()); assertEquals(nameWithSpaces, rootBuilder.catalog.get()); } + + @Test + public void testProxyFieldsSetViaWithMethods() { + final ArrowFlightSqlClientHandler.Builder builder = new ArrowFlightSqlClientHandler.Builder(); + + builder.withProxySettings("proxy.corp.net", 8080); + assertEquals("proxy.corp.net", builder.proxyHost); + assertEquals(Integer.valueOf(8080), builder.proxyPort); + + builder.withProxyBypassPattern("*.internal|localhost"); + assertEquals("*.internal|localhost", builder.proxyBypassPattern); + + builder.withProxyDisable("force"); + assertEquals("force", builder.proxyDisable); + } + + @Test + public void testProxySettingsAcceptNull() { + final ArrowFlightSqlClientHandler.Builder builder = new ArrowFlightSqlClientHandler.Builder(); + + builder.withProxySettings(null, null); + assertNull(builder.proxyHost); + assertNull(builder.proxyPort); + + builder.withProxyBypassPattern(null); + assertNull(builder.proxyBypassPattern); + + builder.withProxyDisable(null); + assertNull(builder.proxyDisable); + } } diff --git a/flight/flight-sql-jdbc-core/src/test/java/org/apache/arrow/driver/jdbc/utils/ArrowFlightProxyDetectorTest.java b/flight/flight-sql-jdbc-core/src/test/java/org/apache/arrow/driver/jdbc/utils/ArrowFlightProxyDetectorTest.java new file mode 100644 index 0000000000..088300d093 --- /dev/null +++ b/flight/flight-sql-jdbc-core/src/test/java/org/apache/arrow/driver/jdbc/utils/ArrowFlightProxyDetectorTest.java @@ -0,0 +1,349 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.arrow.driver.jdbc.utils; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import io.grpc.HttpConnectProxiedSocketAddress; +import io.grpc.ProxiedSocketAddress; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.Proxy; +import java.net.ProxySelector; +import java.net.URI; +import java.util.Collections; +import java.util.List; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +/** Tests for {@link ArrowFlightProxyDetector}. */ +class ArrowFlightProxyDetectorTest { + + private static final InetSocketAddress TARGET = + InetSocketAddress.createUnresolved("dev.iomete.cloud", 443); + + // Use IP literals — InetSocketAddress resolves them without DNS, avoiding test-env failures. + // HttpConnectProxiedSocketAddress requires a resolved proxy address. + private static final String PROXY_HOST = "192.168.1.1"; + private static final int PROXY_PORT = 8080; + + @Nested + // force disabled nullifies explicit proxy; force disabled overrides ProxySelector + class ForceDisabled { + + @Test + void givenForceDisabled_whenProxyFor_thenReturnsNull() throws IOException { + // GIVEN: force disabled with explicit proxy configured + ArrowFlightProxyDetector sut = + new ArrowFlightProxyDetector(PROXY_HOST, PROXY_PORT, null, "force", noProxySelector()); + + // WHEN + ProxiedSocketAddress result = sut.proxyFor(TARGET); + + // THEN + assertNull(result); + } + + @Test + void givenForceDisabledAndProxySelectorReturnsProxy_whenProxyFor_thenReturnsNull() + throws IOException { + // GIVEN: force disabled overrides even ProxySelector + ArrowFlightProxyDetector sut = + new ArrowFlightProxyDetector( + null, null, null, "force", proxySelectorReturning("192.168.1.2", 3128)); + + // WHEN + ProxiedSocketAddress result = sut.proxyFor(TARGET); + + // THEN + assertNull(result); + } + } + + @Nested + // returns configured proxy address; preserves target address + class ExplicitProxy { + + @Test + void givenExplicitProxy_whenProxyFor_thenReturnsConfiguredProxy() throws IOException { + // GIVEN: explicit proxyHost + proxyPort + ArrowFlightProxyDetector sut = + new ArrowFlightProxyDetector(PROXY_HOST, PROXY_PORT, null, null, noProxySelector()); + + // WHEN + ProxiedSocketAddress result = sut.proxyFor(TARGET); + + // THEN + assertProxyAddress(result, PROXY_HOST, PROXY_PORT); + } + + @Test + void givenExplicitProxy_whenProxyFor_thenTargetAddressPreserved() throws IOException { + // GIVEN: explicit proxy + ArrowFlightProxyDetector sut = + new ArrowFlightProxyDetector(PROXY_HOST, PROXY_PORT, null, null, noProxySelector()); + + // WHEN + ProxiedSocketAddress result = sut.proxyFor(TARGET); + + // THEN: target address is forwarded through the proxy + HttpConnectProxiedSocketAddress httpProxy = + assertInstanceOf(HttpConnectProxiedSocketAddress.class, result); + assertEquals(TARGET, httpProxy.getTargetAddress()); + } + } + + @Nested + // matching pattern bypasses proxy; non-matching pattern allows proxy; multiple pipe-separated + // patterns; case-insensitive matching + class BypassPattern { + + @Test + void givenBypassMatchesTarget_whenProxyFor_thenReturnsNull() throws IOException { + // GIVEN: bypass pattern matches target, explicit proxy configured + ArrowFlightProxyDetector sut = + new ArrowFlightProxyDetector( + PROXY_HOST, PROXY_PORT, "*.iomete.cloud", null, noProxySelector()); + + // WHEN + ProxiedSocketAddress result = sut.proxyFor(TARGET); + + // THEN: bypass wins over explicit proxy + assertNull(result); + } + + @Test + void givenBypassDoesNotMatchTarget_whenProxyFor_thenReturnsProxy() throws IOException { + // GIVEN: bypass pattern does NOT match target + ArrowFlightProxyDetector sut = + new ArrowFlightProxyDetector( + PROXY_HOST, PROXY_PORT, "*.internal.net", null, noProxySelector()); + + // WHEN + ProxiedSocketAddress result = + sut.proxyFor(InetSocketAddress.createUnresolved("api.example.com", 443)); + + // THEN + assertProxyAddress(result, PROXY_HOST, PROXY_PORT); + } + + @Test + void givenMultipleBypassPatterns_whenProxyFor_thenAnyMatchBypasses() throws IOException { + // GIVEN: pipe-separated patterns (http.nonProxyHosts format) + ArrowFlightProxyDetector sut = + new ArrowFlightProxyDetector( + PROXY_HOST, + PROXY_PORT, + "localhost|*.internal|10.*|exact.host.com", + null, + noProxySelector()); + + // THEN: each pattern type is tested + assertNull(sut.proxyFor(InetSocketAddress.createUnresolved("localhost", 443)), "exact match"); + assertNull( + sut.proxyFor(InetSocketAddress.createUnresolved("db.internal", 5432)), "suffix wildcard"); + assertNull( + sut.proxyFor(InetSocketAddress.createUnresolved("10.0.0.1", 443)), "prefix wildcard"); + assertNull( + sut.proxyFor(InetSocketAddress.createUnresolved("exact.host.com", 443)), + "exact FQDN match"); + + // non-matching host goes through proxy + assertNotNull( + sut.proxyFor(InetSocketAddress.createUnresolved("external.com", 443)), + "non-matching host should use proxy"); + } + + @Test + void givenBypassPatternIsCaseInsensitive_whenProxyFor_thenMatchesRegardlessOfCase() + throws IOException { + // GIVEN: mixed-case bypass vs mixed-case target + ArrowFlightProxyDetector sut = + new ArrowFlightProxyDetector( + PROXY_HOST, PROXY_PORT, "*.Example.COM", null, noProxySelector()); + + // THEN + assertNull(sut.proxyFor(InetSocketAddress.createUnresolved("API.EXAMPLE.COM", 443))); + assertNull(sut.proxyFor(InetSocketAddress.createUnresolved("api.example.com", 443))); + } + } + + @Nested + // uses selector when no explicit proxy; null when selector returns DIRECT; picks first non-DIRECT + // from multi-proxy list; handles null ProxySelector + class ProxySelectorFallback { + + @Test + void givenNoExplicitProxyAndSelectorReturnsProxy_whenProxyFor_thenUsesSelector() + throws IOException { + // GIVEN: no explicit proxy, ProxySelector returns an HTTP proxy + String selectorHost = "192.168.1.2"; + int selectorPort = 3128; + ArrowFlightProxyDetector sut = + new ArrowFlightProxyDetector( + null, null, null, null, proxySelectorReturning(selectorHost, selectorPort)); + + // WHEN + ProxiedSocketAddress result = sut.proxyFor(TARGET); + + // THEN + assertProxyAddress(result, selectorHost, selectorPort); + } + + @Test + void givenNoExplicitProxyAndSelectorReturnsDirect_whenProxyFor_thenReturnsNull() + throws IOException { + // GIVEN: ProxySelector returns DIRECT (no proxy) + ArrowFlightProxyDetector sut = + new ArrowFlightProxyDetector(null, null, null, null, noProxySelector()); + + // WHEN + ProxiedSocketAddress result = sut.proxyFor(TARGET); + + // THEN + assertNull(result); + } + + @Test + void givenNoExplicitProxyAndSelectorReturnsMultiple_whenProxyFor_thenUsesFirstNonDirect() + throws IOException { + // GIVEN: ProxySelector returns [DIRECT, HTTP proxy] + ProxySelector selector = mock(ProxySelector.class); + InetSocketAddress proxyAddr = new InetSocketAddress("192.168.1.3", 9090); + List proxies = List.of(Proxy.NO_PROXY, new Proxy(Proxy.Type.HTTP, proxyAddr)); + when(selector.select(any(URI.class))).thenReturn(proxies); + + ArrowFlightProxyDetector sut = new ArrowFlightProxyDetector(null, null, null, null, selector); + + // WHEN + ProxiedSocketAddress result = sut.proxyFor(TARGET); + + // THEN: skips DIRECT, uses the HTTP proxy + assertProxyAddress(result, "192.168.1.3", 9090); + } + + @Test + void givenNullProxySelector_whenProxyFor_thenReturnsNull() throws IOException { + // GIVEN: null ProxySelector (can happen if JVM has no default) + ArrowFlightProxyDetector sut = new ArrowFlightProxyDetector(null, null, null, null, null); + + // WHEN + ProxiedSocketAddress result = sut.proxyFor(TARGET); + + // THEN + assertNull(result); + } + } + + @Nested + // returns null when nothing is configured + class NoProxyConfigured { + + @Test + void givenNothingConfigured_whenProxyFor_thenReturnsNull() throws IOException { + // GIVEN: no proxy, no bypass, not disabled, selector returns DIRECT + ArrowFlightProxyDetector sut = + new ArrowFlightProxyDetector(null, null, null, null, noProxySelector()); + + // WHEN + ProxiedSocketAddress result = sut.proxyFor(TARGET); + + // THEN + assertNull(result); + } + } + + @Nested + // forceDisabled > bypass > explicit > selector + class PriorityOrder { + + @Test + void givenForceDisabledWithExplicitProxyAndBypass_whenProxyFor_thenForceDisabledWins() + throws IOException { + // GIVEN: all settings configured, force disabled takes highest priority + ArrowFlightProxyDetector sut = + new ArrowFlightProxyDetector( + PROXY_HOST, + PROXY_PORT, + "*.other.com", + "force", + proxySelectorReturning("192.168.1.2", 3128)); + + // THEN + assertNull(sut.proxyFor(TARGET)); + } + + @Test + void givenBypassMatchesAndExplicitProxy_whenProxyFor_thenBypassWins() throws IOException { + // GIVEN: target matches bypass, explicit proxy also configured + ArrowFlightProxyDetector sut = + new ArrowFlightProxyDetector( + PROXY_HOST, + PROXY_PORT, + "*.iomete.cloud", + null, + proxySelectorReturning("192.168.1.2", 3128)); + + // THEN: bypass takes priority over explicit proxy + assertNull(sut.proxyFor(TARGET)); + } + + @Test + void givenExplicitProxyAndSelector_whenProxyFor_thenExplicitWins() throws IOException { + // GIVEN: both explicit proxy and ProxySelector configured + ArrowFlightProxyDetector sut = + new ArrowFlightProxyDetector( + PROXY_HOST, PROXY_PORT, null, null, proxySelectorReturning("192.168.1.2", 3128)); + + // WHEN + ProxiedSocketAddress result = sut.proxyFor(TARGET); + + // THEN: explicit proxy takes priority over selector + assertProxyAddress(result, PROXY_HOST, PROXY_PORT); + } + } + + private static ProxySelector noProxySelector() { + ProxySelector selector = mock(ProxySelector.class); + when(selector.select(any(URI.class))).thenReturn(Collections.singletonList(Proxy.NO_PROXY)); + return selector; + } + + private static ProxySelector proxySelectorReturning(String host, int port) { + ProxySelector selector = mock(ProxySelector.class); + InetSocketAddress proxyAddr = new InetSocketAddress(host, port); + Proxy proxy = new Proxy(Proxy.Type.HTTP, proxyAddr); + when(selector.select(any(URI.class))).thenReturn(Collections.singletonList(proxy)); + return selector; + } + + private static void assertProxyAddress( + ProxiedSocketAddress result, String expectedHost, int expectedPort) { + assertNotNull(result); + HttpConnectProxiedSocketAddress httpProxy = + assertInstanceOf(HttpConnectProxiedSocketAddress.class, result); + InetSocketAddress proxyAddr = (InetSocketAddress) httpProxy.getProxyAddress(); + assertEquals(expectedHost, proxyAddr.getHostString()); + assertEquals(expectedPort, proxyAddr.getPort()); + } +} diff --git a/flight/flight-sql-jdbc-driver/pom.xml b/flight/flight-sql-jdbc-driver/pom.xml index 559c42597d..b36019202c 100644 --- a/flight/flight-sql-jdbc-driver/pom.xml +++ b/flight/flight-sql-jdbc-driver/pom.xml @@ -22,7 +22,7 @@ under the License. org.apache.arrow arrow-flight - 19.0.0-SNAPSHOT + 19.0.0-iomete.1 flight-sql-jdbc-driver @@ -138,6 +138,14 @@ under the License. + + + IOMETE + IOMETE + 19.0.0-iomete.1 + Apache Arrow Java 19.0.0 + + META-INF/LICENSE.txt diff --git a/flight/flight-sql/pom.xml b/flight/flight-sql/pom.xml index a5954819c3..cc26976302 100644 --- a/flight/flight-sql/pom.xml +++ b/flight/flight-sql/pom.xml @@ -22,7 +22,7 @@ under the License. org.apache.arrow arrow-flight - 19.0.0-SNAPSHOT + 19.0.0-iomete.1 flight-sql diff --git a/flight/pom.xml b/flight/pom.xml index 2fc3e89ef8..5272f0baeb 100644 --- a/flight/pom.xml +++ b/flight/pom.xml @@ -22,7 +22,7 @@ under the License. org.apache.arrow arrow-java-root - 19.0.0-SNAPSHOT + 19.0.0-iomete.1 arrow-flight diff --git a/format/pom.xml b/format/pom.xml index d3578b63d2..7a8fdb6b43 100644 --- a/format/pom.xml +++ b/format/pom.xml @@ -23,7 +23,7 @@ under the License. org.apache.arrow arrow-java-root - 19.0.0-SNAPSHOT + 19.0.0-iomete.1 arrow-format diff --git a/gandiva/pom.xml b/gandiva/pom.xml index 5367bfdedf..a0c4069d2a 100644 --- a/gandiva/pom.xml +++ b/gandiva/pom.xml @@ -22,7 +22,7 @@ under the License. org.apache.arrow arrow-java-root - 19.0.0-SNAPSHOT + 19.0.0-iomete.1 org.apache.arrow.gandiva diff --git a/memory/memory-core/pom.xml b/memory/memory-core/pom.xml index 72ee69d60a..e65de94da1 100644 --- a/memory/memory-core/pom.xml +++ b/memory/memory-core/pom.xml @@ -22,7 +22,7 @@ under the License. org.apache.arrow arrow-memory - 19.0.0-SNAPSHOT + 19.0.0-iomete.1 arrow-memory-core diff --git a/memory/memory-netty-buffer-patch/pom.xml b/memory/memory-netty-buffer-patch/pom.xml index 07dc7d2403..c8ccc77f0f 100644 --- a/memory/memory-netty-buffer-patch/pom.xml +++ b/memory/memory-netty-buffer-patch/pom.xml @@ -22,7 +22,7 @@ under the License. org.apache.arrow arrow-memory - 19.0.0-SNAPSHOT + 19.0.0-iomete.1 arrow-memory-netty-buffer-patch diff --git a/memory/memory-netty/pom.xml b/memory/memory-netty/pom.xml index 6d660da117..9647482887 100644 --- a/memory/memory-netty/pom.xml +++ b/memory/memory-netty/pom.xml @@ -22,7 +22,7 @@ under the License. org.apache.arrow arrow-memory - 19.0.0-SNAPSHOT + 19.0.0-iomete.1 arrow-memory-netty diff --git a/memory/memory-unsafe/pom.xml b/memory/memory-unsafe/pom.xml index 92dc0c9fe5..f8c7f68122 100644 --- a/memory/memory-unsafe/pom.xml +++ b/memory/memory-unsafe/pom.xml @@ -22,7 +22,7 @@ under the License. org.apache.arrow arrow-memory - 19.0.0-SNAPSHOT + 19.0.0-iomete.1 arrow-memory-unsafe diff --git a/memory/pom.xml b/memory/pom.xml index bc34c26050..8ad4e850b2 100644 --- a/memory/pom.xml +++ b/memory/pom.xml @@ -22,7 +22,7 @@ under the License. org.apache.arrow arrow-java-root - 19.0.0-SNAPSHOT + 19.0.0-iomete.1 arrow-memory pom diff --git a/mise.toml b/mise.toml new file mode 100644 index 0000000000..262fd088c7 --- /dev/null +++ b/mise.toml @@ -0,0 +1,2 @@ +[tools] +java = "temurin-17" diff --git a/performance/pom.xml b/performance/pom.xml index 3f18188e3a..5389a792fd 100644 --- a/performance/pom.xml +++ b/performance/pom.xml @@ -22,7 +22,7 @@ under the License. org.apache.arrow arrow-java-root - 19.0.0-SNAPSHOT + 19.0.0-iomete.1 arrow-performance jar diff --git a/plan.md b/plan.md new file mode 100644 index 0000000000..1cc5737eff --- /dev/null +++ b/plan.md @@ -0,0 +1,102 @@ +# Plan: Package IOMETE Fork of Arrow Flight JDBC Driver + +## Context + +IOMETE maintains a fork of Apache Arrow Java with a gRPC proxy detector feature (`grpc-proxy-detector` branch). The fork adds HTTP CONNECT proxy tunneling support to the Flight SQL JDBC driver. We need to produce a distributable fat JAR that is identifiable as IOMETE's fork, distinct from the official Apache Arrow distribution. + +**Decisions made:** +- Publish only the fat JAR (`flight-sql-jdbc-driver`) — single dependency for consumers +- Keep `org.apache.arrow` groupId (drop-in replacement) +- Version: `19.0.0-iomete.1` (dot-separated, encodes upstream version + IOMETE qualifier + patch) +- Keep original Java package names (`org.apache.arrow.driver.jdbc.*`) for easy rebase +- Local build only for now; artifact manager TBD + +## Risk Mitigation (keeping `org.apache.arrow` groupId) + +Since we're keeping the official groupId, we must prevent accidental resolution conflicts: +- **Never publish to Maven Central** under `org.apache.arrow` without Apache PMC approval +- When publishing to a private repo, ensure it takes priority over Maven Central in consumer `settings.xml` +- The version `19.0.0-iomete.1` is distinct enough to avoid accidental resolution against official `19.0.0` + +## Steps + +### Step 1: Set version to `19.0.0-iomete.1` + +Use Maven's `versions:set` to change the version across all modules atomically: + +```bash +mise exec -- mvn versions:set -DnewVersion=19.0.0-iomete.1 -DgenerateBackupPoms=false +``` + +This updates the root POM and all child modules that inherit from it. The fat JAR will be produced as `flight-sql-jdbc-driver-19.0.0-iomete.1.jar`. + +**Files affected:** All `pom.xml` files (version inheritance). No manual edits needed. + +### Step 2: Build the entire project + +The fat JAR module (`flight-sql-jdbc-driver`) depends on several sibling modules. We must build from root: + +```bash +mise exec -- mvn install -DskipTests -Dcheckstyle.skip=true -Dspotless.check.skip=true +``` + +- `-DskipTests`: speed — tests were validated on the branch already +- `-Dcheckstyle.skip` + `-Dspotless.check.skip`: avoid style failures on generated/unmodified code + +### Step 3: Verify the fat JAR + +The shaded JAR will be at: +``` +flight/flight-sql-jdbc-driver/target/flight-sql-jdbc-driver-19.0.0-iomete.1.jar +``` + +Verify it contains the proxy detector and correct metadata: +```bash +# Check JAR size (should be ~40-60MB, it's a fat JAR) +ls -lh flight/flight-sql-jdbc-driver/target/flight-sql-jdbc-driver-19.0.0-iomete.1.jar + +# Verify proxy detector class is present +jar tf flight/flight-sql-jdbc-driver/target/flight-sql-jdbc-driver-19.0.0-iomete.1.jar | grep ProxyDetector + +# Verify JDBC ServiceLoader registration +jar xf flight/flight-sql-jdbc-driver/target/flight-sql-jdbc-driver-19.0.0-iomete.1.jar META-INF/services/java.sql.Driver +cat META-INF/services/java.sql.Driver + +# Check Maven metadata in JAR +jar tf flight/flight-sql-jdbc-driver/target/flight-sql-jdbc-driver-19.0.0-iomete.1.jar | grep pom.properties +``` + +### Step 4: (Optional) Add IOMETE manifest entry + +Add a custom manifest attribute to the shade plugin config in `flight/flight-sql-jdbc-driver/pom.xml` for traceability: + +```xml + + + IOMETE + IOMETE + 19.0.0-iomete.1 + Apache Arrow Java 19.0.0 + + +``` + +**File:** `flight/flight-sql-jdbc-driver/pom.xml` (shade plugin `` section) + +### Step 5: Commit the version change + +Commit on the `grpc-proxy-detector` branch with a clear message indicating the IOMETE release version. + +## Verification + +1. Fat JAR exists at expected path with correct filename +2. `ProxyDetector` class is inside the JAR (grep confirms) +3. `META-INF/services/java.sql.Driver` points to `org.apache.arrow.driver.jdbc.ArrowFlightJdbcDriver` +4. JAR can be loaded as a JDBC driver: `java -cp org.apache.arrow.driver.jdbc.ArrowFlightJdbcDriver` (no ClassNotFoundException) +5. `pom.properties` inside JAR shows `version=19.0.0-iomete.1` + +## Future Considerations (not in scope now) + +- **Artifact manager**: When ready, add `` to root POM pointing to GitHub Packages / Nexus / Artifactory +- **CI/CD**: GitHub Actions workflow to build + publish on git tag `v19.0.0-iomete.*` +- **Upstream rebase**: When Arrow releases 20.0.0, rebase branch, bump to `20.0.0-iomete.1` diff --git a/pom.xml b/pom.xml index 19625617b1..f0da582645 100644 --- a/pom.xml +++ b/pom.xml @@ -28,7 +28,7 @@ under the License. org.apache.arrow arrow-java-root - 19.0.0-SNAPSHOT + 19.0.0-iomete.1 pom Apache Arrow Java Root POM @@ -92,7 +92,7 @@ under the License. - 1695310533 + 1773860567 ${project.build.directory}/generated-sources 1.9.0 5.12.2 @@ -611,6 +611,7 @@ under the License. false true false + /Users/mateus/workspace/conductor-ai/repos/arrow-java/.git false false diff --git a/tools/pom.xml b/tools/pom.xml index d43adb1fdf..cdc261a9a2 100644 --- a/tools/pom.xml +++ b/tools/pom.xml @@ -22,7 +22,7 @@ under the License. org.apache.arrow arrow-java-root - 19.0.0-SNAPSHOT + 19.0.0-iomete.1 arrow-tools Arrow Tools diff --git a/vector/pom.xml b/vector/pom.xml index f46bd0e7b4..cdc0b6e0d0 100644 --- a/vector/pom.xml +++ b/vector/pom.xml @@ -22,7 +22,7 @@ under the License. org.apache.arrow arrow-java-root - 19.0.0-SNAPSHOT + 19.0.0-iomete.1 arrow-vector Arrow Vectors