@@ -929,29 +929,33 @@ def first
929929 # the legacy GET stream (`create_sse_body`).
930930 #
931931 # Registration and activation are split on purpose: the entry is inserted inactive
932- # (reserving the cap slot atomically), the acknowledgement is written outside the lock,
932+ # (reserving the cap slot atomically), the acknowledgement is written outside the registry lock,
933933 # and only then does the entry become eligible for delivery. A concurrent notification between
934934 # the insert and the acknowledgement write skips the inactive entry,
935935 # enforcing the SEP-2575 rule that no notification precedes the acknowledgement.
936936 #
937+ # The acknowledgement write holds the stream's write mutex, which `teardown_listen_subscriptions` also takes
938+ # before it marks an entry closed. The two therefore cannot interleave: the acknowledgement either lands
939+ # before the result, or, if the transport closed first, is not written at all and the stream just closes,
940+ # so no stream ever carries a result ahead of its acknowledgement.
941+ #
937942 # The entry is keyed by an identifier minted here, not by the request id: that id is unique only among
938943 # the requesting client's own in-flight requests, and two clients that pick the same one must each get
939944 # their stream, stamped with the id they sent.
940945 def listen_sse_body ( request_id , honored )
941946 ListenStreamBody . new do |stream |
942947 subscription_key = SecureRandom . uuid
943- rejected = false
948+ subscription = nil
944949 @mutex . synchronize do
945- if @max_listen_subscriptions && @listen_subscriptions . size >= @max_listen_subscriptions
946- rejected = true
947- else
948- @listen_subscriptions [ subscription_key ] = {
950+ unless @max_listen_subscriptions && @listen_subscriptions . size >= @max_listen_subscriptions
951+ subscription = {
949952 request_id : request_id , stream : stream , filter : honored , active : false , write_mutex : Mutex . new , keepalive_wakeup : ConditionVariable . new
950953 }
954+ @listen_subscriptions [ subscription_key ] = subscription
951955 end
952956 end
953957
954- if rejected
958+ if subscription . nil?
955959 close_stream_safely ( stream )
956960 else
957961 acknowledgement = {
@@ -964,9 +968,27 @@ def listen_sse_body(request_id, honored)
964968 }
965969
966970 begin
967- send_to_stream ( stream , acknowledgement )
968- activate_listen_subscription ( subscription_key )
969- start_listen_keepalive_thread ( subscription_key , request_id )
971+ acknowledged = subscription [ :write_mutex ] . synchronize do
972+ next false if subscription [ :closed ]
973+
974+ send_to_stream ( stream , acknowledgement )
975+
976+ # Set on the entry itself, not through the registry: a concurrent close may already have cleared the registry
977+ # while its result write waits on this mutex, and that write must still find the stream acknowledged.
978+ # Set under the registry lock as well, since that is the lock the delivery snapshot reads the flag under.
979+ # This is the one place a write mutex is held while the registry lock is taken; it stays deadlock-free only
980+ # as long as no path takes a write mutex inside `@mutex.synchronize`, so resolve entries under `@mutex`,
981+ # release it, then write.
982+ @mutex . synchronize { subscription [ :active ] = true }
983+
984+ true
985+ end
986+
987+ if acknowledged
988+ start_listen_keepalive_thread ( subscription_key , request_id )
989+ else
990+ close_stream_safely ( stream )
991+ end
970992 rescue *STREAM_WRITE_ERRORS
971993 remove_listen_subscription ( subscription_key )
972994 close_stream_safely ( stream )
@@ -975,15 +997,6 @@ def listen_sse_body(request_id, honored)
975997 end
976998 end
977999
978- # Marks a listen subscription eligible for delivery once its acknowledgement write has completed.
979- # The entry may already be gone when the transport closed concurrently.
980- def activate_listen_subscription ( subscription_key )
981- @mutex . synchronize do
982- subscription = @listen_subscriptions [ subscription_key ]
983- subscription [ :active ] = true if subscription
984- end
985- end
986-
9871000 # Periodically writes an SSE keepalive comment frame to a listen stream so a silently dropped
9881001 # connection is detected and its slot freed, rather than held until the next fan-out write.
9891002 # Mirrors the legacy GET stream's `start_keepalive_thread`; a comment frame (not a data frame)
@@ -1149,11 +1162,15 @@ def teardown_listen_subscriptions
11491162
11501163 removed . each_value do |subscription |
11511164 # Marking the entry closed and writing the result under the stream's write mutex orders
1152- # this against in-flight deliveries: each one either lands before the result or observes
1153- # `closed` and skips, keeping the graceful result the stream's final message.
1165+ # this against in-flight deliveries and against the acknowledgement write: each one either lands
1166+ # before the result or observes `closed` and skips, keeping the graceful result the stream's final message.
11541167 subscription [ :write_mutex ] . synchronize do
11551168 subscription [ :closed ] = true
11561169
1170+ # A stream whose acknowledgement was never written gets no result either: SEP-2575 makes
1171+ # the acknowledgement the first message, so the stream closes abruptly and the client re-sends.
1172+ next unless subscription [ :active ]
1173+
11571174 begin
11581175 send_to_stream ( subscription [ :stream ] , {
11591176 jsonrpc : "2.0" ,
0 commit comments