Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
042afe0
fix: Use gRPC DnsNameResolver for periodic DNS re-resolution
phillipleblanc Mar 25, 2026
72a28ba
feat: Add reset() method, gRPC keep-alive, and transport resilience
lukekim Mar 25, 2026
7724c13
Merge remote-tracking branch 'origin/trunk' into phillipleblanc/fix-d…
lukekim Mar 25, 2026
ce5eb54
fix: Resolve merge conflicts and fix TimeUnit ambiguity
lukekim Mar 25, 2026
beea47a
fix: Ensure FlightStream is closed after query in ResetTest
lukekim Mar 25, 2026
d8c2328
chore: Add CI quality checks, address PR review comments
lukekim Mar 25, 2026
55ea00e
chore: Add permissions block to quality CI job
lukekim Mar 25, 2026
da8fdd8
fix: Add SpotBugs exclusion filter and synchronize queryInternal method
lukekim Mar 25, 2026
9aed5e0
perf: Use snapshot-under-lock in queryInternal for concurrent query t…
lukekim Mar 25, 2026
522c037
perf: Add HTTP connect timeout and double-checked locking for ADBC init
lukekim Mar 25, 2026
137d129
chore: Cap gRPC inbound message size at ~2 GiB and metadata at 16 MiB
lukekim Mar 25, 2026
1718068
perf: Fix medium-priority performance issues (#5-#8)
lukekim Mar 25, 2026
947bb51
fix: Address PR review comments (round 3)
lukekim Mar 25, 2026
d8d736a
fix: Remove outdated commit message template
lukekim Mar 25, 2026
c8fa9a1
fix: Address PR review comments (round 4)
lukekim Mar 25, 2026
6f55f6c
fix: Fix CI failures - SpotBugs exclusion, integration test tables, r…
phillipleblanc Mar 26, 2026
5a464d8
fix: Address Copilot review comments
phillipleblanc Mar 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,14 @@ jobs:
path: target/surefire-reports/
retention-days: 7

- name: Upload coverage report
if: always()
uses: actions/upload-artifact@v4
with:
name: coverage-report-${{ matrix.os }}
path: target/site/jacoco/
retention-days: 7

build:
runs-on: ubuntu-latest
timeout-minutes: 20
Expand Down Expand Up @@ -178,3 +186,52 @@ jobs:
name: test-results-jdk${{ matrix.java.version }}-${{ matrix.java.distribution }}
path: target/surefire-reports/
retention-days: 7

quality:
name: Code quality checks
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: read
steps:
- uses: actions/checkout@v4

- name: Set up JDK 17 (Oracle)
uses: actions/setup-java@v4
with:
java-version: 17
distribution: oracle
cache: maven

- name: Build
run: mvn install -DskipTests=true -Dgpg.skip -B -V

- name: Maven Enforcer
run: mvn validate -B

- name: SpotBugs
run: mvn spotbugs:check -B

- name: Checkstyle
run: mvn checkstyle:check -B

- name: Cache OWASP Dependency-Check data
uses: actions/cache@v4
with:
path: ~/.m2/repository/org/owasp/dependency-check-data
key: dependency-check-data-${{ runner.os }}-${{ hashFiles('**/pom.xml') }}
restore-keys: |
dependency-check-data-${{ runner.os }}-

- name: OWASP Dependency-Check
env:
NVD_API_KEY: ${{ secrets.NVD_API_KEY }}
run: mvn dependency-check:check -B
Comment thread
lukekim marked this conversation as resolved.

- name: Upload dependency-check report
if: always()
uses: actions/upload-artifact@v4
with:
name: dependency-check-report
path: target/dependency-check-report.html
retention-days: 30
38 changes: 38 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,44 @@ SpiceClient client = SpiceClient.builder()
.build();
```

### Long-lived Clients and Transport Resilience

The `SpiceClient` is designed for long-lived reuse. The underlying gRPC channel uses `dns:///` resolution, which periodically re-resolves hostnames so clients automatically recover from load-balancer IP rotation (e.g. AWS NLB). HTTP/2 keep-alive is enabled by default (30s interval, 10s timeout) to detect dead connections quickly.

For the rare case where the transport becomes permanently stuck (e.g. TLS handshake to a wrong backend, persistent `UNAVAILABLE` after retries), use `reset()` to discard the bad connection and immediately establish a fresh one:

```java
SpiceClient client = SpiceClient.builder()
.withApiKey(API_KEY)
.withSpiceCloud()
.build();

// Long-lived usage with transport recovery.
// isTransportFailure() is application-defined; check for
// io.grpc.StatusRuntimeException with Status.UNAVAILABLE,
// SSLHandshakeException, or similar transport-level errors.
try {
try (FlightStream stream = client.query(sql)) {
// process results...
}
} catch (ExecutionException e) {
if (isTransportFailure(e.getCause())) {
client.reset(); // discard bad transport, reconnect immediately
try (FlightStream stream = client.query(sql)) {
// process results with fresh connection...
}
} else {
throw e;
}
}
Comment thread
lukekim marked this conversation as resolved.
```

**DNS cache TTL:** The gRPC `DnsNameResolver` respects the JVM's DNS cache TTL. For more aggressive DNS refresh (recommended for cloud-deployed clients), set the JVM property:

```bash
-Dnetworkaddress.cache.ttl=30
```

### Iterating Through Results

For more control over query results, you can iterate through rows and access individual field values:
Expand Down
79 changes: 78 additions & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,8 @@
<artifactId>maven-surefire-plugin</artifactId>
<version>3.5.4</version>
<configuration>
<argLine>--add-opens=java.base/java.nio=ALL-UNNAMED</argLine>
<!-- @{argLine} is set by JaCoCo's prepare-agent goal -->
<argLine>@{argLine} --add-opens=java.base/java.nio=ALL-UNNAMED</argLine>
</configuration>
</plugin>
<plugin>
Expand Down Expand Up @@ -148,6 +149,82 @@
<waitUntil>published</waitUntil> -->
</configuration>
</plugin>
<!-- P0: Static bug detection — null derefs, concurrency, resource leaks -->
<plugin>
<groupId>com.github.spotbugs</groupId>
<artifactId>spotbugs-maven-plugin</artifactId>
<version>4.9.3.0</version>
<configuration>
<excludeFilterFile>spotbugs-exclude.xml</excludeFilterFile>
</configuration>
</plugin>
<!-- P0: CVE scanning for transitive dependencies (Netty, gRPC, Arrow) -->
<plugin>
<groupId>org.owasp</groupId>
<artifactId>dependency-check-maven</artifactId>
<version>12.1.1</version>
<configuration>
<failBuildOnCVSS>7</failBuildOnCVSS>
</configuration>
</plugin>
<!-- P1: Test coverage reporting -->
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.13</version>
<executions>
<execution>
<id>prepare-agent</id>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>report</id>
<phase>test</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
</executions>
</plugin>
<!-- P2: Build hygiene — enforce Maven/JDK versions, dependency convergence -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-enforcer-plugin</artifactId>
<version>3.5.0</version>
<executions>
<execution>
<id>enforce</id>
<goals>
<goal>enforce</goal>
</goals>
<configuration>
<rules>
<requireMavenVersion>
<version>[3.6.0,)</version>
</requireMavenVersion>
<requireJavaVersion>
<version>[11,)</version>
</requireJavaVersion>
<banDuplicatePomDependencyVersions/>
</rules>
</configuration>
</execution>
</executions>
</plugin>
<!-- P3: Code style consistency (Google Java Style) -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-checkstyle-plugin</artifactId>
<version>3.6.0</version>
<configuration>
<configLocation>google_checks.xml</configLocation>
<consoleOutput>true</consoleOutput>
<violationSeverity>warning</violationSeverity>
<failOnViolation>false</failOnViolation>
</configuration>
</plugin>
</plugins>
</build>
</project>
47 changes: 47 additions & 0 deletions spotbugs-exclude.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
SpotBugs exclusion filter for pre-existing findings.
These are findings in code that predates the introduction of SpotBugs.
They should be addressed in follow-up PRs.
-->
<FindBugsFilter>
<!-- Example code: dead stores are intentional (demonstrating all param types) -->
<Match>
<Package name="ai.spice.example"/>
Comment thread
lukekim marked this conversation as resolved.
<Bug pattern="DLS_DEAD_LOCAL_STORE"/>
</Match>

<!-- Example code: rough constants like 3.14 are intentional demo values for float params -->
<Match>
<Package name="ai.spice.example"/>
<Bug pattern="CNT_ROUGH_CONSTANT_VALUE"/>
</Match>

Comment thread
lukekim marked this conversation as resolved.
<!-- Config: dead store in getUserAgent() -->
<Match>
<Class name="ai.spice.Config"/>
<Bug pattern="DLS_DEAD_LOCAL_STORE"/>
</Match>

<!-- HeaderAuthMiddlewareFactory: internal representation exposure (by design, internal class) -->
<Match>
<Class name="ai.spice.HeaderAuthMiddlewareFactory"/>
<Bug pattern="EI_EXPOSE_REP2"/>
</Match>

<!-- RefreshOptions: public fields are part of the public API -->
<Match>
<Class name="ai.spice.RefreshOptions"/>
<Bug pattern="PA_PUBLIC_PRIMITIVE_ATTRIBUTE"/>
</Match>

<!-- Constructor-throw warnings: these constructors intentionally validate/fail early -->
<Match>
Comment thread
lukekim marked this conversation as resolved.
<Class name="ai.spice.SpiceClient"/>
<Bug pattern="CT_CONSTRUCTOR_THROW"/>
</Match>
<Match>
<Class name="ai.spice.SpiceClientBuilder"/>
<Bug pattern="CT_CONSTRUCTOR_THROW"/>
</Match>
Comment thread
lukekim marked this conversation as resolved.
</FindBugsFilter>
8 changes: 5 additions & 3 deletions src/main/java/ai/spice/HeaderAuthMiddlewareFactory.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,21 +21,23 @@ public HeaderAuthMiddlewareFactory(ClientIncomingAuthHeaderMiddleware.Factory au

@Override
public FlightClientMiddleware onCallStarted(CallInfo callInfo) {
// Create the auth middleware once per RPC, not once per callback
final FlightClientMiddleware authMiddleware = authFactory.onCallStarted(callInfo);
return new FlightClientMiddleware() {
@Override
public void onBeforeSendingHeaders(CallHeaders callHeaders) {
authFactory.onCallStarted(callInfo).onBeforeSendingHeaders(callHeaders);
authMiddleware.onBeforeSendingHeaders(callHeaders);
headers.forEach(callHeaders::insert);
}

@Override
public void onHeadersReceived(CallHeaders callHeaders) {
authFactory.onCallStarted(callInfo).onHeadersReceived(callHeaders);
authMiddleware.onHeadersReceived(callHeaders);
}

@Override
public void onCallCompleted(CallStatus callStatus) {
authFactory.onCallStarted(callInfo).onCallCompleted(callStatus);
authMiddleware.onCallCompleted(callStatus);
}
};
}
Expand Down
Loading
Loading