Repository navigation
Add gRPC multi-query support (onlineQueryMulti) - #210
Merged
Merged
Conversation
Adds ChalkClient.onlineQueryMulti, which runs several independent online
queries in parallel over a single request using the OnlineQueryMulti RPC.
The generated stubs for that RPC were already vendored in this repo but
had never been wired up, so this is a client-side change only.
Callers with N unrelated queries -- different roots, different outputs --
previously paid N round trips.
Details:
- New OnlineQueryMultiResult (AutoCloseable) exposes the per-query results
positionally, plus the request-level errors and the shared trace id.
Each sub-result owns its own Arrow child allocator, because
OnlineQueryResult.close() closes the allocator it is handed; sharing one
would release it while other sub-results' tables still held buffers.
Closing the wrapper closes them all.
- GRPCClient.onlineQuery is decomposed into serializeInputs,
buildBulkRequest, queryStub and decodeBulkResponse, now shared by both
paths so single and multi build byte-identical sub-requests. A golden
test asserts buildBulkRequest field by field to hold that line.
- Every sub-query is sent as a bulk_request: the engine rejects
single_request outright ("Please use OnlineQueryBulkRequest w/ a single
row"), matching the reference Python client.
- A request carries one deadline and one set of headers, so the queries
must agree on environmentId, branch, queryName and queryNameVersion --
the API server resolves one branch to one deployment and routes named
queries by an exact match on x-chalk-query-name, so a mixed batch could
not be honored. The deadline is the longest per-query timeout.
- decodeBulkResponse now rethrows on failure. It previously closed its
tables and allocator but fell through, returning an OnlineQueryResult
wrapping already-closed Arrow objects. The multi path cannot tolerate
that: a mid-batch failure would leak the sibling allocators.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Fixes found reviewing the multi-query settings resolvers. - resolveDeadline dropped the client-level timeout as soon as any one query set its own. A query that sets no timeout is bounded by the client-level timeout, so that is its effective timeout and it has to take part in the maximum; otherwise a client built with a 5m timeout batching a 1s query alongside an unset one deadlined the whole request at 1s, killing a query that would have had 5m on its own. This contradicted the method's own stated invariant. A query that is effectively unbounded now yields no deadline at all. - resolveEnvironment compared raw per-query values, so naming the client's own environment on one query and leaving it unset on another was reported as a conflict even though both resolve to the same environment. It now compares the effective value, as resolveBranch already did. - Dropped the queryNameVersion agreement requirement. The justification does not hold for the version: there is no x-chalk-query-name-version header, and each sub-request carries its own version in its OnlineQueryContext, so queries differing only by version are satisfiable. - unmarshal() NPE'd on a null scalars table, which is exactly what a sub-query that failed to execute leaves behind, so the documented empty slot made the README's own pattern fail with an opaque error. It now explains where to find the reason. - Guard the varargs overload against a null array so it reports the same ClientException as the List overload rather than a raw NPE, and drop a repeated format argument that made the count-mismatch message tautological. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
ChalkClient.onlineQueryMulti, which runs several independent online queries in parallel over a single request, via thechalk.engine.v1.QueryService/OnlineQueryMultiRPCOnlineQueryMultiResult: positional per-query results, request-level errors, shared trace id, oneclose()for the whole batchGRPCClient.onlineQueryintoserializeInputs/buildBulkRequest/queryStub/decodeBulkResponse, now shared by both paths so single and multi build byte-identical sub-requestsdecodeBulkResponseto rethrow on a decode failure instead of returning a result wrapping already-closed Arrow objectsThe generated stubs for this RPC were already vendored in
ai.chalk.protosand had never been wired up, so there is no proto regeneration here. Callers with N unrelated queries — different roots, different outputs — previously paid N round trips.Usage
There is also a varargs overload for the common case:
Use
onlineQueryfor one query over many rows — it is already a bulk operation.onlineQueryMultiis for queries that are unrelated to each other.Semantics
getResults()holds one result per query, in the order passed. The engine pre-sizes its response vector, so the counts always match; a mismatch is rejected client-side rather than silently misattributed, since position is the only correlation the proto offers.getErrors()and leaves the others intact.getGlobalErrors()with its 1-based position in the message text, while its slot ingetResults()holds an empty result reporting no errors of its own. An empty result is therefore not proof of success — check the global errors too. This is documented on the API and covered by a test.bulk_request. The engine rejectssingle_requestoutright ("OnlineQueryRequest is unsupported; Please use OnlineQueryBulkRequest w/ a single row."), which is also what the reference Python client does.environmentId,branchandqueryNameor the call throwsClientExceptionnaming both conflicting indices: the API server resolves one branch to one deployment, and named queries are routed by an exact header match onx-chalk-query-name, so a mixed batch could not be honored. Leaving one of these unset just takes the client's value, which is not a disagreement, andqueryNameVersionmay differ freely since each sub-request carries its own. The deadline is the longest of the effective per-query timeouts, where a query that sets none is bounded by the client-level timeout; per-query timeouts are not independently enforced.Allocator ownership
Each sub-result gets its own Arrow child allocator.
OnlineQueryResult.close()closes the allocator it is handed, so a shared allocator would be released by the first sub-result closed while the others' tables still held buffers. Closing theOnlineQueryMultiResultcloses every sub-result; callers should not close them individually. A failure partway through decoding closes everything already decoded and retains no Arrow memory.Behavior change
decodeBulkResponse(the extracted decode step, previously inline inonlineQuery) now rethrows. The old code closed its tables and allocator on a decode failure but then fell through toreturn, handing back anOnlineQueryResultwhose Arrow objects were already closed — so any use of it threw anyway. The multi path cannot tolerate this, since a mid-batch failure would also leak the sibling allocators. This turns an obscure downstreamIllegalStateExceptioninto aClientExceptionwith a message.Review fixes (second commit)
A review pass over the settings resolvers turned up several problems, all fixed in
0450ccb:resolveDeadlinedropped the client-level timeout as soon as any one query set its own. A client built with.withTimeout(5m)batching a 1s query alongside a query with no timeout deadlined the whole request at 1s, killing a query that would have had 5 minutes on its own — the opposite of the "no sub-query is cancelled before the time its caller allowed" invariant the method claimed. An unset per-query timeout means the client-level timeout, so it now takes part in the maximum, and a query that is effectively unbounded yields no deadline at all.resolveEnvironmentcompared raw values, so naming the client's own environment on one query and leaving it unset on another was rejected as a conflict even though both resolve identically. It now compares the effective value, asresolveBranchalready did.queryNameVersionagreement requirement. The routing justification does not apply to the version: there is nox-chalk-query-name-versionheader, and each sub-request carries its own version in itsOnlineQueryContext, so queries differing only by version are satisfiable. (Agreement on the name stays, since Envoy route-matches on it.)unmarshal()NPE'd on a null scalars table — exactly what the empty slot above leaves behind — so the documented trap made this PR's own README pattern fail with an opaque "Failed to unmarshal" wrapping an NPE. It now names the cause and points atgetGlobalErrors(), and the README example guards the case explicitly.ClientExceptionas theListoverload for a null array instead of a raw NPE, and a repeated format argument that made the count-mismatch message tautological is gone.Verification
TestGrpcMultiQuery(new, 23 cases, no engine required) — all passedTestGrpcMultiQuery,TestOnlineQueryParams,ai.chalk.arrow.*,ai.chalk.internal.*,ai.chalk.flink.*)compileJava,compileTestJava,javadoc,build,shadowJar— all succeed; javadoc warnings unchanged frommaintestDecodeMultiResponseDecodeFailureLeaksNothingandtestDecodeBulkResponseReleasesAllocatorOnFailureforce an Arrow decode failure and assertgetAllocatedMemory() == 0. The first fails against the pre-fix decode path.buildBulkRequestfield by field, which is what guards the single-query path against drift during the refactorTestGrpcClientand the REST-rejection test inTestAllClients. They compile but need a live deployment, and there were noCHALK_*credentials in this environment. CI supplies them.Follow-ups (filed, not fixed here)
withBranch()is inert on the gRPC path.RequestHeaderInterceptornever sendsX-Chalk-Branch-Id, andproxyForRequestreads branch only from that header —OnlineQueryContext.branch_idis[deprecated = true]("set in the header. This field is not set anymore") and is read by neither the API server nor the engine. This affects single queries equally, so it is out of scope here, but it meansonlineQueryMultivalidates branch agreement without the branch being honored. Called out in the Javadoc and README. Same story forX-Chalk-Preview-Deployment.stalenessandstorePlanStagesare never sent on the gRPC path, despiteOnlineQueryBulkRequest.stalenessexisting. Needs aDuration→ bucket-string encoding decision; affects single and multi.OnlineQueryResultFeather.close()closes a shared allocator once per map entry — latent in the REST path, masked only becausetoResult()reads the single key"0". Must be fixed before REST multi-query; a TODO now sits at that call site.queryNameis stricter than the Python gRPC client, which allows mixed names and sends no query-name header at all. Java's gRPC client does send it, and Envoy route-matches on it, hence the stricter rule. Relaxing a validation later is backward-compatible, so this starts strict — but it is the decision most worth a second opinion, since "queries rooted in different feature classes" often means differently-named queries in production.validateMultiParamsrejects aqueryNameVersionset without aqueryName, which plainonlineQuerycurrently accepts and forwards. This is a deliberate port of the reference client's rule (resolve_multi_query_query_name), but it does mean a params object that works viaonlineQueryis refused here; applying it toonlineQuerytoo would be a behavior change needing a release note.maxInboundMessageSize, so very large batches will hit that ceiling sooner than the equivalent separate queries.