Summary
When a lazily started WebRTCSession fails during its first connection attempt, the session remains in the STARTED state and the owned asyncio event loop and daemon thread remain alive. A failure after RTCPeerConnection construction can also leave the partial peer connection unreachable by cleanup.
This makes the failed session appear usable while retaining resources until process shutdown.
Reproduction
I reproduced both failure boundaries deterministically on current main (a6caf5e75f76a31c6cc41eb167c01ef0a2e23f76) with stubbed I/O:
Failure before peer creation
- Construct a
WebRTCSession.
- Make
_init() raise before creating a peer connection.
- Call
_ensure_started().
- Inspect the lifecycle state and owned resources.
Observed after the exception:
session._state == SessionState.STARTED
- the event loop is still running and open
- the daemon loop thread is still alive
- source cleanup was not called
Failure after peer creation
- Let
_init() create an RTCPeerConnection.
- Raise while configuring the source.
- Call
_ensure_started().
- Inspect the partial peer.
Observed after the exception:
session._pc is None, because the peer is assigned only at the end of _init()
peer.close() was not called
- the source and loop-thread resources remain active
The test harness explicitly stops the leaked parent loop after recording these observations.
Root cause
_ensure_started() changes the state to STARTED before initialization but has no failure transition or teardown path.
_init_connection() starts an owned event-loop thread but does not stop, join, or close it when initialization fails.
_init() publishes self._pc only after all later setup and signaling steps succeed.
- An unfinished startup future is not cancelled if an external
BaseException interrupts future.result().
Expected behavior
After any startup failure:
- the session enters the terminal
CLOSED state
- the original exception and HTTP diagnostics remain intact
- an unfinished startup future is cancelled before teardown
- a partially created peer connection and source are each cleaned up once
- pending loop tasks are cancelled and drained
- the event loop is stopped, its thread is joined, and the loop is closed by its owner thread
- a later
_ensure_started() call fails with Cannot use closed WebRTCSession
The fix should reuse the existing single-flight close() path and should not add session restart semantics or change callback, queue, or server-side behavior.
Summary
When a lazily started
WebRTCSessionfails during its first connection attempt, the session remains in theSTARTEDstate and the owned asyncio event loop and daemon thread remain alive. A failure afterRTCPeerConnectionconstruction can also leave the partial peer connection unreachable by cleanup.This makes the failed session appear usable while retaining resources until process shutdown.
Reproduction
I reproduced both failure boundaries deterministically on current
main(a6caf5e75f76a31c6cc41eb167c01ef0a2e23f76) with stubbed I/O:Failure before peer creation
WebRTCSession._init()raise before creating a peer connection._ensure_started().Observed after the exception:
session._state == SessionState.STARTEDFailure after peer creation
_init()create anRTCPeerConnection._ensure_started().Observed after the exception:
session._pc is None, because the peer is assigned only at the end of_init()peer.close()was not calledThe test harness explicitly stops the leaked parent loop after recording these observations.
Root cause
_ensure_started()changes the state toSTARTEDbefore initialization but has no failure transition or teardown path._init_connection()starts an owned event-loop thread but does not stop, join, or close it when initialization fails._init()publishesself._pconly after all later setup and signaling steps succeed.BaseExceptioninterruptsfuture.result().Expected behavior
After any startup failure:
CLOSEDstate_ensure_started()call fails withCannot use closed WebRTCSessionThe fix should reuse the existing single-flight
close()path and should not add session restart semantics or change callback, queue, or server-side behavior.