Fix data races on lazily initialized members outside Room - #1089
Open
tarsyang wants to merge 1 commit into
Open
Conversation
Follow-up to livekit#1088. The remaining lazy members whose first access is not serialized are now created exactly once: - DeviceManager: one utility-queue block creates the discovery session, retains it and registers both KVO observations. Two blocks each touching the lazy session raced on iOS/tvOS. - AudioManager: the two processing delegate adapters are plain lets; their initializers capture nothing. - CameraCapturer: adapter and RTC capturer are created in init, with the adapter's back-reference set after super.init. - E2EEManager: the frame cryptor delegate adapter is a let with its target set after super.init.
tarsyang
requested review from
hiroshihorie,
pblazej and
xianshijing-lk
as code owners
August 17, 2026 09:24
pblazej
approved these changes
Aug 17, 2026
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follow-up to #1088, which covered
Room. This sweeps the remaininglazy vars inSources/LiveKitand fixes the four owners whose first access can happen concurrently from contexts that don't serialize with each other. Same failure mode as before: two threads see empty storage, both run the initializer, and one of the two objects is dropped. Because the WebRTC delegate properties involved are allweak, the dropped object doesn't just leak, whatever was registered through it goes silent.Fixed
DeviceManager.discoverySession: on iOS/tvOSinitdispatches two blocks to the global utility queue and each one touches the session first. TSan on the iOS simulator reports a write/write race between the two blocks, i.e. twoDiscoverySessions get created and the two KVO observations can land on different ones. Now a single block creates the session once, retains it, and registers both observations. Creation deliberately stays off the caller's thread: discovery session setup blocked for over 10 s in the xctest host on my machine (devices()timed out), so making this an eagerletininitwas not an option.AudioManager.capturePostProcessingDelegateAdapter/renderPreProcessingDelegateAdapter: reachable from the public delegate setters and fromadd/remove(localAudioRenderer:)/add/remove(remoteAudioRenderer:)(also viaLocalAudioTrack.add(audioRenderer:)) on any thread. Their initializers don't captureself, so they are plainlets now, likemetricsManagerin Fix data races on Room's lazy member initialization #1088. If the losing adapter had already attached itself as the (weak) APM delegate, the processing delegate or renderer registered through it stops receiving buffers once the stored one attaches.CameraCapturer.adapter/capturer: on iOS 16+/tvOS 17+ theisMultitaskingAccessSupportedlog line ininitforces both, but on macOS, Mac Catalyst, visionOS and older iOS the first touch isstartCapture(),stopCapture()or the publiccaptureSessiongetter, from whatever thread calls them. With the wrong interleaving the retainedLKRTCCameraVideoCapturerends up with a (weak) delegate nobody retains, and frames never reach the track. Both are created ininitnow, with the adapter's back-reference set aftersuper.init. This is the one observable behavior change: on the platforms listed above theAVCaptureSessionnow exists from track creation instead of first use, which is whatRTCCameraVideoCaptureritself intends (its own init comment: the app may want the session for a preview layer before starting).E2EEManager.delegateAdapter: first touched inaddRtpSender/addRtpReceiver, reachable fromsetup(room:)on the caller's thread and fromRoomDelegatecallbacks on the room's delegate queue. The in-SDKsetup(room:)inRoom.connect()runs right aftercleanUp()and finds no publications, butsetup(room:)andRoom.e2eeManagerare public and can run against a live room, where the two paths race. A frame cryptor whose (weak) delegate was the losing adapter never reports state changes. Now aletwith the target set aftersuper.init. Not reproduced under TSan (needs a live room with a frame cryptor); fixed by inspection.Left as they are
SignalClient._requestQueue/_responseQueue,Transport._iceCandidatesQueue,TranscriptionStreamReceiver.partialMessages: actor-isolated.VideoView._pinchGestureRecognizer: main actor.OSLogger.rtcLogger: touched only ininitanddeinit.LKSampleHandler.log: forced ininit.RemoteAudioTrack._adapter: forced ininit(audioTrack.add(_adapter)); could be aletfor consistency, but there is no race to fix, so left untouched.Verification
DispatchQueue.concurrentPerformon the first access; not part of this PR) under-enableThreadSanitizer YES. Onmainit reports data races inAudioManager.capturePostProcessingDelegateAdapter.getter,AudioManager.renderPreProcessingDelegateAdapter.getter,CameraCapturer.adapter.getter(macOS) andDeviceManager.discoverySession.getter(iOS simulator, write/write between the twoinitclosures). On this branch the same probe reports nothing on macOS and iOS, andDeviceManager.devices()/multiCamCompatibleDevices(for:)resolve on the simulator.swiftformat --lint,swiftlint --strict,swift build, and RoomTests / PublishTrackTests / PublishBufferCapturerTests / EncryptedDataChannelTests against a locallivekit-server --dev(all rooms inTestEnvironmentrun with encryption options, soE2EEManagerand the frame cryptor path are exercised).