Skip to content

Commit 4bab7d6

Browse files
Use CancellationToken instead
1 parent f3bdba6 commit 4bab7d6

12 files changed

Lines changed: 150 additions & 184 deletions

File tree

.changeset/fix_data_track_shutdown_deadlock.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ livekit-uniffi: patch
55
livekit: patch
66
---
77

8-
Fix a data track manager deadlock during room disconnect. Shutdown was delivered
9-
through the bounded event channel with `try_send`, so it was silently dropped
10-
whenever in-flight track events had saturated the channel, leaving the manager
11-
task and every caller awaiting disconnect stranded.
8+
Fix a data track manager deadlock during room disconnect. Shutdown is now signaled
9+
via a `CancellationToken` (with child tokens for track tasks) instead of the bounded
10+
event channel, so it cannot be dropped when in-flight track events saturate the
11+
channel. `InputEvent::Shutdown` is removed; use [`ManagerInput::shutdown`].

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ serde_json = "1.0"
8787
thiserror = "2"
8888
tokio = { version = "1", default-features = false }
8989
tokio-stream = "0.1"
90+
tokio-util = "0.7"
9091
uniffi = "0.31"
9192

9293
# For examples

livekit-datatrack/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ livekit-runtime = { workspace = true, features = ["tokio"] }
1313
log = { workspace = true }
1414
thiserror = { workspace = true }
1515
tokio = { workspace = true, default-features = false, features = ["macros", "sync"] }
16+
tokio-util = { workspace = true }
1617
futures-util = { workspace = true, default-features = false, features = ["sink"] }
1718
futures-core = { workspace = true }
1819
bytes = { workspace = true }

livekit-datatrack/src/local/events.rs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,6 @@ pub enum InputEvent {
3737
/// to be recognized by the SFU. Each republished track will be assigned a new SID.
3838
///
3939
RepublishTracks,
40-
/// Shutdown the manager and all associated tracks.
41-
Shutdown,
4240
}
4341

4442
/// An event produced by [`Manager`](super::manager::Manager) requiring external action.

livekit-datatrack/src/local/manager.rs

Lines changed: 60 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,14 @@ use anyhow::{anyhow, Context};
2727
use futures_core::Stream;
2828
use std::{
2929
collections::HashMap,
30-
ops::ControlFlow,
3130
pin::Pin,
3231
sync::Arc,
3332
task::{Context as TaskContext, Poll},
3433
time::Duration,
3534
};
3635
use tokio::sync::{mpsc, oneshot, watch};
3736
use tokio_stream::wrappers::ReceiverStream;
37+
use tokio_util::sync::CancellationToken;
3838

3939
/// Options for creating a [`Manager`].
4040
#[derive(Debug)]
@@ -52,7 +52,7 @@ pub struct Manager {
5252
event_in_tx: mpsc::Sender<InputEvent>,
5353
event_in_rx: mpsc::Receiver<InputEvent>,
5454
event_out_tx: mpsc::Sender<OutputEvent>,
55-
shutdown_rx: watch::Receiver<bool>,
55+
token: CancellationToken,
5656
handle_allocator: packet::HandleAllocator,
5757
descriptors: HashMap<Handle, Descriptor>,
5858
}
@@ -69,15 +69,15 @@ impl Manager {
6969
pub fn new(options: ManagerOptions) -> (Self, ManagerInput, ManagerOutput) {
7070
let (event_in_tx, event_in_rx) = mpsc::channel(Self::EVENT_BUFFER_COUNT);
7171
let (event_out_tx, event_out_rx) = mpsc::channel(Self::EVENT_BUFFER_COUNT);
72-
let (shutdown_tx, shutdown_rx) = watch::channel(false);
72+
let token = CancellationToken::new();
7373

74-
let event_in = ManagerInput::new(event_in_tx.clone(), shutdown_tx);
74+
let event_in = ManagerInput::new(event_in_tx.clone(), token.clone());
7575
let manager = Manager {
7676
encryption_provider: options.encryption_provider,
7777
event_in_tx,
7878
event_in_rx,
7979
event_out_tx,
80-
shutdown_rx,
80+
token,
8181
handle_allocator: packet::HandleAllocator::default(),
8282
descriptors: HashMap::new(),
8383
};
@@ -88,42 +88,30 @@ impl Manager {
8888

8989
/// Run the manager task, consuming self.
9090
///
91-
/// The manager will continue running until receiving [`InputEvent::Shutdown`].
91+
/// The manager continues until [`ManagerInput::shutdown`] is called, the last
92+
/// [`ManagerInput`] is dropped, or the input channel closes.
9293
///
9394
pub async fn run(mut self) {
9495
log::debug!("Task started");
9596
loop {
9697
tokio::select! {
97-
// Biased so queued events are still processed in order once
98-
// shutdown has been signalled out-of-band; see `ManagerInput::send`.
98+
// Biased so shutdown ends event processing immediately.
9999
biased;
100+
_ = self.token.cancelled() => {
101+
break;
102+
}
100103
event = self.event_in_rx.recv() => {
101104
let Some(event) = event else { break };
102-
if self.handle_event(event).await.is_break() {
103-
break;
104-
}
105-
}
106-
_ = self.shutdown_rx.changed() => {
107-
self.drain_pending().await;
108-
break;
105+
self.handle_event(event).await;
109106
}
110107
}
111108
}
112109
self.shutdown().await;
113110
log::debug!("Task ended");
114111
}
115112

116-
/// Drains events that were queued ahead of an out-of-band shutdown signal.
117-
async fn drain_pending(&mut self) {
118-
while let Ok(event) = self.event_in_rx.try_recv() {
119-
if self.handle_event(event).await.is_break() {
120-
break;
121-
}
122-
}
123-
}
124-
125-
/// Handles a single input event, reporting whether the task should stop.
126-
async fn handle_event(&mut self, event: InputEvent) -> ControlFlow<()> {
113+
/// Handles a single input event.
114+
async fn handle_event(&mut self, event: InputEvent) {
127115
log::debug!("Input event: {:?}", event);
128116
match event {
129117
InputEvent::PublishRequest(event) => self.on_publish_request(event).await,
@@ -133,9 +121,7 @@ impl Manager {
133121
InputEvent::SfuPublishResponse(event) => self.on_sfu_publish_response(event).await,
134122
InputEvent::SfuUnpublishResponse(event) => self.on_sfu_unpublish_response(event).await,
135123
InputEvent::RepublishTracks => self.on_republish_tracks().await,
136-
InputEvent::Shutdown => return ControlFlow::Break(()),
137124
}
138-
ControlFlow::Continue(())
139125
}
140126

141127
async fn on_publish_request(&mut self, event: PublishRequest) {
@@ -286,6 +272,7 @@ impl Manager {
286272
frame_rx,
287273
event_in_tx: self.event_in_tx.clone(),
288274
event_out_tx: self.event_out_tx.clone(),
275+
token: self.token.child_token(),
289276
};
290277
let task_handle = livekit_runtime::spawn(track_task.run());
291278

@@ -310,7 +297,9 @@ impl Manager {
310297
return;
311298
};
312299
if *state_tx.borrow() != PublishState::Unpublished {
313-
_ = state_tx.send(PublishState::Unpublished);
300+
// `send_replace` updates even if the track task already dropped its receiver
301+
// after observing manager cancellation.
302+
_ = state_tx.send_replace(PublishState::Unpublished);
314303
}
315304
}
316305

@@ -347,28 +336,18 @@ impl Manager {
347336
_ = result_tx.send(Err(PublishError::Disconnected))
348337
}
349338
Descriptor::Active { state_tx, task_handle, .. } => {
350-
_ = state_tx.send(PublishState::Unpublished);
339+
// `send_replace` updates even if the track task already dropped its
340+
// receiver after observing manager cancellation.
341+
_ = state_tx.send_replace(PublishState::Unpublished);
351342
task_handles.push(task_handle);
352343
}
353344
}
354345
}
355346

356-
// Track tasks emit a final unpublish request as they end, so the input
357-
// channel has to keep draining while they are joined. Joining without
358-
// draining deadlocks as soon as more tasks are ending than the channel
359-
// can buffer.
360-
let join_tasks = async {
361-
for task_handle in task_handles {
362-
task_handle.await;
363-
}
364-
};
365-
tokio::pin!(join_tasks);
366-
loop {
367-
tokio::select! {
368-
_ = &mut join_tasks => break,
369-
// Never yields `None`: the manager owns a sender for its own lifetime.
370-
_ = self.event_in_rx.recv() => {}
371-
}
347+
// Track tasks observe the parent cancellation token via child tokens and
348+
// skip their final unpublish request, so joining alone is sufficient.
349+
for task_handle in task_handles {
350+
task_handle.await;
372351
}
373352
}
374353

@@ -387,6 +366,7 @@ struct TrackTask {
387366
frame_rx: mpsc::Receiver<DataTrackFrame>,
388367
event_in_tx: mpsc::Sender<InputEvent>,
389368
event_out_tx: mpsc::Sender<OutputEvent>,
369+
token: CancellationToken,
390370
}
391371

392372
impl TrackTask {
@@ -397,6 +377,8 @@ impl TrackTask {
397377
let mut state = *self.state_rx.borrow();
398378
while state != PublishState::Unpublished {
399379
tokio::select! {
380+
biased;
381+
_ = self.token.cancelled() => break,
400382
_ = self.state_rx.changed() => {
401383
state = *self.state_rx.borrow();
402384
}
@@ -410,8 +392,11 @@ impl TrackTask {
410392
}
411393
}
412394

413-
let event = UnpublishRequest { handle: self.info.pub_handle };
414-
_ = self.event_in_tx.send(event.into()).await;
395+
// Manager-wide shutdown already owns cleanup; only notify for per-track unpublish.
396+
if !self.token.is_cancelled() {
397+
let event = UnpublishRequest { handle: self.info.pub_handle };
398+
_ = self.event_in_tx.send(event.into()).await;
399+
}
415400

416401
log::debug!("Track task ended: sid={}", sid);
417402
}
@@ -465,7 +450,9 @@ pub(crate) enum PublishState {
465450
#[derive(Debug, Clone)]
466451
pub struct ManagerInput {
467452
event_in_tx: mpsc::Sender<InputEvent>,
468-
drop_guard: Arc<DropGuard>,
453+
token: CancellationToken,
454+
/// Cancels the manager when the last [`ManagerInput`] is dropped.
455+
_drop_guard: Arc<CancelOnDrop>,
469456
}
470457

471458
/// Stream of [`OutputEvent`]s produced by [`Manager`].
@@ -480,32 +467,37 @@ impl Stream for ManagerOutput {
480467
}
481468
}
482469

483-
/// Guard that signals shutdown when the last reference is dropped.
470+
/// Cancels a [`CancellationToken`] when dropped.
484471
#[derive(Debug)]
485-
struct DropGuard {
486-
shutdown_tx: watch::Sender<bool>,
487-
}
472+
struct CancelOnDrop(CancellationToken);
488473

489-
impl Drop for DropGuard {
474+
impl Drop for CancelOnDrop {
490475
fn drop(&mut self) {
491-
_ = self.shutdown_tx.send(true);
476+
self.0.cancel();
492477
}
493478
}
494479

495480
impl ManagerInput {
496-
fn new(event_in_tx: mpsc::Sender<InputEvent>, shutdown_tx: watch::Sender<bool>) -> Self {
497-
Self { event_in_tx, drop_guard: DropGuard { shutdown_tx }.into() }
481+
fn new(event_in_tx: mpsc::Sender<InputEvent>, token: CancellationToken) -> Self {
482+
Self { event_in_tx, token: token.clone(), _drop_guard: Arc::new(CancelOnDrop(token)) }
483+
}
484+
485+
/// Shuts down the manager, ending all event processing.
486+
///
487+
/// Unlike [`Self::send`], this does not use the bounded event channel, so it
488+
/// cannot be dropped when the channel is saturated.
489+
///
490+
pub fn shutdown(&self) {
491+
self.token.cancel();
492+
}
493+
494+
/// Returns a clone of the manager's cancellation token.
495+
pub fn cancellation_token(&self) -> CancellationToken {
496+
self.token.clone()
498497
}
499498

500499
/// Sends an input event to the manager's task to be processed.
501500
pub fn send(&self, event: InputEvent) -> Result<(), InternalError> {
502-
// Shutdown bypasses the bounded event channel. In-flight track events
503-
// routinely saturate it, and a shutdown dropped for lack of capacity
504-
// strands the manager task along with everyone awaiting its completion.
505-
if matches!(event, InputEvent::Shutdown) {
506-
_ = self.drop_guard.shutdown_tx.send(true);
507-
return Ok(());
508-
}
509501
Ok(self.event_in_tx.try_send(event).context("Failed to handle input event")?)
510502
}
511503

@@ -579,7 +571,7 @@ mod tests {
579571
let (manager, input, _) = Manager::new(options);
580572

581573
let join_handle = livekit_runtime::spawn(manager.run());
582-
_ = input.send(InputEvent::Shutdown);
574+
input.shutdown();
583575

584576
timeout(Duration::from_secs(1), join_handle).await.unwrap();
585577
}
@@ -591,21 +583,14 @@ mod tests {
591583

592584
// Fill the event channel before the manager starts draining it so that
593585
// shutdown cannot depend on any remaining capacity.
594-
let mut result_rxs = Vec::new();
595586
for _ in 0..Manager::EVENT_BUFFER_COUNT {
596-
let (result_tx, result_rx) = oneshot::channel();
587+
let (result_tx, _result_rx) = oneshot::channel();
597588
input.send(QueryPublished { result_tx }.into()).unwrap();
598-
result_rxs.push(result_rx);
599589
}
600-
input.send(InputEvent::Shutdown).unwrap();
590+
input.shutdown();
601591

602592
let join_handle = livekit_runtime::spawn(manager.run());
603593
timeout(Duration::from_secs(1), join_handle).await.unwrap();
604-
605-
// Events queued ahead of the shutdown signal are still processed.
606-
for result_rx in result_rxs {
607-
assert!(result_rx.await.is_ok());
608-
}
609594
}
610595

611596
#[tokio::test]
@@ -900,7 +885,7 @@ mod tests {
900885
assert!(active_track.is_published());
901886

902887
// Shutdown the manager
903-
input.send(InputEvent::Shutdown).unwrap();
888+
input.shutdown();
904889
sleep(Duration::from_millis(50)).await;
905890

906891
// Pending publish receives disconnected error

livekit-datatrack/src/remote/events.rs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,6 @@ pub enum InputEvent {
4040
/// tracks are subscribed to locally.
4141
///
4242
ResendSubscriptionUpdates,
43-
/// Shutdown the manager, ending any subscriptions.
44-
Shutdown,
4543
}
4644

4745
/// An event produced by [`Manager`](super::manager::Manager) requiring external action.

0 commit comments

Comments
 (0)