use super::*;
use super::outbound::Outbox;
use super::projection::{projection_of, stored_task_state};
use super::transport::names_session;
#[derive(Clone)]
pub struct DispatchState {
pub(super) inner: Arc<Mutex<DispatchInner>>,
}
pub(super) struct DispatchInner {
pub(super) role: String,
pub(super) workspace: PathBuf,
pub(super) command: Vec<String>,
pub(super) max_sessions: u32,
pub(super) relay_required: Vec<String>,
pub(super) relay_count: Option<u32>,
pub(super) backend: Arc<dyn SessionBackend>,
pub(super) store: ClientStore,
pub(super) bridge: Bridge,
pub(super) sessions: HashMap<String, SessionSlot>,
pub(super) outbox: Option<Arc<dyn Outbox>>,
pub(super) accept_new: Arc<AtomicBool>,
pub(super) link_up: Arc<AtomicBool>,
pub(super) cluster_ref: String,
pub(super) topology: String,
pub(super) transports: HashMap<String, (AdapterIo, Vec<Capability>)>,
pub(super) parked: Option<(AdapterIo, Vec<Capability>)>,
pub(super) stall: crate::session::stall::StallWatch,
pub(super) revived: Vec<(String, AdapterIo, Vec<Capability>)>,
pub(super) held_handoffs: HashMap<String, Vec<Handoff>>,
pub(super) control_settles: Vec<ControlNote>,
pub(super) in_frame: Vec<AdapterIo>,
}
#[derive(Clone)]
pub struct SessionSlot {
pub(super) session: SessionRef,
pub(super) task_id: Option<String>,
pub(super) ready: bool,
pub(super) payload: Option<Envelope>,
pub(super) msg_id: Option<String>,
pub(super) origin: Option<Principal>,
pub(super) causality: Causality,
pub(super) dropped_at: Option<Instant>,
pub(super) last_beat: Option<Instant>,
pub(super) read_only: bool,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ControlNote {
pub task_id: String,
pub noted_at: Instant,
pub word: ControlWord,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ControlWord {
Cancel,
Recycle,
}
impl ControlWord {
pub fn outcome(self) -> Outcome {
match self {
Self::Cancel => Outcome::Cancelled,
Self::Recycle => Outcome::Failed,
}
}
pub fn refusal(self) -> &'static str {
match self {
Self::Cancel => "operator cancel",
Self::Recycle => "operator recycle",
}
}
}
pub const CONTROL_SETTLE_BOUND: Duration = Duration::from_secs(HEARTBEAT_INTERVAL.as_secs() * 3);
pub(super) fn due_control_settles(inner: &DispatchInner, now: Instant) -> Vec<ControlNote> {
inner
.control_settles
.iter()
.filter(|note| now.saturating_duration_since(note.noted_at) >= CONTROL_SETTLE_BOUND)
.cloned()
.collect()
}
pub(super) fn note_beat(inner: &mut DispatchInner, task_id: &str, now: Instant) {
let Some(key) = slot_key_named(inner, task_id) else {
return;
};
if let Some(slot) = inner.sessions.get_mut(&key) {
slot.last_beat = Some(now);
}
}
pub(super) fn has_attached_transport(inner: &DispatchInner, key: &str, slot: &SessionSlot) -> bool {
inner
.transports
.keys()
.any(|session_id| names_session(key, slot, session_id))
}
pub(super) fn rebase_generation(
inner: &DispatchInner,
task_id: &str,
body: impl FnOnce(&Observation) -> Observation,
) -> anyhow::Result<Option<Verdict>> {
let Some(row) = inner.store.get_session(task_id)? else {
return Ok(None);
};
let stored = stored_observation(&inner.store, Some(&row));
let event = LifecycleEvent::Supersede {
v: Version::new(stored.version.generation.saturating_add(1), 0),
old_generation_dead: true,
body: body(&stored),
};
Ok(Some(apply_persist(
&inner.bridge,
&inner.store,
task_id,
&event,
)?))
}
pub(super) fn slot_task(slot: &SessionSlot) -> String {
slot.task_id
.clone()
.unwrap_or_else(|| slot.session.task_id.clone())
}
pub(super) fn slot_key_named(inner: &DispatchInner, session_id: &str) -> Option<String> {
inner
.sessions
.iter()
.find(|(key, slot)| names_session(key, slot, session_id))
.map(|(key, _)| key.clone())
}
pub(super) fn slot_key_serving_task(inner: &DispatchInner, task_id: &str) -> Option<String> {
let mut bound = None;
for (key, slot) in inner.sessions.iter() {
if slot.task_id.as_deref() != Some(task_id) {
continue;
}
if !slot.read_only {
return Some(key.clone());
}
if bound.is_none() {
bound = Some(key.clone());
}
}
bound
}
#[must_use = "the connection stops being held as soon as the guard is dropped"]
pub struct FrameGuard<'a> {
pub(super) state: &'a DispatchState,
pub(super) io: AdapterIo,
}
impl Drop for FrameGuard<'_> {
fn drop(&mut self) {
self.state
.inner
.lock()
.in_frame
.retain(|held| !held.same_connection(&self.io));
}
}
pub(super) fn render_tokens(tokens: &[String], session: &str, task: &str) -> Vec<String> {
tokens
.iter()
.map(|token| token.replace("{session}", session).replace("{task}", task))
.collect()
}
pub(super) fn session_exited(inner: &DispatchInner, task_id: &str) -> bool {
let Some(row) = inner.store.get_session(task_id).ok().flatten() else {
return false;
};
projection_of(&row, stored_task_state(inner, task_id)).lifecycle == Lifecycle::Exited
}
pub(super) fn live_sessions(inner: &DispatchInner) -> usize {
inner
.sessions
.values()
.filter(|slot| !session_exited(inner, &slot.session.task_id))
.count()
}