authorclover caruso <git@paperclover.net> 2026-10-10 19:03:10-07:00
committerclover caruso <git@paperclover.net> 2026-10-10 22:56:05-07:00
log57278aa264f7bbda73a30923db9b52a5541ae034
treec46a9af79b5cb07b861d973965a9a7d0f29849c1
parent7802ff53b2f17ccccf6e2589b59f9513202f7e07
signature Signed by SSH key SHA256:52mNGHRsVFBDED9IAX5pe+LRWUefqTbxEReunq21QvU (~clover)

fix: release section cache before waiting for sync shutdown

Keep section creation and other structure changes from holding the kept-section cache lock while a stalled network worker finishes. Cover timeout failure with a gated remote and assert that the cache remains usable during shutdown. Fixes #108 Assisted-by: gpt-6.1-sol

1 files changed, 103 insertions(+), 3 deletions(-)

crates/snowbound/src/library.rs+103-3
......@@ -1016,9 +1016,12 @@ impl Library {
10161016
10171017 /// Closes the sections kept open, before the notebook's files change.
10181018 pub fn close_kept(&self) {
1019 if let Ok(mut kept) = self.kept.lock() {
1020 close(kept.trim(0, |_| 1));
1021 }
1019 let sections = self
1020 .kept
1021 .lock()
1022 .map(|mut kept| kept.trim(0, |_| 1))
1023 .unwrap_or_default();
1024 close(sections);
10221025 }
10231026
10241027 pub async fn close_kept_async(&self) -> Result<(), Box<dyn Error>> {
......@@ -1598,6 +1601,103 @@ pub fn locate(path: &Path) -> Located {
15981601mod tests {
15991602 use super::*;
16001603
1604 #[test]
1605 fn closing_a_stalled_section_leaves_the_kept_cache_accessible() {
1606 use std::sync::{Condvar, mpsc};
1607
1608 struct Stalled {
1609 file: PathBuf,
1610 entered: Arc<Mutex<Option<mpsc::Sender<()>>>>,
1611 release: Arc<(Mutex<bool>, Condvar)>,
1612 }
1613
1614 impl notebook::Remote for Stalled {
1615 fn read(&mut self) -> io::Result<Vec<u8>> {
1616 onestore::read_file(&self.file)
1617 }
1618
1619 fn stamp(&mut self) -> io::Result<onestore::Stamp> {
1620 if let Some(entered) = self.entered.lock().unwrap().take() {
1621 entered.send(()).unwrap();
1622 let (lock, changed) = &*self.release;
1623 let _ = changed
1624 .wait_timeout_while(lock.lock().unwrap(), Duration::from_secs(5), |done| {
1625 !*done
1626 })
1627 .unwrap();
1628 }
1629 Err(io::ErrorKind::TimedOut.into())
1630 }
1631
1632 fn publish(
1633 &mut self,
1634 transaction: &onestore::Transaction,
1635 ) -> Result<(), onestore::CommitError> {
1636 transaction.commit_file(&self.file)
1637 }
1638
1639 fn confirm(&mut self, base: &onestore::Stamp) -> Result<(), onestore::CommitError> {
1640 onestore::confirm_file(&self.file, base)
1641 }
1642 }
1643
1644 let root =
1645 std::env::temp_dir().join(format!("snowbound-stalled-close-{}", std::process::id()));
1646 let _ = notebook::fs::remove_dir_all(&root);
1647 notebook::fs::create_dir_all(&root).unwrap();
1648 let file = root.join("Stalled.one");
1649 notebook::fs::write(
1650 &file,
1651 &onestore::create_section("Stalled.one", "", "Author").unwrap(),
1652 )
1653 .unwrap();
1654 let (entered, waiting) = mpsc::channel();
1655 let release = Arc::new((Mutex::new(false), Condvar::new()));
1656 let gate = Arc::clone(&release);
1657 let entered = Arc::new(Mutex::new(Some(entered)));
1658 let section = Section::open_with(
1659 &file,
1660 root.join("cache"),
1661 move |file| {
1662 Ok(Stalled {
1663 file: file.to_owned(),
1664 entered: Arc::clone(&entered),
1665 release: Arc::clone(&gate),
1666 })
1667 },
1668 || {},
1669 )
1670 .unwrap();
1671 waiting.recv_timeout(Duration::from_secs(3)).unwrap();
1672 let library = Arc::new(Library::section(&file, &root.join("cache")));
1673 library.keep("Stalled.one", section);
1674 let closing = Arc::clone(&library);
1675 let close = std::thread::spawn(move || closing.close_kept());
1676 let deadline = Instant::now() + Duration::from_secs(1);
1677 let available = loop {
1678 if library
1679 .kept
1680 .try_lock()
1681 .is_ok_and(|kept| !kept.contains(&"Stalled.one".to_owned()))
1682 {
1683 break true;
1684 }
1685 if Instant::now() >= deadline {
1686 break false;
1687 }
1688 std::thread::yield_now();
1689 };
1690 *release.0.lock().unwrap() = true;
1691 release.1.notify_all();
1692 close.join().unwrap();
1693 assert!(
1694 available,
1695 "stopping a network worker held the UI's cache lock"
1696 );
1697 drop(library);
1698 notebook::fs::remove_dir_all(root).unwrap();
1699 }
1700
16011701 #[cfg(unix)]
16021702 #[test]
16031703 fn stalled_theme_reads_leave_the_cache_usable_and_keep_local_edits() {