Skip to content

Commit 6dd5f81

Browse files
authored
Merge pull request #1499 from jaytaph/gos-281-close-tab
GOS-281: Zone::detach_tab frees a tab's slot without waiting
2 parents bde54cd + 5a94df9 commit 6dd5f81

2 files changed

Lines changed: 57 additions & 14 deletions

File tree

‎crates/gosub_engine/src/engine/engine.rs‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1373,6 +1373,37 @@ mod tests {
13731373
engine.shutdown().await.expect("shutdown");
13741374
}
13751375

1376+
/// A detached tab gives its `max_tabs` slot back at once, before its worker has
1377+
/// stopped, so a zone that keeps opening and closing tabs never runs out.
1378+
#[tokio::test]
1379+
async fn detached_tab_frees_its_slot() {
1380+
let mut engine = engine_with_max_zones(1);
1381+
let _join = tokio::spawn(engine.start().expect("start"));
1382+
let mut zone = engine.zone_builder().services(services()).create().expect("zone");
1383+
let max = ZoneConfig::default().max_tabs;
1384+
1385+
let mut tabs = Vec::new();
1386+
for _ in 0..max {
1387+
tabs.push(zone.tab_builder().create().await.expect("tab"));
1388+
}
1389+
assert!(matches!(
1390+
zone.tab_builder().create().await,
1391+
Err(EngineError::TabLimitExceeded)
1392+
));
1393+
1394+
for _ in 0..max * 2 {
1395+
let closing = zone.detach_tab(tabs.remove(0).tab_id).expect("known tab");
1396+
assert!(zone.detach_tab(crate::tab::TabId::new()).is_none(), "unknown id");
1397+
tabs.push(zone.tab_builder().create().await.expect("the slot is free"));
1398+
tokio::time::timeout(Duration::from_secs(10), closing)
1399+
.await
1400+
.expect("the worker stops");
1401+
}
1402+
1403+
engine.close_zone(zone).await;
1404+
engine.shutdown().await.expect("shutdown");
1405+
}
1406+
13761407
/// A page served as an attachment is offered as a download; `RenderDownload` overrides
13771408
/// that and loads the already-spooled body as the page. No second request is made.
13781409
#[tokio::test(flavor = "current_thread")]

‎crates/gosub_engine/src/engine/zone/zone.rs‎

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -427,23 +427,35 @@ impl<C: RenderConfiguration> Zone<C> {
427427
/// The worker performs its own teardown on exit (emits `TabClosed`, drops the
428428
/// tab's session storage). Returns `false` when the tab is unknown.
429429
pub async fn close_tab(&mut self, tab_id: TabId) -> bool {
430-
let Some(info) = self.tabs.remove(&tab_id) else {
431-
return false;
432-
};
433-
434-
// A send error means the worker already exited; awaiting the join handle is
435-
// still correct in that case.
436-
let _ = info.cmd_tx.send(TabCommand::CloseTab).await;
430+
match self.detach_tab(tab_id) {
431+
Some(closing) => {
432+
closing.await;
433+
true
434+
}
435+
None => false,
436+
}
437+
}
437438

439+
/// Takes a tab out of the zone, so it no longer counts against `max_tabs`, and
440+
/// returns the stopping of it: a future that asks the worker to stop and waits for
441+
/// it to exit. For a caller that cannot wait, such as a UI thread, which spawns it.
442+
/// Returns `None` when the tab is unknown.
443+
pub fn detach_tab(&mut self, tab_id: TabId) -> Option<impl std::future::Future<Output = ()> + Send + 'static> {
444+
let info = self.tabs.remove(&tab_id)?;
438445
let secs = self.context.config_store.get_uint("engine.io_shutdown_secs") as u64;
439-
match tokio::time::timeout(std::time::Duration::from_secs(secs), info.join_handle).await {
440-
Ok(Ok(())) => {}
441-
Ok(Err(e)) => log::warn!("Tab {tab_id} worker join error: {e}"),
442-
// The worker keeps running detached and cleans up whenever it exits.
443-
Err(_) => log::warn!("Tab {tab_id} worker did not stop within {secs}s"),
444-
}
445446

446-
true
447+
Some(async move {
448+
// A send error means the worker already exited; awaiting the join handle is
449+
// still correct in that case.
450+
let _ = info.cmd_tx.send(TabCommand::CloseTab).await;
451+
452+
match tokio::time::timeout(std::time::Duration::from_secs(secs), info.join_handle).await {
453+
Ok(Ok(())) => {}
454+
Ok(Err(e)) => log::warn!("Tab {tab_id} worker join error: {e}"),
455+
// The worker keeps running detached and cleans up whenever it exits.
456+
Err(_) => log::warn!("Tab {tab_id} worker did not stop within {secs}s"),
457+
}
458+
})
447459
}
448460

449461
/// Closes every tab in this zone. Used by `GosubEngine::close_zone`.

0 commit comments

Comments
 (0)