Skip to content

Threading refactor (6/7): Tracer correctness & chain.isDone()#6

Open
igorbernstein2 wants to merge 3 commits into
threading-refactor-phase-5from
threading-refactor-phase-6
Open

Threading refactor (6/7): Tracer correctness & chain.isDone()#6
igorbernstein2 wants to merge 3 commits into
threading-refactor-phase-5from
threading-refactor-phase-6

Conversation

@igorbernstein2

Copy link
Copy Markdown
Owner

Summary

Phase 6 of 7. Two structural cleanups motivated by the new threading model:

  1. Per-attempt tracer pairing moves into Active state with an onExit safety net — fixes three tracer-leak hazards (synchronous attempt.start throw, cancelled in-flight attempts, double-fire on stale onClose).

What's in here

Commit What it does
refactor: add State.onExit and localize per-attempt tracer pairing to Active (+ two tracer fixes) State machine adds an onExit hook called from onStateChange before the swap. Active owns its own tracer-pairing
flag + finishAttempt helper. Listener paths, onCancel, and onExit safety net all call finishAttempt so tracer.onAttemptFinish is always balanced — including cancelled in-flight attempts (no longer dropped by the
stale-state guard) and synchronous attempt.start throws. Listener-path tracer.onAttemptFinish is gated on the stale-state check first, matching onMessage. Scheduled cleanup (timer + stop-hook unregister) consolidates
from three sites to one onExit.
refactor: replace CleanupListener.closed flag with chain.isDone() VOperationImpl.start needs to detect whether chain.start synchronously delivered a terminal onClose, to skip registering the gRPC cancellation
listener (which would otherwise leak onto grpcContext). Previously tracked via a closed flag on CleanupListener — a piggyback field that existed only for VOperationImpl's coordination. Add isDone() to the VRpc
interface and ask the chain directly. CleanupListener shrinks back to its single concern: relay events and unhook the gRPC cancellation listener on close.

Stack position

  • Base: threading-refactor-phase-5
  • Next: threading-refactor-phase-7 — documentation

@igorbernstein2
igorbernstein2 force-pushed the threading-refactor-phase-6 branch 3 times, most recently from fb931b0 to c212e84 Compare June 17, 2026 20:02
@igorbernstein2
igorbernstein2 force-pushed the threading-refactor-phase-5 branch from 6a94038 to 49dd066 Compare June 17, 2026 20:02
private boolean attemptFinished = false;

private void finishAttempt(VRpcResult result) {
if (!attemptFinished) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can this be called from different threads? compareAndSet?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nope, the primary purpose of this refactor is to make all of this be single threaded

@igorbernstein2
igorbernstein2 force-pushed the threading-refactor-phase-5 branch from 49dd066 to de62fa1 Compare June 22, 2026 21:41
@igorbernstein2
igorbernstein2 force-pushed the threading-refactor-phase-6 branch 2 times, most recently from 852ed0b to c503a03 Compare June 23, 2026 20:55
@igorbernstein2
igorbernstein2 force-pushed the threading-refactor-phase-5 branch from b82a480 to 8f643ad Compare June 29, 2026 17:20
@igorbernstein2
igorbernstein2 force-pushed the threading-refactor-phase-6 branch from c503a03 to 7864221 Compare June 29, 2026 17:20
… Active

State machine now has an onExit hook called from onStateChange before the
swap. States that hold cleanup-worthy resources override it.

Active owns its own tracer-pairing flag + finishAttempt helper (no longer
a RetryingVRpc-level bool). Listener paths and onCancel call finishAttempt
with the right result; onExit is a safety net that guarantees the pairing
is balanced even if a future exit path forgets. Fixes the
attempt.start synchronous-throw leak.

Also resolves two latent tracer hazards:
- tracer.onAttemptFinish now fires when an in-flight attempt is cancelled
  (previously the late server onClose was dropped by the stale-state guard,
  so the cancelled attempt's tracer span leaked).
- The listener-path tracer.onAttemptFinish is gated on the stale-state
  check first, matching onMessage above — a discarded onClose can no
  longer double-fire the tracer.

Scheduled cleanup (timer + stop-hook unregister) consolidates from three
parallel sites (onCancel, timer-fire body, stop-hook body) into one onExit.
onCancel drops to the default no-op.

Done.onStart no longer balances per-attempt tracer state — that lives on
Active now.
VOperationImpl.start needs to detect whether chain.start synchronously
delivered a terminal onClose, so it can skip registering the gRPC
cancellation listener (which would otherwise leak onto grpcContext).
Previously this was tracked via a 'closed' flag on CleanupListener — a
piggyback bookkeeping field on the listener wrapper that exists only
for VOperationImpl's coordination.

Add isDone() to the VRpc interface and ask the chain directly. The
chain is the natural source of truth for its own terminal state.
CleanupListener shrinks back to its single concern: relay events and
unhook the gRPC cancellation listener on close.

Implementations: RetryingVRpc delegates to currentState.isDone();
VRpcImpl reports state==CLOSED; ForwardingVRpc forwards; PendingVRpc
defers to realCall once handed off, otherwise reports isCancelled.
Test fakes (DelayedVRpc, FakeVRpc, anonymous VOperationImplTest chains)
implement the new method.

Drive-by: drop the defensive handling of tracer.onOperationStart throws
from RetryingVRpc.start, and the symmetric `!started` early-return in
Done.onStart that paired with it. CompositeVRpcTracer catches throws
from every child tracer, so the only way tracer.onOperationStart reaches
RetryingVRpc with a real throw is a test that bypasses Composite. Dead
code in production; relying on the existing chain.cancel cascade is
simpler than maintaining a separate short-circuit path.

Also: the already-started error in RetryingVRpc.start now dispatches
listener.onClose through ctx.getExecutor() rather than invoking it
synchronously on the caller, matching the dispatch convention used
everywhere else in the chain.
@igorbernstein2
igorbernstein2 force-pushed the threading-refactor-phase-6 branch from 7864221 to be9a7b3 Compare June 30, 2026 17:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants