use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::mpsc::{self, Receiver, Sender};
use std::thread::{self, JoinHandle};
use crate::SessionId;
use crate::pal::session_store::SessionStore;
pub(super) struct RecordWriter {
updates: Sender<Command>,
worker: JoinHandle<()>,
}
enum Command {
SetAttached { generation: u64, attached: bool },
Stop,
}
impl RecordWriter {
pub(super) fn start<S: SessionStore + Clone + Send + 'static>(
store: &S,
id: SessionId,
current_generation: Arc<AtomicU64>,
) -> Self {
let (updates, queue) = mpsc::channel();
let worker = thread::spawn({
let store = store.clone();
move || publish_updates(&store, id, ¤t_generation, &queue)
});
Self { updates, worker }
}
pub(super) fn set_attached(&self) -> impl Fn(u64, bool) + Clone + Send + 'static {
let updates = self.updates.clone();
move |generation: u64, attached: bool| {
_ = updates.send(Command::SetAttached {
generation,
attached,
});
}
}
pub(super) fn finish(self) {
_ = self.updates.send(Command::Stop);
_ = self.worker.join();
}
}
#[cfg_attr(test, mutants::skip)]
fn publish_updates<S: SessionStore>(
store: &S,
id: SessionId,
current_generation: &AtomicU64,
queue: &Receiver<Command>,
) {
while let Ok(Command::SetAttached {
generation,
attached,
}) = queue.recv()
{
if current_generation.load(Ordering::SeqCst) != generation {
continue;
}
if let Ok(Some(mut record)) = store.read(id) {
record.attached = attached;
_ = store.publish(&record);
}
}
}
#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))]
mod tests {
use std::path::PathBuf;
use super::*;
use crate::AppCommand;
use crate::pal::session_store::MemorySessionStore;
use crate::protocol::PROTOCOL_VERSION;
use crate::session_record::{ProcessIdentity, SessionRecord};
fn published() -> (MemorySessionStore, SessionId, ProcessIdentity) {
let store = MemorySessionStore::new();
let owner = ProcessIdentity::for_test(1);
let id = store.allocate_id(&owner).unwrap();
store
.publish(&SessionRecord {
id,
supervisor: owner,
pipe_name: "pipe".to_string(),
launch_directory: PathBuf::from("/work"),
command: AppCommand::for_test(&["app.exe"]),
started_at_unix_ms: 1,
attached: false,
protocol_version: PROTOCOL_VERSION,
})
.unwrap();
(store, id, owner)
}
#[test]
fn an_update_that_is_still_current_is_published() {
let (store, id, _owner) = published();
let generation = Arc::new(AtomicU64::new(1));
let writer = RecordWriter::start(&store, id, Arc::clone(&generation));
writer.set_attached()(1, true);
writer.finish();
assert!(store.read(id).unwrap().unwrap().attached);
}
#[test]
fn an_update_a_newer_one_has_overtaken_is_discarded() {
let (store, id, _owner) = published();
let generation = Arc::new(AtomicU64::new(2));
let writer = RecordWriter::start(&store, id, Arc::clone(&generation));
writer.set_attached()(1, true);
writer.finish();
assert!(!store.read(id).unwrap().unwrap().attached);
}
#[test]
fn nothing_a_client_left_behind_republishes_a_deleted_record() {
let (store, id, owner) = published();
let generation = Arc::new(AtomicU64::new(1));
let writer = RecordWriter::start(&store, id, Arc::clone(&generation));
let leftover = writer.set_attached();
writer.finish();
store.delete_owned_by(id, &owner).unwrap();
leftover(1, true);
assert!(store.read(id).unwrap().is_none());
}
#[test]
fn teardown_waits_for_an_update_that_is_already_being_written() {
testing::with_watchdog(|| {
let (store, id, _owner) = published();
let generation = Arc::new(AtomicU64::new(1));
let writer = RecordWriter::start(&store, id, Arc::clone(&generation));
store.stall_publishes();
writer.set_attached()(1, true);
store.wait_for_stalled_publish();
let finished = thread::spawn(move || writer.finish());
store.resume_publishes();
finished.join().unwrap();
assert!(store.read(id).unwrap().unwrap().attached);
});
}
}