mirror of
https://github.com/logos-blockchain/lssa.git
synced 2026-08-01 20:43:14 +00:00
138 lines
4.9 KiB
Rust
138 lines
4.9 KiB
Rust
|
|
//! A set of background tasks that can be stopped and waited on.
|
||
|
|
|
||
|
|
use std::sync::{Arc, Mutex, MutexGuard, PoisonError, Weak};
|
||
|
|
|
||
|
|
use log::warn;
|
||
|
|
use storage::sequencer::RocksDBIO;
|
||
|
|
use tokio::task::JoinHandle;
|
||
|
|
|
||
|
|
/// Background tasks owned by one component, stoppable on demand and stopped
|
||
|
|
/// anyway when the last handle goes away.
|
||
|
|
///
|
||
|
|
/// `JoinHandle::abort` only *requests* cancellation, and dropping a handle
|
||
|
|
/// detaches rather than cancels, so neither on its own says when a task has
|
||
|
|
/// actually stopped. That matters because these tasks hold a store handle:
|
||
|
|
/// until they are gone the `RocksDB` lock is still held and a restarting
|
||
|
|
/// sequencer cannot reopen its home directory. [`TaskGroup::shutdown`] is the
|
||
|
|
/// answer to "have they stopped yet"; the `Drop` below stays as the best-effort
|
||
|
|
/// path for panics and tests that never call it.
|
||
|
|
///
|
||
|
|
/// Cloneable so the owner can keep it (tying task lifetime to its own) while a
|
||
|
|
/// shutdown path elsewhere holds a clone.
|
||
|
|
#[derive(Clone, Default)]
|
||
|
|
pub struct TaskGroup(Arc<TaskGroupInner>);
|
||
|
|
|
||
|
|
#[derive(Default)]
|
||
|
|
struct TaskGroupInner(Mutex<Vec<JoinHandle<()>>>);
|
||
|
|
|
||
|
|
/// A weak handle to the store, for observing when it is finally closed.
|
||
|
|
///
|
||
|
|
/// Every strong reference lives inside a task or a server that shutdown stops,
|
||
|
|
/// but the last drop runs on whichever thread owned it, not on the one awaiting
|
||
|
|
/// shutdown. Watching the count is the difference between knowing the database
|
||
|
|
/// file is closed and assuming it from another crate's drop order.
|
||
|
|
pub struct StoreRelease(Weak<RocksDBIO>);
|
||
|
|
|
||
|
|
impl StoreRelease {
|
||
|
|
#[must_use]
|
||
|
|
pub fn new(store: &Arc<RocksDBIO>) -> Self {
|
||
|
|
Self(Arc::downgrade(store))
|
||
|
|
}
|
||
|
|
|
||
|
|
/// How many holders are left. Zero means the store is closed.
|
||
|
|
#[must_use]
|
||
|
|
pub fn holders(&self) -> usize {
|
||
|
|
self.0.strong_count()
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl Drop for TaskGroupInner {
|
||
|
|
fn drop(&mut self) {
|
||
|
|
for task in Self::take(&self.0) {
|
||
|
|
task.abort();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl TaskGroupInner {
|
||
|
|
/// Empties the handle list, so a second shutdown (or a drop after one) is a
|
||
|
|
/// no-op rather than a second abort.
|
||
|
|
fn handles(handles: &Mutex<Vec<JoinHandle<()>>>) -> MutexGuard<'_, Vec<JoinHandle<()>>> {
|
||
|
|
handles.lock().unwrap_or_else(PoisonError::into_inner)
|
||
|
|
}
|
||
|
|
|
||
|
|
fn take(handles: &Mutex<Vec<JoinHandle<()>>>) -> Vec<JoinHandle<()>> {
|
||
|
|
std::mem::take(&mut *Self::handles(handles))
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl TaskGroup {
|
||
|
|
/// Takes ownership of already-spawned tasks.
|
||
|
|
#[must_use]
|
||
|
|
pub fn new(handles: Vec<JoinHandle<()>>) -> Self {
|
||
|
|
Self(Arc::new(TaskGroupInner(Mutex::new(handles))))
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Whether any task has ended on its own.
|
||
|
|
///
|
||
|
|
/// These tasks run for the lifetime of the sequencer, so a finished one is a
|
||
|
|
/// task that panicked, and whatever it was doing is not happening any more.
|
||
|
|
#[must_use]
|
||
|
|
pub fn any_finished(&self) -> bool {
|
||
|
|
TaskGroupInner::handles(&self.0.0)
|
||
|
|
.iter()
|
||
|
|
.any(JoinHandle::is_finished)
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Stops every task and waits for it to finish.
|
||
|
|
///
|
||
|
|
/// Returns only once the runtime has dropped each task's future, so whatever
|
||
|
|
/// they held (a store handle, a network client) is released by the time this
|
||
|
|
/// returns. Cancellation is the expected outcome, so it is not reported; a
|
||
|
|
/// panic is, since it means the task died on its own terms earlier.
|
||
|
|
pub async fn shutdown(&self) {
|
||
|
|
let handles = TaskGroupInner::take(&self.0.0);
|
||
|
|
for handle in handles {
|
||
|
|
handle.abort();
|
||
|
|
if let Err(err) = handle.await
|
||
|
|
&& err.is_panic()
|
||
|
|
{
|
||
|
|
warn!("Background task panicked before shutdown: {err}");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
#[cfg(test)]
|
||
|
|
mod tests {
|
||
|
|
use super::*;
|
||
|
|
|
||
|
|
#[tokio::test]
|
||
|
|
async fn a_task_that_ends_on_its_own_is_visible() {
|
||
|
|
let group = TaskGroup::new(vec![tokio::spawn(async {})]);
|
||
|
|
// A watcher only ends by panicking, so "finished" is the signal that a
|
||
|
|
// peer's deliveries have stopped happening.
|
||
|
|
tokio::task::yield_now().await;
|
||
|
|
assert!(group.any_finished());
|
||
|
|
|
||
|
|
let running = TaskGroup::new(vec![tokio::spawn(std::future::pending())]);
|
||
|
|
assert!(!running.any_finished());
|
||
|
|
}
|
||
|
|
|
||
|
|
#[tokio::test]
|
||
|
|
async fn shutdown_ends_a_task_that_would_never_end_on_its_own() {
|
||
|
|
let group = TaskGroup::new(vec![tokio::spawn(std::future::pending())]);
|
||
|
|
|
||
|
|
// The watchers and the drive task are infinite loops, so awaiting one
|
||
|
|
// without cancelling it first hangs here for ever.
|
||
|
|
tokio::time::timeout(std::time::Duration::from_secs(5), group.shutdown())
|
||
|
|
.await
|
||
|
|
.expect("shutdown must not hang on a task that never finishes by itself");
|
||
|
|
|
||
|
|
// Shutting down twice is a no-op rather than a second abort.
|
||
|
|
tokio::time::timeout(std::time::Duration::from_secs(5), group.shutdown())
|
||
|
|
.await
|
||
|
|
.expect("a second shutdown must return immediately");
|
||
|
|
}
|
||
|
|
}
|