Skip to content

WebSocket middleware transports send concurrently without synchronization, violating Jakarta WebSocket thread-safety #38

@nficano

Description

@nficano

The four WebSocket transports — arcp-middleware-spring-boot/SpringWebSocketTransport, arcp-middleware-jakarta/JakartaWebSocketTransport, arcp-middleware-vertx/VertxWebSocketTransport, and arcp-runtime-jetty/WebSocketJsonTransport — all implement Transport.send(Envelope) by calling the underlying socket's text-send method directly. None of them serialize concurrent writes. The Jakarta WebSocket specification explicitly requires application-level synchronization around RemoteEndpoint.Basic.sendText; Spring's WebSocketSession.sendMessage likewise throws IllegalStateException if two threads attempt simultaneous sends. The runtime exercises this constantly: SessionLoop sends from at least three thread families (the agent worker on runtime.workerPool(), the scheduler's heartbeat tick, and the inbound dispatch thread driving onNext). Under load, two threads in any of these will overlap a send() call and the WebSocket layer will throw, dropping the session and any in-flight events.\n\nThe defect is in arcp-middleware-spring-boot/src/main/java/dev/arcp/middleware/spring/SpringWebSocketTransport.java around the send method, arcp-middleware-jakarta/src/main/java/dev/arcp/middleware/jakarta/JakartaWebSocketTransport.java around the send method, arcp-middleware-vertx/src/main/java/dev/arcp/middleware/vertx/VertxWebSocketTransport.java around the send method, and arcp-runtime-jetty/src/main/java/dev/arcp/runtime/jetty/WebSocketJsonTransport.java around the send method. The StdioTransport already demonstrates the correct pattern with its writeLock ReentrantLock guarding the write/flush region.\n\nFix prompt: In each of the four WebSocket-backed Transport implementations listed above (SpringWebSocketTransport, JakartaWebSocketTransport, VertxWebSocketTransport, WebSocketJsonTransport), add a private final ReentrantLock writeLock = new ReentrantLock() field and serialize the body of send(Envelope) under writeLock.lock() / writeLock.unlock() in a try/finally — exactly mirroring the pattern in arcp-core/src/main/java/dev/arcp/core/transport/StdioTransport.java. The serialization must cover both the mapper.writeValueAsString call and the underlying socket send so the bytes of a single envelope are not interleaved with another envelope's. For Vert.x specifically, also confirm that calling socket.writeTextMessage from a non-event-loop thread is safe; if not, dispatch the write to the socket's context via socket.context().runOnContext(...) instead of relying on a Java lock. Add a JUnit 5 test in each middleware module that fires 100 concurrent Transport.send calls from a fixed thread pool of size 8 and asserts no exceptions are thrown and the receiver observes 100 well-formed envelopes.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingseverity:criticalCritical severity

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions