Skip to content

Threading refactor (2/7): SessionImpl — synchronized(lock) → SynchronizationContext#2

Open
igorbernstein2 wants to merge 6 commits into
threading-refactor-phase-1from
threading-refactor-phase-2
Open

Threading refactor (2/7): SessionImpl — synchronized(lock) → SynchronizationContext#2
igorbernstein2 wants to merge 6 commits into
threading-refactor-phase-1from
threading-refactor-phase-2

Conversation

@igorbernstein2

Copy link
Copy Markdown
Owner

Summary

Phase 2 of 7. Replaces the per-SessionImpl Object lock with an explicit SynchronizationContext. Builds on the infrastructure introduced in [Phase 1].

The migration is staged so each step keeps tests green:

  1. Add the syncContext alongside the existing lock.
  2. Move RPC mutations onto it (startRpc / cancelRpc).
  3. Remove the lock entirely.
  4. Wire up uncaught-exception abort.

What's in here

Commit What it does
chore: add session SynchronizationContext alongside the existing lock sessionSyncContext serializes stream callbacks. onMessage/onClose dispatch onto it; the per-session heartbeat tick trampolines through it.
synchronized(lock) blocks remain inside the handlers — the two coexist for now. Affinity asserts added at boundary methods.
chore: make startRpc and cancelRpc async via sessionSyncContext SessionImpl.startRpc and cancelRpc now submit to sessionSyncContext rather than running synchronously on the caller. VRpcSessionApi.startRpc
becomes void — errors flow through rpc.handleError() onto ctx.getExecutor().
chore: remove synchronized(lock) from SessionImpl All session state mutations now run on sessionSyncContext, so the per-session Object lock is no longer needed. nextRpcId becomes AtomicLong for the
cross-thread newCall() caller. handleVRpcResponse / handleVRpcErrorResponse drop the localCancel/localRpc capture-and-recheck dance — sessionSyncContext serializes them now.
chore: abort session on uncaught exception in sessionSyncContext (+ closeReason synthesizer) Split terminal close into notifyTerminalClose (per-target try/catch fan-out) and abortFromUncaughtException (global
handler). Uncaught syncContext exceptions always set closeReason to ERROR. notifyTerminalClose synthesizes a fallback closeReason if missing — defensive against future writers who forget. Adds three regression tests.

Stack position

  • Base: threading-refactor-phase-1
  • Next: threading-refactor-phase-3 — executor isolation (cached pool for user callbacks, gRPC DirectExecutor)

@igorbernstein2
igorbernstein2 force-pushed the threading-refactor-phase-2 branch from a702c4e to 0f6a9e4 Compare June 17, 2026 20:02
}

private void handleOpenSessionResponse(OpenSessionResponse openSession) {
sessionSyncContext.throwIfNotInThisSynchronizationContext();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I guess it doesn't hurt to do it twice, but we just did it in the dispatchResponseMessage above.

Interestingly, for some branches in dispatchResponseMessage we add this duplication and for some don't, e.g., handleSessionParamsResponse, handleGoAwayResponse, handleSessionRefreshConfigResponse don't have it, while handleOpenSessionResponse, handleVRpcResponse, handleVRpcErrorResponse, and handleHeartBeatResponse do.

@igorbernstein2
igorbernstein2 force-pushed the threading-refactor-phase-2 branch from 0f6a9e4 to 932ad80 Compare June 22, 2026 21:41
SessionImpl gains sessionSyncContext that serializes stream callbacks.
onMessage/onClose dispatch onto it, and the per-session heartbeat tick
trampolines through it. synchronized(lock) blocks remain inside the
handlers — the two coexist for now. Affinity asserts added at boundary
methods and every handle*.
SessionImpl.startRpc and cancelRpc now submit to sessionSyncContext rather
than running synchronously on the caller. VRpcSessionApi.startRpc is void
— errors flow through rpc.handleError() onto ctx.getExecutor(). VRpcImpl
drops its synchronous post-startRpc error branch.
All session state mutations now run on sessionSyncContext, so the per-session
Object lock is no longer needed. Public methods (start, close, forceClose,
startRpc, cancelRpc) submit onto sessionSyncContext; nextRpcId becomes
AtomicLong for the cross-thread newCall() caller. handleVRpcResponse and
handleVRpcErrorResponse drop the localCancel/localRpc capture-and-recheck
dance — sessionSyncContext serializes them now. Stale lock-era comments
on the fields are replaced with a sessionSyncContext-ownership note.
SessionImplTest.testHeartbeat polls for the now-async nextHeartbeat update.
Split terminal close into notifyTerminalClose (per-target try/catch fan-out)
and abortFromUncaughtException (global handler). Uncaught syncContext
exceptions always set closeReason to ERROR — the prior reason is folded into
the description so tracer/metrics correctly attribute aborts.

notifyTerminalClose synthesizes a fallback closeReason if missing: every
caller sets it today (forceClose, startGracefulClose, dispatchStreamClosed,
abortFromUncaughtException), but a future writer who forgets would NPE
inside the fan-out — and the throw escapes to the syncContext uncaught
handler, which early-returns on the already-CLOSED state and silently skips
the remaining cleanup. The synthesizer mirrors startGracefulClose: log a
warning with an IllegalStateException for stack-trace observability, then
build a fallback CloseSessionRequest so the rest of the fan-out runs.

Adds three regression tests (listener.onReady throws, onClose throws,
both throw).
Five SessionImpl handlers reachable from dispatchResponseMessage —
handleSessionParamsResponse, handleSessionRefreshConfigResponse,
handleGoAwayResponse, handleUnknownResponseMessage — and the
dispatchStreamClosed entry point were missing the
throwIfNotInThisSynchronizationContext assertion that the other handlers
already have. The assertions are strictly redundant today (every caller
is either dispatchResponseMessage, which already asserts, or
sessionSyncContext.execute), but the asymmetry is incidental and adding
them uniformly documents the threading contract and guards against
future direct callers.
@igorbernstein2
igorbernstein2 force-pushed the threading-refactor-phase-2 branch from 932ad80 to f285a29 Compare June 29, 2026 17:20
@igorbernstein2
igorbernstein2 force-pushed the threading-refactor-phase-1 branch from baf7ac7 to be630ac 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