You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
FfiQueue.put holds the subscriber lock across call_soon_threadsafe, so subscribe/unsubscribe on other event loops wait for the whole delivery #888
FfiQueue.put runs on the native FFI callback thread and holds self._lock while it runs every subscriber's filter_fn and calls loop.call_soon_threadsafe(...) for each match:
call_soon_threadsafe writes to the target loop's self-pipe, which releases the GIL. When other threads are busy, the callback thread has to win the GIL back once per subscriber, and the lock stays held the whole time.
subscribe and unsubscribe take the same lock. Every FFI request (perform_rpc, publish_data, set_attributes, Room.disconnect, ...) calls subscribe from its event loop thread. So that loop waits, off CPU, until the callback thread finishes delivering one event to all subscribers.
Production impact
We run many jobs as threads in one process (JobExecutorType.THREAD). Each job has its own event loop and its own Room, so _subscribers holds a few entries per job. Our loop stall monitor samples the loop thread's stack during each stall. In 7 days it recorded 6 stalls of 100 to 222 ms with the loop thread blocked here:
File ".../livekit/rtc/participant.py", line 468, in perform_rpc
File ".../livekit/rtc/participant.py", line 481, in _perform_rpc_ffi
File ".../livekit/rtc/_ffi_client.py", line 150, in subscribe
In the sampled one, the loop thread used 3 ms of CPU during a 101 ms stall, while over the surrounding 714 ms the FFI callback thread (Dummy-105) used 220 ms.
Suggested fix
Make the subscriber list copy-on-write. subscribe and unsubscribe build a new list under the lock and assign it, and put iterates the list it read without taking the lock:
The attribute read is atomic under the GIL. A put that runs at the same time as a change delivers as if it ran just before the change, which the current lock also allows. The lock only serializes writers.
We carry this as a monkeypatch, with tests that hold a put mid-delivery (a filter_fn that blocks on an event) and check that subscribe and unsubscribe on another thread still return. Happy to open a PR with those tests.
Related
#563 and #564 reduce how many events reach each subscriber. This issue is separate: the lock is held across delivery even when every subscriber filters.
Hi, I'm Himanshu, looking for a scoped contribution. I saw #892. Could I help validate subscribe/unsubscribe while delivery is in progress, including a loop that closes during delivery? I'd use synchronization barriers to test subscriber semantics without depending on a noisy timing benchmark.
I haven't run this reproduction or the candidate PR's tests yet; this is an offer to investigate, not a claim that the gap is confirmed.
Activity
himanshumehta commentedon Oct 25, 2026
Hi, I'm Himanshu, looking for a scoped contribution. I saw #892. Could I help validate subscribe/unsubscribe while delivery is in progress, including a loop that closes during delivery? I'd use synchronization barriers to test subscriber semantics without depending on a noisy timing benchmark.
I haven't run this reproduction or the candidate PR's tests yet; this is an offer to investigate, not a claim that the gap is confirmed.