release: lock-disciplined STREAMS reads (#7339) (#7488)

* fix(streaming): lock-disciplined STREAMS reads via peek_stream()

Writers pop STREAMS under STREAMS_LOCK, but four hot reads used bare
STREAMS.get(): the streaming worker start, the gateway run start, and the two
SSE attach paths in routes.py. Each is individually saved today by GIL-atomic
dict.get plus a None-guard, but the check-then-act windows (get → register,
get → subscribe) race teardown pops, and the bare pattern invites unprotected
copies. peek_stream() centralizes the locked read; all four sites migrate with
no behavioral change.

* test: dual-patch config.STREAMS alongside routes.STREAMS in SSE seam tests

peek_stream() reads the canonical api.config.STREAMS registry; tests that
inject a fake registry by patching only the routes-level re-export left the
helper reading the real (empty) registry and hung the SSE loop to the
pytest-timeout ceiling. Mirror the test_issue6623 pattern: patch both
bindings.

* docs(changelog): stamp #7339 lock-disciplined STREAMS reads

---------

Co-authored-by: Sebastian B <sb@wmc.sh>
Co-authored-by: n <a@n>
This commit is contained in:
authored and GitHub committed 2026-09-09 20:22:36 +00:00
1 parent b1a5ade137
commit 2477d992f4
8 files changed
+143 -4

No files matched your search

+15
View File
@@ -9209,6 +9209,21 @@ def create_stream_channel() -> StreamChannel:
STREAMS: dict = {}
STREAMS_LOCK = threading.Lock()
def peek_stream(stream_id):
"""Lock-disciplined stream queue lookup.
Writers mutate STREAMS under STREAMS_LOCK (teardown in api/streaming.py,
the route layer's start/cancel paths); reads must take the same lock so a
read racing a teardown pop can never observe-and-use a queue the registry
has already released. Returns the queue or None — callers keep their
existing None-guard fallbacks.
"""
with STREAMS_LOCK:
return STREAMS.get(stream_id)
# stream_id -> session_id owner, populated synchronously before worker startup so
# stream-id authorization does not depend on worker lifecycle registration.
STREAM_SESSION_OWNERS: dict = {}