Skip to content

Threading refactor (4/7): Pool & op-executor contract#4

Open
igorbernstein2 wants to merge 9 commits into
threading-refactor-phase-3from
threading-refactor-phase-4
Open

Threading refactor (4/7): Pool & op-executor contract#4
igorbernstein2 wants to merge 9 commits into
threading-refactor-phase-3from
threading-refactor-phase-4

Conversation

@igorbernstein2

Copy link
Copy Markdown
Owner

Summary

Phase 4 of 7. Routes PendingVRpc per-op state through the op executor, consolidates cancel trampolines at VOperationImpl, and replaces the OpExecutor backing with an inline-capable queue.

Test infrastructure first: a 30-second @Timeout on the session/pool tests so a callback-dispatch regression surfaces as a clear failure instead of a silent hang.

What's in here

Commit What it does
test: add 30-second @Timeout to all session/pool integration tests Class-level JUnit 5 @Timeout(30) on RetryingVRpcTest, VRpcTracerTest, ClientTest, TableBaseTest, SessionImplTest, SessionPoolImplTest.
Converts silent hangs (orphaned futures when the dispatch chain breaks) into bounded failures.
chore: route PendingVRpc per-op state through the op executor (+ deadline-monitor fix) PendingVRpc.cancel and drainTo move isCancelled/realCall onto ctx.getExecutor(); the pool lock now covers only queue /
poolState / session list. Plus: PendingVRpc.start arms the deadline monitor only AFTER committing to the queue (previously a closed-pool fast-fail would leak the timer, which then fired and corrupted tracer
per-attempt metrics).
chore: consolidate cancel trampolines at VOperationImpl VOperationImpl captures opExecutor in start() and trampolines start/cancel via it. RetryingVRpc.start runs synchronously on the op-executor task.
tracer.onOperationStart reordered before started=true (a throwing tracer short-circuits to direct listener.onClose). listener.onMessage failures classify as USER_FAILURE.
chore: replace OpExecutor backing with an inline-capable queue; tighten Client.close (+ FQ-name cleanup) OpExecutor switches from SequentialExecutor to an internal ArrayDeque + drain loop, and gains
runInline() — runs synchronously on the caller when idle, otherwise queues. VOperationImpl uses runInline for chain.start so the start dispatch skips the queue+drain round-trip. Client.close drains
userCallbackExecutor first via a 5s-bounded shutdownAndAwait. Drive-by: replace fully-qualified type names with imports; swap Guava Objects.hashCode(id) for Long.hashCode(id) in ChannelPoolDpImpl.AfeId.

Stack position

  • Base: threading-refactor-phase-3
  • Next: threading-refactor-phase-5 — close ordering & shutdown hardening

@igorbernstein2
igorbernstein2 force-pushed the threading-refactor-phase-4 branch from 64d0864 to bb3fd91 Compare June 17, 2026 20:02
@igorbernstein2
igorbernstein2 force-pushed the threading-refactor-phase-3 branch from 8f18528 to b32ff78 Compare June 17, 2026 20:02
@igorbernstein2
igorbernstein2 force-pushed the threading-refactor-phase-3 branch from b32ff78 to 5c4f476 Compare June 22, 2026 21:41
@igorbernstein2
igorbernstein2 force-pushed the threading-refactor-phase-4 branch from bb3fd91 to 1d18231 Compare June 22, 2026 21:41
Tests that call Future.get() on vRpc chains can hang indefinitely if an
exception breaks the callback dispatch chain and orphans the future (see
ISSUE-006). A class-level JUnit 5 @timeout(30) converts a silent hang into
a clear test failure within a bounded time.

Applied to: RetryingVRpcTest, VRpcTracerTest, ClientTest, TableBaseTest,
SessionImplTest, SessionPoolImplTest.
PendingVRpc.cancel and drainTo move isCancelled/realCall onto
ctx.getExecutor(); the pool lock now covers only queue / poolState /
session list. close() switches to cancelWithResult to honor the new
op-executor contract. NOOP_CALL sentinel removed.

PendingVRpc.start arms the deadline monitor only AFTER committing to the
queue (inside the synchronized block). Previously the timer was scheduled
before the pool-state check, so the closed-pool fast-fail path returned
without cancelling it — the timer then fired later and called
listener.onClose a second time with DEADLINE_EXCEEDED. RetryingVRpc.Active
suppressed the duplicate at the user-facing layer, but
tracer.onAttemptFinish still ran twice and corrupted per-attempt metrics.

Adds SessionPoolImplTest#pendingVRpcOnClosedPoolDoesNotLeakDeadlineMonitor.
VOperationImpl captures opExecutor in start() and trampolines start/cancel
via it. RetryingVRpc.start runs synchronously on the op-executor task
RetryingVRpc.cancel no longer wraps in execute. Tracer.onOperationStart
reordered before started=true (a throwing tracer short-circuits to direct
listener.onClose). listener.onMessage failures classify as USER_FAILURE.
CleanupListener tracks a closed flag to prevent gRPC-context listener
leaks on synchronous chain close.
…en Client.close

OpExecutor switches from a SequentialExecutor backing to an internal
ArrayDeque + drain loop, and gains runInline() — runs the task synchronously
on the caller thread when the executor is idle, otherwise queues it.
VOperationImpl uses runInline for chain.start so the start dispatch skips
the queue+drain round-trip. Drain rejections (e.g. during shutdown) reset
drainScheduled so subsequent submissions can retry.

Client.close drains userCallbackExecutor first via a 5s-bounded
shutdownAndAwait so pool.close's cancelWithResult onClose notifications
complete before the executor is torn down. Resource.close becomes idempotent.

Drive-by cleanups: replace fully-qualified type names with imports across
touched files, and swap Guava Objects.hashCode(id) for Long.hashCode(id) in
ChannelPoolDpImpl.AfeId to avoid per-call boxing and array allocation.
The tracer reaching RetryingVRpc is always CompositeVRpcTracer or
NoopVrpcTracer (both produced by Metrics.newTableTracer); both
guarantee no-throw: NoopVrpcTracer is empty-body, CompositeVRpcTracer
wraps every child in try/catch and swallows. The defensive try/catch
here and the elaborate `started=true` ordering comment justifying it
were unreachable.

If a future contributor wires in a tracer that bypasses Composite, the
OpExecutor uncaught handler installed by VOperationImpl.start is the
remaining safety net.
…n paths

Previously the cancelled-pending-drain path in SessionPoolImpl synthesized a
Duration.ZERO + Status.CANCELLED result and routed through onVRpcFinish,
relying on its internal isOk() gate to skip the picker-latency update. The
sentinel value at the call site was misleading and forced readers to chase
through onVRpcFinish to confirm nothing was actually being measured.

Split SessionHandle.onVRpcFinish into:
  - onVRpcFinish(elapsed, result): vRPC reached the wire; updates picker
    latency on success as before.
  - onPendingVRpcCancelled(): vRPC was cancelled before attaching to a real
    call; pool-occupancy bookkeeping only.

Both share a private releaseToPool() helper. SessionPoolImpl mirrors the
split with onVRpcComplete + onPendingVRpcCancelled, factoring the post-
release tryDrain logic into afterRelease (annotated @GuardedBy("this")).

Each call site now names what happened; no more Duration.ZERO sentinel and
no more synthesized CANCELLED result. Behaviorally identical: the only
intra-method ordering change (latency update happens after the AFE re-add
inside onVRpcFinish) is invisible under the pool's synchronized (this).

Adds a SessionListTest case asserting that onPendingVRpcCancelled performs
the same bookkeeping as the OK path but does not touch the AFE's latency
stats.
@igorbernstein2
igorbernstein2 force-pushed the threading-refactor-phase-3 branch from 5c4f476 to e4ca57f Compare June 29, 2026 17:20
@igorbernstein2
igorbernstein2 force-pushed the threading-refactor-phase-4 branch from 1d18231 to d824fd4 Compare June 29, 2026 17:20
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