use std::sync::atomic::Ordering;
use std::sync::{Arc, Mutex, Weak};
use std::thread::JoinHandle;
use std::time::Instant;
use super::handler::{ProductionParticipantHandler, elapsed_ms};
use crate::server::participant::ParticipantSemanticError;
#[derive(Debug, Default)]
pub(super) struct WarmWorker(Mutex<WorkerState>);
#[derive(Debug, Default)]
struct WorkerState {
started: bool,
stopped: bool,
thread: Option<JoinHandle<()>>,
failure: Option<String>,
}
impl WarmWorker {
fn start(
&self,
handler: Weak<ProductionParticipantHandler>,
) -> Result<(), ParticipantSemanticError> {
let mut worker = self
.0
.lock()
.map_err(|_| worker_error("participant warm start lock poisoned"))?;
if worker.started || worker.stopped {
return Err(worker_error("participant warm already started"));
}
worker.started = true;
worker.thread = Some(
std::thread::Builder::new()
.name("liminal-participant-warm".to_owned())
.spawn(move || warm_in_background(&handler))
.map_err(|error| {
worker_error(&format!("participant warm spawn failed: {error}"))
})?,
);
drop(worker);
Ok(())
}
fn join(&self) -> Result<(), ParticipantSemanticError> {
let mut worker = self
.0
.lock()
.map_err(|_| worker_error("participant warm join lock poisoned"))?;
worker.stopped = true;
if let Some(failure) = &worker.failure {
return Err(worker_error(failure));
}
if let Some(thread) = worker.thread.take()
&& thread.join().is_err()
{
let message = "participant warm panicked during shutdown";
worker.failure = Some(message.to_owned());
return Err(worker_error(message));
}
drop(worker);
Ok(())
}
}
fn worker_error(message: &str) -> ParticipantSemanticError {
ParticipantSemanticError::Internal {
message: message.to_owned(),
}
}
#[derive(Clone, Debug)]
pub(in crate::server) struct ParticipantWarm {
handler: Weak<ProductionParticipantHandler>,
worker: Arc<WarmWorker>,
}
impl ParticipantWarm {
pub(in crate::server) fn join(&self) -> Result<(), ParticipantSemanticError> {
self.stop();
self.worker.join()
}
pub(in crate::server) fn stop(&self) {
if let Some(handler) = self.handler.upgrade() {
handler.warm_stop.store(true, Ordering::SeqCst);
if let Err(error) = handler.pending.wake_waiters() {
tracing::error!(
error = %error,
"a server stop could not wake the touches waiting on a restore"
);
}
}
}
pub(in crate::server) fn finish(&self) -> Result<usize, ParticipantSemanticError> {
self.handler.upgrade().map_or(Ok(0), |handler| {
handler.restore_remaining("a measurement waiting for the warm")
})
}
pub(in crate::server) fn record_clean_stop(&self) {
if let Some(handler) = self.handler.upgrade() {
handler.record_clean_stop_capacity();
}
}
}
impl ProductionParticipantHandler {
pub(in crate::server) fn install_warm(
self: &Arc<Self>,
) -> Result<ParticipantWarm, ParticipantSemanticError> {
self.self_ref.set(Arc::downgrade(self)).map_err(|_| {
ParticipantSemanticError::Internal {
message: "participant warm was already installed".to_owned(),
}
})?;
Ok(ParticipantWarm {
handler: Arc::downgrade(self),
worker: Arc::clone(&self.warm_worker),
})
}
pub(super) fn start_warm(&self) {
let Some(handler) = self.self_ref.get().cloned() else {
return;
};
if let Err(error) = self.warm_worker.start(handler) {
tracing::error!(%error, "participant warm thread could not start");
}
}
}
fn warm_in_background(handler: &Weak<ProductionParticipantHandler>) {
let Some(owner) = handler.upgrade() else {
return;
};
let remaining = match owner.pending.remaining() {
Ok(remaining) => remaining,
Err(error) => {
tracing::error!(error = %error, "participant warm could not list pending conversations");
return;
}
};
drop(owner);
let total = remaining.len();
let started = Instant::now();
tracing::info!(
step = "participant warm",
conversations = total,
"participant warm started"
);
let mut refused = Vec::new();
for (index, conversation_id) in remaining.into_iter().enumerate() {
let Some(owner) = handler.upgrade() else {
return;
};
if owner.warm_stop.load(Ordering::SeqCst) {
tracing::info!(
step = "participant warm",
done = index,
total,
elapsed_ms = elapsed_ms(started),
"participant warm stopped with the server; the rest restore on the next start"
);
return;
}
if let Err(refusal) = owner.ensure_restored(conversation_id) {
refused.push((conversation_id, refusal.to_string()));
}
}
tracing::info!(
step = "participant warm",
conversations = total,
unloadable = refused.len(),
refused = ?refused,
elapsed_ms = elapsed_ms(started),
"participant warm finished"
);
verify_restored_histories(handler);
}
fn verify_restored_histories(handler: &Weak<ProductionParticipantHandler>) {
let Some(owner) = handler.upgrade() else {
return;
};
let restored = match owner.restored_conversation_ids() {
Ok(restored) => restored,
Err(error) => {
tracing::error!(error = %error, "participant history check could not list restored conversations");
return;
}
};
drop(owner);
let started = Instant::now();
let mut identities = 0_usize;
let mut read = 0_usize;
let mut mismatched = Vec::new();
for conversation_id in restored {
let Some(owner) = handler.upgrade() else {
return;
};
if owner.warm_stop.load(Ordering::SeqCst) {
return;
}
let checked = match owner.verify_identity_history(conversation_id) {
Ok(checked) => checked,
Err(error) => {
tracing::error!(
step = "participant history check",
conversation_id = ?conversation_id,
error = %error,
"a restored conversation's identities disagree with its checkpoint's history index"
);
mismatched.push(conversation_id);
continue;
}
};
identities = identities.saturating_add(checked.identities);
read = read.saturating_add(checked.checked);
}
tracing::info!(
step = "participant history check",
identities,
read,
mismatches = mismatched.len(),
mismatched = ?mismatched,
elapsed_ms = elapsed_ms(started),
"history cross-check finished"
);
}