use std::sync::mpsc::{self, Sender};
use std::sync::{Arc, OnceLock};
use crate::backend::SandboxBackend;
pub(crate) struct CleanupJob {
pub(crate) backend: Arc<dyn SandboxBackend>,
pub(crate) container_id: String,
pub(crate) after_teardown: Option<Box<dyn FnOnce() + Send>>,
}
static SENDER: OnceLock<Sender<CleanupJob>> = OnceLock::new();
fn sender() -> &'static Sender<CleanupJob> {
SENDER.get_or_init(|| {
let (tx, rx) = mpsc::channel::<CleanupJob>();
std::thread::Builder::new()
.name("rightsize-cleanup".to_string())
.spawn(move || {
for job in rx {
let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
job.backend.cleanup_sync(&job.container_id);
}));
if let Some(after_teardown) = job.after_teardown {
let _ =
std::panic::catch_unwind(std::panic::AssertUnwindSafe(after_teardown));
}
}
})
.expect("failed to start the rightsize cleanup thread");
tx
})
}
pub(crate) fn enqueue(job: CleanupJob) {
let _ = sender().send(job);
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::{ContainerSpec, ExecResult};
use std::sync::Mutex;
use std::sync::atomic::{AtomicUsize, Ordering};
struct RecordingBackend {
cleaned: Arc<Mutex<Vec<String>>>,
calls: Arc<AtomicUsize>,
}
#[async_trait::async_trait]
impl SandboxBackend for RecordingBackend {
fn name(&self) -> &str {
"recording"
}
fn supports_native_networks(&self) -> bool {
false
}
async fn create(
&self,
_spec: ContainerSpec,
) -> crate::error::Result<Box<dyn crate::backend::SandboxHandle>> {
unimplemented!()
}
async fn start(
&self,
_handle: &dyn crate::backend::SandboxHandle,
) -> crate::error::Result<()> {
unimplemented!()
}
async fn stop(
&self,
_handle: &dyn crate::backend::SandboxHandle,
) -> crate::error::Result<()> {
unimplemented!()
}
async fn remove(
&self,
_handle: &dyn crate::backend::SandboxHandle,
) -> crate::error::Result<()> {
unimplemented!()
}
async fn exec(
&self,
_handle: &dyn crate::backend::SandboxHandle,
_cmd: &[String],
) -> crate::error::Result<ExecResult> {
unimplemented!()
}
async fn logs(
&self,
_handle: &dyn crate::backend::SandboxHandle,
) -> crate::error::Result<String> {
unimplemented!()
}
async fn follow_logs(
&self,
_handle: &dyn crate::backend::SandboxHandle,
_consumer: Box<dyn Fn(String) + Send + Sync>,
) -> crate::error::Result<crate::backend::FollowHandle> {
unimplemented!()
}
async fn ensure_network(&self, _network_id: &str) -> crate::error::Result<()> {
Ok(())
}
async fn remove_network(&self, _network_id: &str) -> crate::error::Result<()> {
Ok(())
}
fn cleanup_sync(&self, container_id: &str) {
self.calls.fetch_add(1, Ordering::SeqCst);
self.cleaned.lock().unwrap().push(container_id.to_string());
}
fn remove_by_name(&self, _name: &str) {}
fn watchdog_kill_command(&self) -> Vec<String> {
vec!["true".to_string()]
}
}
#[test]
fn enqueue_runs_cleanup_sync_on_the_background_thread() {
let cleaned = Arc::new(Mutex::new(Vec::new()));
let calls = Arc::new(AtomicUsize::new(0));
let backend: Arc<dyn SandboxBackend> = Arc::new(RecordingBackend {
cleaned: cleaned.clone(),
calls: calls.clone(),
});
enqueue(CleanupJob {
backend: backend.clone(),
container_id: "container-under-test".to_string(),
after_teardown: None,
});
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5);
while calls.load(Ordering::SeqCst) == 0 && std::time::Instant::now() < deadline {
std::thread::sleep(std::time::Duration::from_millis(10));
}
assert_eq!(calls.load(Ordering::SeqCst), 1);
assert_eq!(cleaned.lock().unwrap().as_slice(), ["container-under-test"]);
}
#[test]
fn after_teardown_runs_on_the_background_thread_only_once_cleanup_sync_has_returned() {
let cleaned = Arc::new(Mutex::new(Vec::new()));
let calls = Arc::new(AtomicUsize::new(0));
let backend: Arc<dyn SandboxBackend> = Arc::new(RecordingBackend {
cleaned: cleaned.clone(),
calls: calls.clone(),
});
let cleanup_sync_calls_seen_by_after_teardown = Arc::new(AtomicUsize::new(usize::MAX));
let seen = cleanup_sync_calls_seen_by_after_teardown.clone();
let calls_for_closure = calls.clone();
let after_teardown_ran = Arc::new(AtomicUsize::new(0));
let after_teardown_ran_writer = after_teardown_ran.clone();
enqueue(CleanupJob {
backend: backend.clone(),
container_id: "container-under-test".to_string(),
after_teardown: Some(Box::new(move || {
seen.store(calls_for_closure.load(Ordering::SeqCst), Ordering::SeqCst);
after_teardown_ran_writer.fetch_add(1, Ordering::SeqCst);
})),
});
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5);
while after_teardown_ran.load(Ordering::SeqCst) == 0 && std::time::Instant::now() < deadline
{
std::thread::sleep(std::time::Duration::from_millis(10));
}
assert_eq!(
after_teardown_ran.load(Ordering::SeqCst),
1,
"after_teardown must run exactly once"
);
assert_eq!(
cleanup_sync_calls_seen_by_after_teardown.load(Ordering::SeqCst),
1,
"after_teardown must observe cleanup_sync as already having run"
);
}
#[test]
fn a_missing_after_teardown_is_a_harmless_no_op() {
let calls = Arc::new(AtomicUsize::new(0));
let backend: Arc<dyn SandboxBackend> = Arc::new(RecordingBackend {
cleaned: Arc::new(Mutex::new(Vec::new())),
calls: calls.clone(),
});
enqueue(CleanupJob {
backend: backend.clone(),
container_id: "no-after-teardown".to_string(),
after_teardown: None,
});
let calls2 = Arc::new(AtomicUsize::new(0));
let backend2: Arc<dyn SandboxBackend> = Arc::new(RecordingBackend {
cleaned: Arc::new(Mutex::new(Vec::new())),
calls: calls2.clone(),
});
enqueue(CleanupJob {
backend: backend2.clone(),
container_id: "after-the-none-job".to_string(),
after_teardown: None,
});
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5);
while calls2.load(Ordering::SeqCst) == 0 && std::time::Instant::now() < deadline {
std::thread::sleep(std::time::Duration::from_millis(10));
}
assert_eq!(calls2.load(Ordering::SeqCst), 1);
}
}