use parking_lot::RwLock;
use std::sync::Arc;
use std::time::Instant;
#[derive(Clone, Debug)]
pub enum IndexState {
Idle {
since: Instant,
},
Initializing {
progress: u8,
started_at: Instant,
},
Building {
phase: BuildPhase,
indexed_count: usize,
total_count: usize,
started_at: Instant,
},
Updating {
updating_count: usize,
started_at: Instant,
},
Invalidating {
reason: InvalidationReason,
started_at: Instant,
},
Ready {
symbol_count: usize,
file_count: usize,
completed_at: Instant,
},
Degraded {
reason: DegradationReason,
available_symbols: usize,
since: Instant,
},
Error {
message: String,
since: Instant,
},
}
impl IndexState {
pub fn kind(&self) -> IndexStateKind {
match self {
IndexState::Idle { .. } => IndexStateKind::Idle,
IndexState::Initializing { .. } => IndexStateKind::Initializing,
IndexState::Building { .. } => IndexStateKind::Building,
IndexState::Updating { .. } => IndexStateKind::Updating,
IndexState::Invalidating { .. } => IndexStateKind::Invalidating,
IndexState::Ready { .. } => IndexStateKind::Ready,
IndexState::Degraded { .. } => IndexStateKind::Degraded,
IndexState::Error { .. } => IndexStateKind::Error,
}
}
pub fn is_ready(&self) -> bool {
matches!(self, IndexState::Ready { .. })
}
pub fn is_error(&self) -> bool {
matches!(self, IndexState::Error { .. })
}
pub fn is_transitional(&self) -> bool {
matches!(
self,
IndexState::Initializing { .. }
| IndexState::Building { .. }
| IndexState::Updating { .. }
| IndexState::Invalidating { .. }
)
}
pub fn state_started_at(&self) -> Instant {
match self {
IndexState::Idle { since } => *since,
IndexState::Initializing { started_at, .. } => *started_at,
IndexState::Building { started_at, .. } => *started_at,
IndexState::Updating { started_at, .. } => *started_at,
IndexState::Invalidating { started_at, .. } => *started_at,
IndexState::Ready { completed_at, .. } => *completed_at,
IndexState::Degraded { since, .. } => *since,
IndexState::Error { since, .. } => *since,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum IndexStateKind {
Idle,
Initializing,
Building,
Updating,
Invalidating,
Ready,
Degraded,
Error,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum BuildPhase {
Idle,
Scanning,
Indexing,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum InvalidationReason {
ConfigurationChanged,
FileSystemChanged,
ManualRequest,
CacheCorruption,
DependencyChanged,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum DegradationReason {
ParseStorm {
pending_parses: usize,
},
IoError {
message: String,
},
ScanTimeout {
elapsed_ms: u64,
},
ResourceLimit {
kind: ResourceKind,
},
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ResourceKind {
MaxFiles,
MaxSymbols,
MaxCacheBytes,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct IndexStateTransition {
pub from: IndexStateKind,
pub to: IndexStateKind,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct BuildPhaseTransition {
pub from: BuildPhase,
pub to: BuildPhase,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum TransitionResult {
Success,
InvalidTransition {
from: IndexStateKind,
to: IndexStateKind,
},
GuardFailed {
condition: String,
},
}
pub struct IndexStateMachine {
state: Arc<RwLock<IndexState>>,
}
impl IndexStateMachine {
pub fn new() -> Self {
Self { state: Arc::new(RwLock::new(IndexState::Idle { since: Instant::now() })) }
}
pub fn state(&self) -> IndexState {
self.state.read().clone()
}
pub fn transition_to_initializing(&self) -> TransitionResult {
let mut state = self.state.write();
let from_kind = state.kind();
match &*state {
IndexState::Idle { .. } | IndexState::Error { .. } => {
*state = IndexState::Initializing { progress: 0, started_at: Instant::now() };
TransitionResult::Success
}
_ => TransitionResult::InvalidTransition {
from: from_kind,
to: IndexStateKind::Initializing,
},
}
}
pub fn transition_to_building(&self, total_count: usize) -> TransitionResult {
let mut state = self.state.write();
let from_kind = state.kind();
match &*state {
IndexState::Initializing { .. }
| IndexState::Ready { .. }
| IndexState::Degraded { .. } => {
*state = IndexState::Building {
phase: BuildPhase::Idle,
indexed_count: 0,
total_count,
started_at: Instant::now(),
};
TransitionResult::Success
}
_ => TransitionResult::InvalidTransition {
from: from_kind,
to: IndexStateKind::Building,
},
}
}
pub fn transition_to_updating(&self, updating_count: usize) -> TransitionResult {
let mut state = self.state.write();
let from_kind = state.kind();
match &*state {
IndexState::Ready { .. } | IndexState::Degraded { .. } => {
*state = IndexState::Updating { updating_count, started_at: Instant::now() };
TransitionResult::Success
}
_ => TransitionResult::InvalidTransition {
from: from_kind,
to: IndexStateKind::Updating,
},
}
}
pub fn transition_to_invalidating(&self, reason: InvalidationReason) -> TransitionResult {
let mut state = self.state.write();
let from_kind = state.kind();
match &*state {
IndexState::Initializing { .. }
| IndexState::Building { .. }
| IndexState::Updating { .. }
| IndexState::Invalidating { .. } => TransitionResult::InvalidTransition {
from: from_kind,
to: IndexStateKind::Invalidating,
},
_ => {
*state = IndexState::Invalidating { reason, started_at: Instant::now() };
TransitionResult::Success
}
}
}
pub fn transition_to_ready(&self, file_count: usize, symbol_count: usize) -> TransitionResult {
let mut state = self.state.write();
let from_kind = state.kind();
match &*state {
IndexState::Building { .. }
| IndexState::Updating { .. }
| IndexState::Invalidating { .. }
| IndexState::Degraded { .. } => {
*state =
IndexState::Ready { symbol_count, file_count, completed_at: Instant::now() };
TransitionResult::Success
}
IndexState::Ready { .. } => {
*state =
IndexState::Ready { symbol_count, file_count, completed_at: Instant::now() };
TransitionResult::Success
}
_ => TransitionResult::InvalidTransition { from: from_kind, to: IndexStateKind::Ready },
}
}
pub fn transition_to_degraded(&self, reason: DegradationReason) -> TransitionResult {
let mut state = self.state.write();
let from_kind = state.kind();
let available_symbols = match &*state {
IndexState::Ready { symbol_count, .. } => *symbol_count,
IndexState::Degraded { available_symbols, .. } => *available_symbols,
_ => 0,
};
match &*state {
IndexState::Error { .. } => TransitionResult::InvalidTransition {
from: from_kind,
to: IndexStateKind::Degraded,
},
_ => {
*state = IndexState::Degraded { reason, available_symbols, since: Instant::now() };
TransitionResult::Success
}
}
}
pub fn transition_to_error(&self, message: String) -> TransitionResult {
let mut state = self.state.write();
let _from_kind = state.kind();
*state = IndexState::Error { message, since: Instant::now() };
TransitionResult::Success
}
pub fn transition_to_idle(&self) -> TransitionResult {
let mut state = self.state.write();
*state = IndexState::Idle { since: Instant::now() };
TransitionResult::Success
}
pub fn update_building_progress(
&self,
indexed_count: usize,
phase: BuildPhase,
) -> TransitionResult {
let mut state = self.state.write();
match &mut *state {
IndexState::Building { total_count, started_at, .. } => {
*state = IndexState::Building {
phase,
indexed_count,
total_count: *total_count,
started_at: *started_at,
};
TransitionResult::Success
}
_ => TransitionResult::InvalidTransition {
from: state.kind(),
to: IndexStateKind::Building,
},
}
}
pub fn update_initialization_progress(&self, progress: u8) -> TransitionResult {
let mut state = self.state.write();
match &mut *state {
IndexState::Initializing { started_at, .. } => {
*state = IndexState::Initializing {
progress: progress.min(100),
started_at: *started_at,
};
TransitionResult::Success
}
_ => TransitionResult::InvalidTransition {
from: state.kind(),
to: IndexStateKind::Initializing,
},
}
}
}
impl Default for IndexStateMachine {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_initial_state() {
let machine = IndexStateMachine::new();
assert!(matches!(machine.state(), IndexState::Idle { .. }));
}
#[test]
fn test_idle_to_initializing() {
let machine = IndexStateMachine::new();
assert!(matches!(machine.transition_to_initializing(), TransitionResult::Success));
assert!(matches!(machine.state(), IndexState::Initializing { .. }));
}
#[test]
fn test_initializing_to_building() {
let machine = IndexStateMachine::new();
machine.transition_to_initializing();
assert!(matches!(machine.transition_to_building(100), TransitionResult::Success));
assert!(matches!(machine.state(), IndexState::Building { .. }));
}
#[test]
fn test_building_to_ready() {
let machine = IndexStateMachine::new();
machine.transition_to_initializing();
machine.transition_to_building(100);
assert!(matches!(machine.transition_to_ready(100, 5000), TransitionResult::Success));
assert!(matches!(machine.state(), IndexState::Ready { .. }));
}
#[test]
fn test_ready_to_updating() {
let machine = IndexStateMachine::new();
machine.transition_to_initializing();
machine.transition_to_building(100);
machine.transition_to_ready(100, 5000);
assert!(matches!(machine.transition_to_updating(5), TransitionResult::Success));
assert!(matches!(machine.state(), IndexState::Updating { .. }));
}
#[test]
fn test_ready_to_degraded() {
let machine = IndexStateMachine::new();
machine.transition_to_initializing();
machine.transition_to_building(100);
machine.transition_to_ready(100, 5000);
assert!(matches!(
machine.transition_to_degraded(DegradationReason::IoError {
message: "error".to_string()
}),
TransitionResult::Success
));
assert!(matches!(machine.state(), IndexState::Degraded { .. }));
}
#[test]
fn test_any_to_error() {
let machine = IndexStateMachine::new();
assert!(matches!(
machine.transition_to_error("error".to_string()),
TransitionResult::Success
));
assert!(matches!(machine.state(), IndexState::Error { .. }));
}
#[test]
fn test_invalid_transition() {
let machine = IndexStateMachine::new();
assert!(matches!(
machine.transition_to_ready(0, 0),
TransitionResult::InvalidTransition { .. }
));
}
#[test]
fn test_update_building_progress() {
let machine = IndexStateMachine::new();
machine.transition_to_initializing();
machine.transition_to_building(100);
assert!(matches!(
machine.update_building_progress(50, BuildPhase::Indexing),
TransitionResult::Success
));
}
#[test]
fn test_update_initialization_progress() {
let machine = IndexStateMachine::new();
machine.transition_to_initializing();
assert!(matches!(machine.update_initialization_progress(50), TransitionResult::Success));
}
#[test]
fn test_update_initialization_progress_clamps_at_100() {
let machine = IndexStateMachine::new();
machine.transition_to_initializing();
assert!(matches!(machine.update_initialization_progress(250), TransitionResult::Success));
assert!(matches!(machine.state(), IndexState::Initializing { progress: 100, .. }));
}
#[test]
fn test_transition_to_invalidating_rejects_transitional_states() {
let machine = IndexStateMachine::new();
machine.transition_to_initializing();
assert!(matches!(
machine.transition_to_invalidating(InvalidationReason::ManualRequest),
TransitionResult::InvalidTransition {
from: IndexStateKind::Initializing,
to: IndexStateKind::Invalidating
}
));
}
#[test]
fn test_transition_to_degraded_keeps_available_symbols_from_ready() {
let machine = IndexStateMachine::new();
machine.transition_to_initializing();
machine.transition_to_building(100);
machine.transition_to_ready(42, 777);
assert!(matches!(
machine.transition_to_degraded(DegradationReason::ParseStorm { pending_parses: 3 }),
TransitionResult::Success
));
assert!(matches!(machine.state(), IndexState::Degraded { available_symbols: 777, .. }));
}
#[test]
fn test_transition_to_degraded_from_error_is_invalid() {
let machine = IndexStateMachine::new();
machine.transition_to_error("fatal".to_string());
assert!(matches!(
machine.transition_to_degraded(DegradationReason::ResourceLimit {
kind: ResourceKind::MaxFiles
}),
TransitionResult::InvalidTransition {
from: IndexStateKind::Error,
to: IndexStateKind::Degraded
}
));
}
#[test]
fn test_update_building_progress_preserves_total_count() {
let machine = IndexStateMachine::new();
machine.transition_to_initializing();
machine.transition_to_building(123);
assert!(matches!(
machine.update_building_progress(10, BuildPhase::Scanning),
TransitionResult::Success
));
assert!(matches!(
machine.state(),
IndexState::Building {
indexed_count: 10,
total_count: 123,
phase: BuildPhase::Scanning,
..
}
));
}
#[test]
fn test_state_helpers_report_expected_flags() {
let machine = IndexStateMachine::new();
let idle = machine.state();
assert_eq!(idle.kind(), IndexStateKind::Idle);
assert!(!idle.is_ready());
assert!(!idle.is_error());
assert!(!idle.is_transitional());
assert!(idle.state_started_at() <= Instant::now());
machine.transition_to_initializing();
let initializing = machine.state();
assert_eq!(initializing.kind(), IndexStateKind::Initializing);
assert!(initializing.is_transitional());
machine.transition_to_error("boom".to_string());
let error = machine.state();
assert_eq!(error.kind(), IndexStateKind::Error);
assert!(error.is_error());
assert!(!error.is_ready());
}
}