Repository navigation
feat: async query submission, status, results, and cancellation - #56
Conversation
Adds queryAsync()/queryAsyncWithParams(), returning an AsyncQuery handle (status/waitForCompletion/results/cancel) backed by the runtime's Flight DoAction async-query actions. Requires the runtime in distributed/scheduler mode. Purely additive - query()/queryWithParams() are unchanged.
There was a problem hiding this comment.
Pull request overview
Adds additive asynchronous query execution through Flight DoAction, including status polling, cancellation, and Arrow result retrieval.
Changes:
- Adds async query APIs and lifecycle status handling.
- Implements chunked Arrow result reading.
- Adds mock-server tests and usage documentation.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
SpiceClient.java |
Implements async Flight actions and public submission APIs. |
AsyncQuery.java |
Provides polling, cancellation, and result access. |
AsyncQueryResultReader.java |
Streams chunked Arrow results. |
QueryStatus.java |
Defines async query lifecycle states. |
AsyncQueryTest.java |
Tests async query behavior. |
TestFlightSqlServer.java |
Mocks async Flight actions. |
README.md |
Documents async query usage. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
* release: prepare v0.8.0 Bumps pom.xml/Version.java to 0.8.0 and adds release notes covering the four additive feature PRs (#53-#56): search, Nsql/NsqlGenerateSql, active-query management, and async queries. japicmp.oldVersion stays at 0.6.0 rather than bumping to 0.7.0: v0.7.0 was tagged and GitHub-released but its Maven Central publish never completed, so it isn't a resolvable dependency. See the pom.xml comment for detail. * docs: reflect that #54 (search) and #55 (nsql) have merged * docs: scope v0.8.0 to search + nsql only #53 (active queries) and #56 (async queries) will not merge for this release; remove them from the release notes' What's New and reflect that in Release status instead of listing them as still-pending dependencies. * Update release notes for v0.8.0 Removed release status section and notes on features not included in v0.8.0. * Update release notes for v0.8.0 Removed testing section from release notes for v0.8.0. * Update release notes for v0.8.0 Removed highlights section from release notes for v0.8.0. * Change japicmp.oldVersion from 0.6.0 to 0.7.0 Updated the old version for japicmp to 0.7.0.
# Conflicts: # src/main/java/ai/spice/SpiceClient.java
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.
Suppressed comments (3)
src/main/java/ai/spice/AsyncQueryResultReader.java:122
results()passesMath.max(chunkCount, 1), so this reader cannot distinguish a declared zero-chunk result from a real one-chunk result. AnyExecutionExceptionwhile fetching the sole real chunk (for example expiration, authentication, or transport failure) is therefore silently converted into a successful empty reader. Preserve the declared count separately and suppress the missing chunk only whentotal_chunk_countwas actually zero.
if (index == 0 && chunkCount <= 1) {
return false;
src/main/java/ai/spice/AsyncQuery.java:108
- The timeout does not bound this wait. A status RPC can block past the deadline, and the unconditional 500 ms sleep can overshoot a shorter timeout; the next poll may even return
SUCCEEDEDafter the deadline and be accepted because terminal status is checked first. Propagate the remaining deadline to each statusDoActionand cap polling sleep to the remaining duration.
public QueryStatus waitForCompletion(Duration timeout) throws ExecutionException {
long deadlineNanos = (timeout == null) ? -1 : System.nanoTime() + timeout.toNanos();
while (true) {
QueryStatus current = status();
src/main/java/ai/spice/SpiceClient.java:1827
- This applies
withQueryTimeouttoGetAsyncQueryResult, so downloading a large async result chunk can be aborted by the planning timeout. That contradicts the documented contract inSpiceClientBuilder.java:244-247that result streaming is not limited by this timeout. Use the auth-only stream options for the result action while retainingcallOptionsfor control-plane actions.
Iterator<Result> results = channel.rawClient.doAction(new Action(actionType, requestBody),
channel.callOptions);
- Preserve the runtime-declared chunk count separately from the schema-probe floor, so a real chunk-0 fetch failure on a declared non-empty result propagates instead of being reported as an empty result (AsyncQueryResultReader). - Bound each status poll in waitForCompletion to the caller's remaining timeout, and cap the poll-interval sleep to the same remaining time, so the deadline is an actual wall-clock cap. - Use the auth-only stream options for async result-chunk downloads instead of the options carrying the planning/query timeout. - Retry an async DoAction exactly once on a freshly rebuilt channel after UNAUTHENTICATED, matching the sync query paths' behavior. - Capture the submitted DoAction body in the test mock and assert the serialized sql/parameters for a parameterized async submit. - Remove the unused Action parameter flagged by CodeQL in the test mock's status/cancel handlers.
…s ones Duration#toMillis() truncates toward zero, so a sub-millisecond remaining poll budget became a 0ms gRPC deadline (expire-immediately) instead of a near-zero one, and the resulting DEADLINE_EXCEEDED was reported as a generic transport failure rather than a timeout. Round the per-call deadline up instead of down, and translate a real RPC-level timeout into the same "Timed out" wording waitForCompletion's own deadline check uses.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.
Suppressed comments (1)
src/main/java/ai/spice/AsyncQuery.java:118
- A finite timeout is inferred from the sign of
deadlineNanos, butSystem.nanoTime()has an arbitrary origin and may be negative, and adding the duration can overflow. In either casedeadlineNanos >= 0can be false and this method silently treats a finite timeout as an indefinite wait. Tracktimeout != nullseparately and compute remaining time from elapsednanoTime()differences rather than using the deadline's sign as a sentinel.
long deadlineNanos = (timeout == null) ? -1 : System.nanoTime() + timeout.toNanos();
while (true) {
Duration remaining = null;
if (deadlineNanos >= 0) {
# Conflicts: # src/main/java/ai/spice/SpiceClient.java
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.
Suppressed comments (2)
src/main/java/ai/spice/AsyncQuery.java:115
System.nanoTime()may be negative, and adding a finite timeout may wrap into the sign bit. UsingdeadlineNanos >= 0as the bounded/unbounded sentinel can therefore turn a bounded wait into an indefinite one. Track whether a timeout was supplied separately; nano-time subtraction remains valid across wraparound.
long deadlineNanos = (timeout == null) ? -1 : System.nanoTime() + timeout.toNanos();
src/main/java/ai/spice/AsyncQuery.java:170
- These are two different status snapshots. The runtime defines
CLOSEDas expired results, so if a query changes fromSUCCEEDEDduringwaitForCompletion()toCLOSEDon this poll,finalStatusstill passes the success check while this response has no result metadata. The reader then defaults to zero chunks and suppresses the missing chunk-0 error, silently returning an empty result. Use the status from the same response as the metadata (or return the terminal response from the wait helper).
QueryStatus finalStatus = waitForCompletion();
JsonObject statusResponse = this.client.asyncQueryStatus(this.queryId);
…es async (#58) * docs: add active query management and async queries to v0.8.0 notes #53 (listActiveQueries/cancelActiveQuery) has merged to trunk since v0.8.0's release notes were first written, and #56 (queryAsync/ queryAsyncWithParams) is expected to merge before release. Both are purely additive, so the compatibility statement is unchanged. * Update release notes for v0.8.0 Added documentation for asynchronous query execution and active query management features. * docs: address review comments on v0.8.0 release notes - Move the AsyncQuery-handle paragraph (status/waitForCompletion/cancel) back under Async Queries; it was left stranded under Active Query Management by a later reordering of the two sections. - Reword "Public API unchanged" to "Additive, backward-compatible change" -- new public methods were added, so the API did change, just without breaking anything existing. * feat!: rename query/queryWithParams to sql/sqlWithParams; queryAsync becomes query Matches the breaking-rename pattern already shipped in the dotnet, js, and python SDKs: query()/queryWithParams() now submit SQL for asynchronous execution and return an AsyncQuery handle, and the previous synchronous, streaming behavior moves to new sql()/ sqlWithParams() methods. - SpiceClient.query(String) -> SpiceClient.sql(String) - SpiceClient.queryWithParams(String, Object...) -> SpiceClient.sqlWithParams(String, Object...) - SpiceClient.queryAsync(String) -> SpiceClient.query(String) - SpiceClient.queryAsyncWithParams(String, Object...) -> SpiceClient.queryWithParams(String, Object...) Updates every call site and cross-reference in src/main, src/test, README.md, and docs/parameterized_queries.md. Historical release notes (v0.5.0.md, v0.6.0.md) are left untouched since they accurately document what those versions actually shipped at the time. Full test suite passes unchanged in behavior -- this is a pure rename, no logic changes. * docs: document query/queryWithParams -> sql/sqlWithParams as breaking Replaces the "Additive, backward-compatible change" compatibility statement, which the rename in the previous commit made false, with an actual Breaking Changes section describing it: query()/queryWithParams() now submit for asynchronous execution and return an AsyncQuery handle; the previous synchronous, streaming behavior moved to sql()/ sqlWithParams(). Also fixes two now-stale sync-path cross-references (Nsql and Async Queries sections) that still said query()/ queryWithParams() where they meant the new sql()/sqlWithParams(). * build: excuse query/queryWithParams from the japicmp compatibility gate The gate correctly caught the intentional breaking change: query()'s return type changed from FlightStream to AsyncQuery, and queryWithParams()'s from ArrowReader to AsyncQuery, since both now submit for asynchronous execution instead of streaming results directly. Documented, scoped exclusions for exactly these two methods keep the gate meaningful for catching any other, unintended breaking change in this or a future release. Verified locally with the exact CI command (mvn checkstyle:check japicmp:cmp) -- BUILD SUCCESS, and the generated report confirms SpiceClient is otherwise fully binary- and source-compatible with the published 0.7.0.
Adds asynchronous query support:
queryAsync(sql)/queryAsyncWithParams(sql, params...)submit a query and return anAsyncQueryhandle (status(),waitForCompletion(),results(),cancel()), backed by the Spice runtime's FlightDoActionasync-query actions. This requires the runtime to be running in distributed/scheduler mode.This brings spice-java up to parity with the equivalent (currently unreleased) capability in gospice. Unlike gospice's v9, which made this change by repurposing
Query/QueryWithParams(a breaking change), this SDK keepsquery()/queryWithParams()untouched and adds the async path as new, additive API under anAsync-suffixed name, consistent with spice-dotnet's naming convention and this SDK's japicmp compatibility gate.Includes unit tests against an in-process mock Flight server (no live runtime required) and a README section.