Skip to content

Fix deadlock between close and reconnect#214

Open
Sean Pollock (seanavery) wants to merge 8 commits into
viam-modules:mainfrom
seanavery:RSDK-13991
Open

Fix deadlock between close and reconnect#214
Sean Pollock (seanavery) wants to merge 8 commits into
viam-modules:mainfrom
seanavery:RSDK-13991

Conversation

@seanavery

@seanavery Sean Pollock (seanavery) commented May 19, 2026

Copy link
Copy Markdown
Collaborator

Description

Flaky test surfaced deadlock between reconnect and Close.

Comment thread rtsp.go
rc.unsubscribeAll()
// Wait for the reconnect worker to exit before taking closeMu
rc.activeBackgroundWorkers.Wait()
rc.closeMu.Lock()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Benjamin Rewis (@benjirewis) can you and Sean Pollock (@seanavery) discuss this locking w.r.t active background workers and expectations around in flight requests during close?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't looked through this whole method/module, but I'd "normally" expect to see:

rc.closeMu.Lock()
rc.cancelFunc()
rc.closeMu.Unlock()
rc.activeBackgroundWorkers.Wait()

As a pattern for "request that all ongoing work stop and then wait for that ongoing work to actually stop." Let me know if you want to sync offline Sean Pollock (@seanavery). You'd have to walk me through what Close is trying to do here, but what you have currently looks a bit off.

@seanavery Sean Pollock (seanavery) May 19, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hey Benjamin Rewis (@benjirewis) Dan Gottlieb (@dgottlieb), walking through my understanding of what was happening here

In the original Close:

rc.cancelFunc()
rc.closeMu.Lock()
rc.unsubscribeAll()
rc.activeBackgroundWorkers.Wait()

clientReconnectBackgroundWorker takes closeMu inside reconnectClient for each transport attempt. So it would block on the lock that Close was holding, while Close was blocked on Wait() waiting for that same worker to exit.

The test sets reconnectIntervalDuration = 10ms, which is why this surfaced so reliably. (Prod uses 5s)

The suggested Lock, cancel, Unlock, Wait pattern works nicely, the key thing is not holding the lock over Wait(). I didn't wrap cancelFunc() itself because context.CancelFunc is goroutine safe and the RWMutex around closeConnection() (the bit that does client.Close() rc.client = nil etc) already serializes teardown vs concurrent readers. Happy to switch to the canonical ordering for style, lmk.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sean and I spoke offline a bit about better usage of closeMu and cancelCtx + better documentation around the four mutexes on the rtspCamera struct.

@seanavery Sean Pollock (seanavery) May 20, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks Benjamin Rewis (@benjirewis). First pass at adopting your suggested pattern, and it seems to have turned out nicely

Bonus: holding closeMu over cancel let me wrap SubscribeRTP in RLock, which closed out a separate orphan subscription race and let me drop the mu dance around the FIR call.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm surprised you're calling closeMu.Lock() twice in this function. Does everything below this line need closeMu held? If so, is there reason not to include it in the critical section above instead of this second one?

@seanavery Sean Pollock (seanavery) May 28, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, not entirely happy with this, but it seems to be necessary for now:

  • first lock makes cancelFunc atomic w the RLock holders
  • cannot hold lock over Wait (reconnect loop deadlock)
  • second lock is for general teardown

Lmk if a comment near the second lock would help

I think it is possible to refactor reconnect to not need the closeMu but seems out of scope for this PR.

@seanavery

Copy link
Copy Markdown
Collaborator Author

apt commands are failing on arm64 runners right now:

Cannot initiate the connection to ports.ubuntu.com:80 (2620:2d:4002:1::10b). - connect (101: Network is unreachable) Cannot initiate the connection to ports.ubuntu.com:80 (2620:2d:4002:1::10c). - connect (101: Network is unreachable) Cannot initiate the connection to ports.ubuntu.com:80 (2620:2d:4002:1::10a). - connect (101: Network is unreachable) [IP: 91.189.91.104 80]

Comment thread rtsp.go
Comment thread rtsp.go
rc.unsubscribeAll()
// Wait for the reconnect worker to exit before taking closeMu
rc.activeBackgroundWorkers.Wait()
rc.closeMu.Lock()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm surprised you're calling closeMu.Lock() twice in this function. Does everything below this line need closeMu held? If so, is there reason not to include it in the critical section above instead of this second one?

Comment thread rtsp.go

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The usages of the closeMu certainly look more "normal" to me now, but I won't claim to have all the context to approve here. I'm still suspicious of the need to lock twice in Close, but your added documentation and the more consistent pattern in using/setting the client object make sense to me!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants