-
Notifications
You must be signed in to change notification settings - Fork 10
concurrency
Verdict: platform threads for Netty I/O, virtual thread per task for everything else (parse, session lookup, handlers, finalization). Rule everywhere: no synchronized (VT pinning on Java 21), use ReentrantLock; never block event loop; writes marshalled back to channel.eventLoop().
| Work | Thread | Proof |
|---|---|---|
| accept, decode HTTP, validation handlers, writes |
netty-io platform EL |
NettyServer#NettyServer |
| body parse + dispatch + handler |
tachyon-vt-N VT (or custom threadFactory) |
DefaultTachyonServer#defaultExecutor, McpOperationHandler#handlePost |
| POST-SSE final response finalize | VT | McpOperationHandler#completePostRequest |
| session/task janitors | daemon single-thread scheduler | AbstractJanitor#start |
| slow handler watchdog log | daemon handler-watchdog (only when DEBUG) |
HandlerWatchdog#SCHEDULER |
| extension shutdown | VT ext-shutdown-<id>
|
DefaultTachyonServer#shutdownExtensions |
| transport-triggered subscription terminal observation | server executor; fallback VT after shutdown rejection (graceful shutdown uses its caller) | SubscriptionsListenHandler#executeCompletion |
| Kotlin coroutines | dispatcher over server executor | tachyon-kotlin |
Ack timestamp publication and stream exception capture: observability.
DefaultTachyonServer.lifecycleLock (start/close) DefaultTachyonServer#lifecycleLock, OperationTracker.lock OperationTracker#lock, SessionManager.LifecycleLock per id, InMemorySessionEventStore.lock, DefaultResourceRegistry.writeLock, SubscriptionRegistry.lock (ack-first atomicity), TaskEntry.lock. Comments cite JEP 491 (fixed Java 24) as reason. Commit 6edcabf3 "get rid of synchronized".
OutboundSseStreamMessageRouter.withDispatchContext(sessionId, stream, action) sets ThreadLocals only during decode + handler kickoff McpDispatcher#invokeHandlerAsync. Consequence: notifications sent synchronously from handler thread route onto POST-SSE stream; from another thread (async continuation) they fall back to session GET connection. Also DefaultTaskRegistry.publish reads owner session from it. Test: tachyon-core/src/test/java/dev/tachyonmcp/core/transport/netty/ForeignThreadContinuationTest.java.
client notifications/cancelled → inboundRequests.get(key).cancel(true) → completion cancel listener → FutureTask.cancel(true) (interrupts VT) + handlerStage.cancel(true) McpDispatcher#invokeHandlerAsync, McpDispatcher#handleCancellation. HandlerFutures.completeOn propagates cancel from mapped to source HandlerFutures#completeOn. joinInterruptibly restores interrupt flag HandlerFutures#joinInterruptibly.
- Refuse if called on Netty event loop (would deadlock drain) DefaultTachyonServer#requireNotOnEventLoop.
-
netty.stopAccepting()— close server socket, keep children. -
deadline = now + runtime.shutdownGracePeriod(5s default). -
operations.drain(deadline)— stop admission, waitactive==0. Admission counts until both dispatch future andtransportCompletion(response flushed) doneOperationTracker. -
executor.shutdown()+ await remaining, elseshutdownNow. -
subscriptionRegistry.closeAll()(graceful listen results), extensions shutdown within deadline, task janitor stop,sessionManager.close(), event store close. -
finallynetty.close().
New requests during drain ⇒ RejectedExecutionException ⇒ 503 "Server shutting down". Tests: ServerShutdownGraceTest, e2e ShutdownDrainTest.
-
Annotation registration is synchronous on the caller and delegates to existing registries; the group is not atomic (
DefaultTachyonServer#requireNotOnEventLoop). Spring invokes it before transport startup; handler dispatch still uses virtual threads. -
Handler may block (VT) but not pin: no
synchronized, no long native calls; CPU-heavy →context.engine().executor()RpcMethodHandler. -
Any Netty write off EL →
eventLoop.execute/runOnEventLoop; catchRejectedExecutionExceptionon shutdown. -
ByteBufownership:retain()before async hop,release()infinally; rejecting handler mustmarkRejected(releases + drops rest)ChannelHandlerUtils#rejectAndClose. -
Peeking handlers call
PeekedBody.peek— one parse per POST body, cached on a channel attribute keyed by the request instance, reused by the dispatch site viaPeekedBody.cached(read on the event loop, before the async hop). It parsescontent().duplicate(), so the downstream reader index stays intactPeekedBody#peek.
Related: request-lifecycle, sse-streams.
📄 source .llm-wiki/concepts/concurrency.md · updated 2026-09-17 · verified at 1011a627 · tags [concept, concurrency, virtual-threads]
🧭 Start
⚙️ Concepts (cross-cutting)
- request-lifecycle
- netty-pipeline
- protocol-versions
- sessions
- sse-streams
- feature-registries
- tasks
- extensions
- json-layer
- errors
- concurrency
- declarative-configuration
- configuration
- security-guards
- observability
- api-stability
📦 Modules