use std::collections::VecDeque;
use crate::barrier::{BarrierState, FlycheckBarrier};
use crate::lsp::LspEvent;
use crate::repo::watch::WtId;
#[derive(Debug)]
struct ActiveTxn {
wt: WtId,
barrier: FlycheckBarrier,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ClusterAction {
SwitchOverlay { wt: WtId },
EmitVerdict { wt: WtId, authoritative_error: bool },
Idle,
}
#[derive(Debug, Clone)]
pub enum DriverEvent {
RoutedBatch { wt: WtId },
Lsp(LspEvent),
Deactivated { wt: WtId },
}
#[derive(Debug, Default)]
pub struct ClusterDriver {
current: Option<ActiveTxn>,
pending: VecDeque<WtId>,
recheck_current: bool,
}
impl ClusterDriver {
pub fn new() -> Self {
Self::default()
}
pub fn is_busy(&self) -> bool {
self.current.is_some()
}
pub fn pending(&self) -> Vec<WtId> {
self.pending.iter().cloned().collect()
}
pub fn on_event(&mut self, ev: DriverEvent) -> ClusterAction {
match ev {
DriverEvent::RoutedBatch { wt } => self.on_routed_batch(wt),
DriverEvent::Lsp(ev) => self.on_lsp(ev),
DriverEvent::Deactivated { wt } => self.on_deactivated(wt),
}
}
fn on_routed_batch(&mut self, wt: WtId) -> ClusterAction {
if self.current.is_none() {
self.current = Some(ActiveTxn {
wt: wt.clone(),
barrier: FlycheckBarrier::arm(false),
});
return ClusterAction::SwitchOverlay { wt };
}
let in_flight = self.current.as_ref().map(|a| a.wt.clone());
if in_flight.as_ref() == Some(&wt) {
self.recheck_current = true;
} else if !self.pending.contains(&wt) {
self.pending.push_back(wt);
}
ClusterAction::Idle
}
fn on_lsp(&mut self, ev: LspEvent) -> ClusterAction {
let state = match self.current.as_mut() {
None => return ClusterAction::Idle,
Some(active) => active.barrier.observe(&ev),
};
match state {
BarrierState::Waiting => ClusterAction::Idle,
BarrierState::Settled => {
let txn = self
.current
.take()
.expect("current is Some in the Settled arm");
let action = ClusterAction::EmitVerdict {
wt: txn.wt.clone(),
authoritative_error: txn.barrier.has_authoritative_error(),
};
self.start_next_after_settle(txn.wt);
action
}
}
}
fn on_deactivated(&mut self, wt: WtId) -> ClusterAction {
self.pending.retain(|w| w != &wt);
if self.current.as_ref().is_some_and(|a| a.wt == wt) {
self.recheck_current = false;
}
ClusterAction::Idle
}
fn start_next_after_settle(&mut self, just_finished: WtId) {
let next = if self.recheck_current {
self.recheck_current = false;
Some(just_finished)
} else {
self.pending.pop_front()
};
if let Some(wt) = next {
self.current = Some(ActiveTxn {
wt,
barrier: FlycheckBarrier::arm(false),
});
}
}
pub fn take_followup(&mut self) -> Option<ClusterAction> {
self.current
.as_ref()
.map(|a| ClusterAction::SwitchOverlay { wt: a.wt.clone() })
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::lsp::PublishDiagnostics;
use crate::{Diagnostic, Severity};
use std::path::PathBuf;
fn wt(s: &str) -> PathBuf {
PathBuf::from(s)
}
fn rustc_err(uri: &str) -> LspEvent {
LspEvent::Diagnostics(PublishDiagnostics {
uri: uri.into(),
authoritative_errors: 1,
advisory_errors: 0,
total: 1,
diagnostics: vec![Diagnostic {
file_path: uri.into(),
line: 1,
col: 1,
severity: Severity::Error,
code: Some("E0599".into()),
message: "x".into(),
source: Some("rustc".into()),
}],
})
}
#[test]
fn idle_cluster_first_batch_starts_transaction() {
let mut d = ClusterDriver::new();
assert_eq!(
d.on_event(DriverEvent::RoutedBatch { wt: wt("/r/a") }),
ClusterAction::SwitchOverlay { wt: wt("/r/a") }
);
assert!(d.is_busy());
}
#[test]
fn judgment_a_second_wt_is_queued_not_started() {
let mut d = ClusterDriver::new();
d.on_event(DriverEvent::RoutedBatch { wt: wt("/r/a") }); assert_eq!(
d.on_event(DriverEvent::RoutedBatch { wt: wt("/r/b") }),
ClusterAction::Idle
);
assert_eq!(d.pending(), vec![wt("/r/b")]);
assert_eq!(
d.on_event(DriverEvent::RoutedBatch { wt: wt("/r/b") }),
ClusterAction::Idle
);
assert_eq!(d.pending(), vec![wt("/r/b")]);
assert!(d.is_busy(), "still exactly one in-flight txn");
}
#[test]
fn judgment_b_no_verdict_before_settle() {
let mut d = ClusterDriver::new();
d.on_event(DriverEvent::RoutedBatch { wt: wt("/r/a") });
for _ in 0..3 {
assert_eq!(
d.on_event(DriverEvent::Lsp(rustc_err("file:///r/a/x.rs"))),
ClusterAction::Idle
);
}
assert_eq!(
d.on_event(DriverEvent::Lsp(LspEvent::IndexingEnded)),
ClusterAction::Idle,
"indexing-end is NOT a verdict boundary"
);
assert!(d.is_busy(), "no settle yet ⇒ txn still in flight");
}
#[test]
fn verdict_emitted_exactly_at_settle_with_correct_bit() {
let mut d = ClusterDriver::new();
d.on_event(DriverEvent::RoutedBatch { wt: wt("/r/a") });
d.on_event(DriverEvent::Lsp(rustc_err("file:///r/a/x.rs")));
assert_eq!(
d.on_event(DriverEvent::Lsp(LspEvent::FlycheckEnded)),
ClusterAction::EmitVerdict {
wt: wt("/r/a"),
authoritative_error: true
}
);
assert!(!d.is_busy());
assert_eq!(d.take_followup(), None);
}
#[test]
fn green_verdict_when_no_authoritative_error_in_window() {
let mut d = ClusterDriver::new();
d.on_event(DriverEvent::RoutedBatch { wt: wt("/r/a") });
assert_eq!(
d.on_event(DriverEvent::Lsp(LspEvent::FlycheckEnded)),
ClusterAction::EmitVerdict {
wt: wt("/r/a"),
authoritative_error: false
}
);
}
#[test]
fn settle_starts_next_queued_txn_serialized() {
let mut d = ClusterDriver::new();
d.on_event(DriverEvent::RoutedBatch { wt: wt("/r/a") }); d.on_event(DriverEvent::RoutedBatch { wt: wt("/r/b") }); d.on_event(DriverEvent::RoutedBatch { wt: wt("/r/c") }); let v = d.on_event(DriverEvent::Lsp(LspEvent::FlycheckEnded));
assert_eq!(
v,
ClusterAction::EmitVerdict {
wt: wt("/r/a"),
authoritative_error: false
}
);
assert!(d.is_busy(), "B's txn started — still exactly one in flight");
assert_eq!(
d.take_followup(),
Some(ClusterAction::SwitchOverlay { wt: wt("/r/b") })
);
assert_eq!(d.pending(), vec![wt("/r/c")], "C still queued behind B");
d.on_event(DriverEvent::Lsp(LspEvent::FlycheckEnded));
assert_eq!(
d.take_followup(),
Some(ClusterAction::SwitchOverlay { wt: wt("/r/c") })
);
d.on_event(DriverEvent::Lsp(LspEvent::FlycheckEnded));
assert!(!d.is_busy());
assert_eq!(d.take_followup(), None);
}
#[test]
fn change_to_in_flight_wt_triggers_self_recheck_after_settle() {
let mut d = ClusterDriver::new();
d.on_event(DriverEvent::RoutedBatch { wt: wt("/r/a") }); assert_eq!(
d.on_event(DriverEvent::RoutedBatch { wt: wt("/r/a") }),
ClusterAction::Idle
);
assert!(d.pending().is_empty(), "self-recheck is not a queue entry");
d.on_event(DriverEvent::Lsp(LspEvent::FlycheckEnded));
assert_eq!(
d.take_followup(),
Some(ClusterAction::SwitchOverlay { wt: wt("/r/a") }),
"self-recheck re-runs the just-finished WT"
);
}
#[test]
fn deactivated_prunes_queue_and_cancels_self_recheck() {
let mut d = ClusterDriver::new();
d.on_event(DriverEvent::RoutedBatch { wt: wt("/r/a") }); d.on_event(DriverEvent::RoutedBatch { wt: wt("/r/b") }); d.on_event(DriverEvent::RoutedBatch { wt: wt("/r/a") }); assert_eq!(
d.on_event(DriverEvent::Deactivated { wt: wt("/r/b") }),
ClusterAction::Idle
);
assert!(d.pending().is_empty());
d.on_event(DriverEvent::Deactivated { wt: wt("/r/a") });
d.on_event(DriverEvent::Lsp(LspEvent::FlycheckEnded));
assert!(!d.is_busy());
assert_eq!(
d.take_followup(),
None,
"no needless re-check of an idle WT"
);
}
#[test]
fn lsp_event_with_no_transaction_is_idle_never_verdict() {
let mut d = ClusterDriver::new();
assert_eq!(
d.on_event(DriverEvent::Lsp(LspEvent::FlycheckEnded)),
ClusterAction::Idle
);
assert_eq!(
d.on_event(DriverEvent::Lsp(rustc_err("file:///x.rs"))),
ClusterAction::Idle
);
assert!(!d.is_busy());
}
}