fix(bigquery): BQAA P1 preview blockers — branch-safe tracing, lifecycle bounds, boundary redaction, HITL pause/resume#1344
Conversation
|
Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA). View this failed invocation of the CLA check for more information. For the most up to date status, view the checks section at the bottom of the pull request. |
Code Review ResultsScope: PR #1344, Triage Groups
P1 -- High
Requirements Completeness
Actionable Findings
Coverage
Actionable Findings Recap
|
…on, HITL pair keys, fail-closed redaction, atomic finalize gate, per-operation tool spans Resolves all five review findings on the previous commit: 1. BatchProcessor.close() now waits (bounded by the same shutdownTimeout deadline) for an in-flight flush to release the flush lock before tearing down the writer and Arrow resources, instead of treating an empty queue as proof that no flush is active. 2. HITL_*_COMPLETED rows (both the event path and the user-message path) now carry pause_kind and function_call_id, so completions are joinable to their HITL_*_REQUEST / TOOL_PAUSED rows when multiple HITL requests share an invocation. 3. New JsonFormatter.redactTree replaces the smartTruncate-based attributes boundary: raw containers are walked natively with keys redacted before any Jackson conversion, and an unserializable leaf becomes "[UNSERIALIZABLE]" instead of routing the whole map through the textual fallback that exposed sibling secrets. JsonNode leaves are handled before the Iterable branch (ObjectNode iterates its values). 4. The invocation-finalization gate is now atomic: the isProcessed check runs inside computeIfAbsent's mapping function under the per-key lock, paired with finalize's mark-before-remove ordering; the residual tombstone-cache-expiry window is documented. 5. Tool spans carry an operation identity (ToolContext.functionCallId) and a parent captured at push time: ADK runs an event's function calls concurrently by default within one branch, so TOOL_STARTING rows are stamped from the pushed record, completions/errors pop by identity rather than stack position, and parents skip concurrent siblings. Tests: 160 focused BQAA tests pass (previous commit: 153), full core suite 1,616 passed / 0 failed. New coverage: close-waits-for-in-flight- flush, per-leaf fail-closed redaction (including nested containers and converted POJO leaves), HITL completion pair keys on both paths, and same-branch concurrent tools with out-of-order identity pops. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
All five findings are addressed in #1 — #2 — HITL completion drops its pair keys → Fixed. #3 — redaction fallback fails open → Fixed. #4 — finalization gate check/act race + expiring tombstone → Fixed (with documented residual). #5 — parallel tools cross-pop within one branch → Fixed. Verification: focused BQAA suite 160 passed / 0 failed (previous head: 153); full 🤖 Generated with Claude Code |
Code Review ResultsScope: PR #1344, Triage Groups
P1 -- High
Actionable Findings
Coverage
Actionable Findings Recap
|
…-truncation state redaction, durable lifecycle tokens Resolves the three remaining P1 findings from the second PR google#1344 review: 1. BatchProcessor teardown is now ownership-transferred, never concurrent: flush uses a ReentrantLock; close() publishes its deadline (capping drain/in-flight appends to the remaining budget), requests teardown, and waits for the mutex within the deadline. If the wait expires, the in-flight flush performs the teardown when it releases the mutex — resources are never destroyed under an active flush. Final drop stats are delivered via closeAndFold's callback at actual teardown completion, so counters recorded by that last flush (e.g. its append failure) reach plugin-level folding exactly once. 2. Session state is redacted BEFORE truncation: smartTruncate's whole-object fallback stringifies the map on one unserializable value, which put embedded secrets beyond the final redaction boundary. getAttributes now routes session.state() through the fail-closed redactTree first, then length-bounds the JsonNode. 3. Per-invocation lifecycle tokens are durable: each append continuation captures its InvocationLifecycle at logEvent time; finalization marks the token before removing the processor mapping. Unlike the bounded processed-invocations cache, the captured token cannot be evicted, so a continuation completing arbitrarily late can never resurrect a processor. The map entry is removed at finalize for memory bounds; outstanding continuations keep the token reachable. Tests: close-waits-for-real-blocked-append (replacing the manual lock toggle), deferred-teardown-and-stats-delivery past the close deadline, plugin-path session-state redaction with an unserializable sibling, and token-survives-tombstone-eviction (cache invalidated via reflection, late append still dropped, no processor resurrected). Focused BQAA suite 163 passed (previous: 160); full core 1,619 passed / 0 failed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
All three round-2 findings are addressed in #2 — session state stringified before the redaction boundary → Fixed. #3 — tombstone eviction resurrects processors → Fixed with a durable token. #1 — deadline-expired teardown under an active flush, late counters lost → Fixed with ownership transfer.
Both requested test shapes are in: Verification: focused BQAA suite 163 passed / 0 failed (previous head: 160); full Remaining known gap from your testing-gaps list: the Runner-level two-turn HITL flow is still covered at callback level rather than through a real 🤖 Generated with Claude Code |
Code Review ResultsScope: PR #1344, Triage Groups
P1 -- High
Actionable Findings
Coverage
Actionable Findings Recap
|
…le close-owned drain deadline Resolves both round-3 P1 findings on PR google#1344: 1. Append admission is now atomic with lifecycle finalization: InvocationLifecycle.runIfActive executes the processor append inside the token's monitor, shared with markFinalized. A continuation that passed the processor-map gate can no longer get descheduled across finalization and then offer into a drained queue or record an after_close drop that the already-delivered final snapshot misses; finalization now waits for any in-flight admission (a short, non-blocking-offer critical section), and post-finalization admissions are refused and counted at the plugin level. 2. The explicit pre-close flush in invocation finalization is removed: it ran before close() published its deadline and consumed a separate full append budget, doubling the effective drain bound with multiple queued batches. closeAndFold now owns the complete drain under one shutdownTimeout deadline. Cleanup per review: the stale tombstone-residual javadoc on getOrCreateProcessorIfActive now describes the durable-token guarantee, and the TraceManager class javadoc no longer claims within-branch execution is sequential (tools run concurrently and use operation- identity spans). Tests: finalization blocks on an in-flight admission and refuses the next one (latch-based interleaving), and a two-batch never-completing drain finishes within a single shutdownTimeout bound. Focused BQAA suite 165 passed (previous: 163); full core 1,621 passed / 0 failed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Both round-3 findings are addressed in #1 — append admission not atomic with finalization → Fixed with a shared token critical section. #2 — pre-close flush escapes the close deadline → Fixed. Cleanup notes:
On the Runner-level HITL verdict: agreed with your assessment — I'll land the two-turn Runner test before checking off that tracker acceptance item, as a follow-up to this PR. Verification: focused BQAA suite 165 passed / 0 failed (previous head: 163); full 🤖 Generated with Claude Code |
Fresh full review at
|
…lision-proof tool identity, HITL content parity Resolves both round-4 P1 findings and the P2 on PR google#1344: 1. One absolute deadline per shutdown operation: ensureInvocationCompleted computes its deadline at subscription (Completable.defer) and shares it between the pending-task wait and the processor drain; plugin-wide close() shares one deadline across the task wait, every processor's drain, and both executor terminations. BatchProcessor gains deadline-accepting closeAndFold/close variants so each drain consumes only the remaining budget — a stuck parse plus a queued processor, or N stuck processors, are all bounded by a single shutdownTimeout. 2. Collision-proof tool operation identity: the framework materializes an absent function-call ID as "", so two concurrent id-less calls collided and cross-popped. toolOperationId now uses the real ID when non-empty, else a synthetic ID keyed by ToolContext instance identity (weak-keys cache; the same instance flows through before/after/error callbacks of one call). hitlPairKeys also filters empty-string IDs, which are useless as join keys. 3. HITL completion content normalized to "result" on the event path, matching the user-message path and the Python plugin, so one event type has one queryable content shape. Tests: concurrent id-less tools keep span ownership out of completion order (plugin-level, span-id pairing per tool); multi-batch drain bound tightened to <900ms so the old two-deadline behavior fails; new pending-task-plus-drain and multi-stuck-processor cases assert the shared bound; HITL event-path completion asserts content.result. Focused BQAA suite 168 passed (previous: 165); full core 1,624 passed / 0 failed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
All three round-4 findings are addressed in P1 — shared absolute shutdown deadline → Fixed. P1 — id-less tool calls collide on P2 — HITL completion content divergence → Fixed. Verification: focused BQAA suite 168 passed / 0 failed (previous head: 165); full Standing follow-up unchanged: the Runner-level two-turn HITL test before the tracker acceptance item is checked off. 🤖 Generated with Claude Code |
Code Review ResultsScope: PR #1344, Triage Groups
P1 -- High
Actionable Findings
Coverage
Actionable Findings Recap
|
…eadline-safe Storage Writer entry and teardown Resolves both round-5 P1 findings on PR google#1344: 1. Finalization is completion-ordered: RxJava's doFinally notifies the downstream BEFORE running its action, so blockingAwait()/subscribers could observe success while lifecycle marking, processor draining, and teardown were still running. Both ensureInvocationCompleted and plugin-wide close now run an idempotent cleanup action via onErrorComplete().andThen(Completable.fromAction(cleanup)) — cleanup completes before the returned Completable does — with the same action in a trailing doFinally to cover disposal. 2. Storage Writer calls obey the public bound. Admission: StreamWriter's default LimitExceededBehavior.Block parks append() up to five minutes on inflight quota; the writer is now built with ThrowException, so quota saturation surfaces as an accounted append failure instead of an unbounded block. Teardown: StreamWriter.close() can block minutes (thread join + client/callback-pool waits); it is detached to a daemon thread so teardownOnce — and the public close()/deferred-flush path — honors the absolute deadline. No further work touches the writer at that point, and drop counters are final before delivery. Tests: new completion-ordering regression (async pending-task completion with latch-blocked cleanup; the returned Completable must not complete until cleanup releases); blocking StreamWriter.close() must not block the public close() while the writer is still closed eventually; the multi-processor close bound is tightened to <900ms so the old per-processor-deadline behavior fails; writer.close() verifications use timeout() now that teardown is detached. Focused BQAA suite 170 passed (previous: 168); full core 1,626 passed / 0 failed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Both round-5 findings are addressed in #1 — #2 — Storage Writer entry/teardown escape the bound → Fixed on both sides.
Test-gap items from your coverage notes: the multi-processor close bound is tightened from Verification: focused BQAA suite 170 passed / 0 failed (previous head: 168); full Standing follow-ups unchanged: Runner-level two-turn HITL test before the tracker acceptance item is checked off, and the CLA on this fork account. 🤖 Generated with Claude Code |
Code Review ResultsScope: PR #1344, P1 -- High
Actionable Findings
Coverage
Actionable Findings Recap
|
Resolves the round-6 P1 on PR google#1344: detached StreamWriter closes no longer spawn one raw daemon thread per completed invocation. Teardown submits the close to a plugin-owned bounded closer service (2 daemon threads, 256-task backlog, core timeout) shared by all BatchProcessors, so a Storage outage converts invocation throughput into a bounded backlog instead of native-thread exhaustion. Saturation policy: never block teardown and never spawn unbounded threads — a rejected close is counted ("writer_close_saturated", unit: writers) and the StreamWriter is left for BigQueryWriteClient shutdown to reclaim at the transport level. Plugin-wide close shuts the closer service down within the same shared absolute deadline, interrupting closes still blocked past it. Tests: 25 completed invocations with indefinitely blocked writer closes grow closer threads by at most the pool bound (delta-based, immune to sibling-test instances), leave no retained processors, and plugin shutdown still completes within its bound with the close backlog pending. Focused BQAA suite 171 passed (previous: 170); full core 1,627 passed / 0 failed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
The round-6 P1 is addressed in #1 — unbounded per-invocation closer threads → Fixed with a plugin-owned bounded closer service.
Requested regression is in: Verification: focused BQAA suite 171 passed / 0 failed (previous head: 170); full On the remaining coverage note: agreed the 🤖 Generated with Claude Code |
Round 7 fresh review — 1 P1 remainsI re-reviewed the complete PR at P1 — saturation and forced shutdown abandon live
|
ad83e20 to
e7537a7
Compare
|
The round-7 P1 is addressed in Your source trace was right and my round-6 comment was wrong: a non-pooling
Requested regressions:
Verification: focused BQAA suite 173 passed / 0 failed (previous head: 171); full Remaining gates: 🤖 Generated with Claude Code |
Round 8 fresh review — 1 P1 remainsI re-reviewed the complete squashed PR at P1 — processor construction can still abandon a permitted writer
There is a second interleaving with the same root cause. This is reachable before pending-task coordination protects it: for an already-completed Suggested fixEstablish a cleanup owner immediately after permit acquisition, before
A simpler implementation can use a global closing gate plus post-publication recheck, provided processor creation and the close iteration have an explicit atomic handoff. Do not hold a global mutex across potentially blocking Requested regressions:
P3 — stale shutdown comments
All findings from rounds 1–7 remain fixed for their covered interleavings. The branch is correctly squashed to one commit. Verdict: not ready until cleanup ownership covers the interval from permit acquisition through processor publication. |
e7537a7 to
5de0d93
Compare
|
Both round-8 items are addressed in the amended single commit P1 — ownership gap from permit acquisition to publication → Fixed with the suggested lease design.
Both requested regressions are in:
P3 — stale comments → Fixed. The shutdown comment now describes the actual drain-without-interruption/reclaim-owner behavior, and the round-7 test is renamed to Verification: focused BQAA suite 175 passed / 0 failed (previous head: 173); full Remaining gates unchanged: 🤖 Generated with Claude Code |
…d lifecycle, boundary redaction, HITL pause/resume Resolves the plugin-local P1 items from the BQAA preview readiness tracker (google#1316) across seven review rounds. Framework-level terminal agent/run error callbacks remain intentionally deferred to a separate framework API change. Tracing correctness: - TraceManager keeps ID-only span records (no plugin-owned OTel spans are created or exported; ambient trace IDs are still inherited) in per-branch stacks keyed by InvocationContext.branch(), with kind-checked pops, so concurrent ParallelAgent branches cannot pop each other's spans. Tool spans additionally carry an operation identity — the function-call ID when present, else a per-ToolContext synthetic ID (the framework materializes absent IDs as "") — plus a push-time parent, because ADK executes an event's function calls concurrently within one branch. Privacy boundary: - JsonFormatter.redactTree walks raw containers with sensitive-key redaction BEFORE any Jackson conversion and fails closed per leaf ("[UNSERIALIZABLE]"), and the assembled attributes tree passes through it at the output boundary. Session state is redacted before truncation so smartTruncate's whole-map textual fallback can never expose embedded secrets. Lifecycle and shutdown: - Per-invocation lifecycle tokens are durable (captured by every append continuation; immune to tombstone-cache eviction) and append admission is atomic with finalization via the token's monitor. - One absolute deadline bounds each shutdown operation end-to-end: pending-task waits, every processor drain, and executor termination share it; finalization is completion-ordered (cleanup runs before the returned Completable completes, since RxJava's doFinally notifies downstream first) with disposal covered by the same idempotent action. - BatchProcessor teardown is ownership-transferred (ReentrantLock; an in-flight flush past the deadline performs the teardown), appends are bounded (get(timeout) plus LimitExceededBehavior.ThrowException so Storage quota saturation cannot park append() for minutes), and the final drop-stat snapshot is delivered at true teardown completion. - Live StreamWriters are bounded by admission permits acquired BEFORE construction (each writer owns an internal client and a non-daemon append thread; MAX_LIVE_WRITERS <= closer queue capacity, so a constructed writer can never lose its cleanup owner). Detached closes run on a bounded plugin-owned closer service; at plugin shutdown the unstarted queue drains to a bounded reclaim owner without interrupting active closes. Rows refused at the cap are accounted ("writer_permit_exhausted"), alongside "after_close", "shutdown_timeout", "writer_create_error", and "late_after_finalize". Event contract: - Synthetic adk_request_* function calls emit HITL_*_REQUEST (not _COMPLETED); long-running calls emit pairable TOOL_PAUSED rows with pause_kind and function_call_id; user-message handling inspects FunctionResponse parts, routing HITL responses to HITL_*_COMPLETED (with pair keys, content.result on both producer paths, Python parity) and non-HITL responses to TOOL_COMPLETED with pair keys. Adds the TOOL_PAUSED view and pair-key columns on TOOL_COMPLETED. Tests: focused BQAA suite 173 passed (upstream baseline: 135); full core main profile 1,629 passed / 0 failed / 24 skipped. New regressions cover parallel-branch and concurrent id-less tool span ownership, zero exported spans, nested and fail-closed redaction (attributes and session state), atomic admission/finalization interleavings, durable tokens across cache eviction, single-deadline multi-batch / task-plus-drain / multi-processor shutdown bounds, completion-ordered cleanup, blocked-append and blocked-close teardown ownership, writer permit exhaustion, and shutdownNow-leftover reclaim. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
5de0d93 to
70d7d54
Compare
|
Round-9 P3 addressed in the amended single commit With the round-9 runtime verdict clean across all nine review rounds, the PR is moving out of draft. Remaining items tracked outside this PR: 🤖 Generated with Claude Code |
Summary
Resolves the plugin-local P1 items from the BQAA Java preview readiness tracker (#1316) — the correctness, lifecycle, privacy-boundary, and duplicate-telemetry blockers that can be fixed inside the
agentanalyticspackage. All changes verified against the tracker's pinned commit08f4fdcand mirrored, where applicable, on the Python plugin's contract atgoogle/adk-python@9d306f5.Intentionally excluded: "Add terminal agent/run failure analytics" (the last P1) requires new framework plugin API (
onAgentErrorCallback/onRunErrorCallback, mirroring Python's RFC #5044) and should land as its own framework change; this PR only covers defects local to the plugin.Fixes (tracker item → change)
1. Trace state safe for concurrent
ParallelAgentbranchesTraceManagernow keeps span records in per-branch stacks keyed byInvocationContext.branch()(distinct per ParallelAgent sub-agent viaBaseAgent.createInvocationContext). Agent and model spans within a branch are sequential and use per-branch LIFO; tool spans carry an operation identity and push-time parent because ADK runs an event's function calls concurrently by default; a branch completing first can no longer pop another branch's span. Pops are additionally kind-checked (invocation,agent:*,llm_request,tool), so an error callback firing without its matching push cannot pop an unrelated record. Parent resolution walks the branch ancestor chain, so a branch's first span parents to the invocation root.2. No plugin-owned OpenTelemetry spans
TraceManagerno longer callstracer.spanBuilder(...).startSpan()— records are ID-only (16-hex local IDs). Ambient OTel context is still consulted fortrace_id(and the invocation root'sspan_id) so rows stay joinable to Cloud Trace, but a host with an SDK exporter configured receives zero BQAA-owned spans. Regression test asserts this with an in-memory exporter.3. Sensitive-key redaction at the final attributes boundary
logEventnow passes the fully assembled attributes map through the redaction walk immediately before serialization, regardless of producer —state_delta, custom tags, labels, and nested extra attributes included. Redaction-only (no length truncation; does not setis_truncated, matching Python).4. Per-invocation processor/task lifecycle
BatchProcessor.start()stores itsScheduledFuture; an idempotentclose()cancels it, so completed invocations no longer leave periodic tasks retaining closed processors/writers until plugin-wide shutdown.5. Bounded append, drain, and timeout cleanup
writer.append(...).get(shutdownTimeout)bounds each Storage Write RPC (the future is cancelled on timeout); the final drain inclose()is bounded by ashutdownTimeoutdeadline instead of looping until empty. Rows abandoned at the deadline are counted (shutdown_timeout); rows appended after close are rejected and counted (after_close).6. Writer-construction and late-continuation loss accounting
New
PluginState.appendRowgates every append on the invocation lifecycle: a late parse/offload continuation completing afterensureInvocationCompletedfinalized the invocation is dropped and counted (late_after_finalize) instead of silently recreating a processor nothing will close.StreamWriterconstruction failures are logged SEVERE and counted (writer_create_error); the mapping stays unpopulated so the next event retries construction.7. HITL and long-running-tool pause/resume semantics
adk_request_*FunctionCall now emits the plainHITL_*_REQUESTevent (previously mislabeled_COMPLETED).TOOL_PAUSEDrow withpause_kind(hitl_credential/hitl_confirmation/hitl_input/tool, derived from the call name) andfunction_call_id.HITL_*_COMPLETED(neverTOOL_COMPLETED); non-HITL responses — by construction the resume side of a paused long-running tool — emitTOOL_COMPLETEDcarrying the pause pair keys.TOOL_PAUSEDview andpause_kind/function_call_idcolumns on theTOOL_COMPLETEDview so the pause↔resume join works end-to-end in SQL.Verification
coremain profile: 1,631 passed, 0 failures/errors, 24 skipped (upstream baseline: 1,591).state_delta, in unserializable-sibling attribute trees, and in session state ahead of truncation; HITL request/pause/complete rows correctly named and pairable withpause_kind/function_call_idon completions.Review rounds addressed
redactTreeboundary, atomic finalize gate, per-operation tool spans. Note: within one branch, ADK executes an event's function calls CONCURRENTLY by default — tool spans therefore carry an operation identity (ToolContext.functionCallId) and a push-time parent; only non-tool kinds use top-of-stack semantics.closeAndFoldstats delivery at true teardown completion; session-state redaction beforesmartTruncate's whole-object fallback; durable per-invocation lifecycle tokens captured by every append continuation, immune to tombstone-cache eviction.runIfActiveshared withmarkFinalized); removed the pre-close flush socloseAndFoldowns the complete drain.shutdownTimeout); collision-proof tool operation identity for id-less calls (the framework materializes absent IDs as"") via a weak-keys per-ToolContext synthetic ID; HITL completion content normalized toresulton both producer paths (Python parity).andThen(fromAction)BEFORE the returned Completable completes, since RxJava'sdoFinallynotifies downstream first; disposal covered by the same idempotent action); deadline-safe Storage Writer usage (LimitExceededBehavior.ThrowExceptionmakes append admission nonblocking with accounted rejections, andStreamWriter.close()is detached to a daemon thread so teardown honors the absolute deadline).writer_close_saturated), shut down within the shared shutdown deadline — a Storage outage becomes a bounded backlog instead of native-thread exhaustion.writer_permit_exhausted); at plugin shutdown the closer's unstarted queue drains to a bounded reclaim owner WITHOUT interrupting active closes. Branch squashed to a single commit per repo requirement.WriterLeaseis registered BEFOREcreateWriter()and owns the permit and (once attached) the writer; construction/start()failures and plugin close racing admission all dispatch the writer through the lease's dispatch-once protocol; plugin close publishes aclosinggate, drains every live lease, and identity-removes published processors (creators racing publication self-close via a post-publication recheck — exactly one party wins, never neither). Stale round-7 shutdown comments and test naming reworded to the queue-drain/no-interruption behavior.Tracker: #1316
🤖 Generated with Claude Code