mirror of
https://github.com/telemt/telemt.git
synced 2026-09-10 20:44:08 +03:00
012dc07a98
Co-Authored-By: brekotis <93345790+brekotis@users.noreply.github.com>
221 lines
7.8 KiB
Rust
221 lines
7.8 KiB
Rust
use std::sync::Arc;
|
|
use std::sync::atomic::{AtomicUsize, Ordering};
|
|
|
|
use arc_swap::ArcSwap;
|
|
use tokio::sync::Barrier;
|
|
|
|
use super::*;
|
|
use crate::config::ProxyConfig;
|
|
use crate::maestro::generation::test_runtime_generation;
|
|
|
|
fn test_runtime() -> (
|
|
Arc<WebProcessRuntime>,
|
|
Arc<crate::maestro::generation::RuntimeGeneration>,
|
|
) {
|
|
let generation = test_runtime_generation(1, ProxyConfig::default());
|
|
let runtime = WebProcessRuntime::start(Arc::new(ArcSwap::from(Arc::clone(&generation))));
|
|
(runtime, generation)
|
|
}
|
|
|
|
async fn stop_runtime(
|
|
runtime: Arc<WebProcessRuntime>,
|
|
generation: Arc<crate::maestro::generation::RuntimeGeneration>,
|
|
) {
|
|
runtime.shutdown().await;
|
|
generation.stop_sessions().await;
|
|
generation.stop_background_tasks().await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn pause_is_idempotent_and_resume_advances_only_real_transitions() {
|
|
let (runtime, generation) = test_runtime();
|
|
let initial = runtime.operator_lifecycle_status();
|
|
assert_eq!(initial.state, OperatorLifecycleState::Running);
|
|
assert_eq!(initial.epoch, 0);
|
|
|
|
let paused = runtime.pause_operator().await.unwrap();
|
|
assert_eq!(paused.state, OperatorLifecycleState::Paused);
|
|
assert!(!paused.admission_open);
|
|
let repeated_pause = runtime.pause_operator().await.unwrap();
|
|
assert_eq!(repeated_pause.epoch, paused.epoch);
|
|
|
|
let resumed = runtime.resume_operator().await.unwrap();
|
|
assert_eq!(resumed.state, OperatorLifecycleState::Running);
|
|
assert!(resumed.admission_open);
|
|
let repeated_resume = runtime.resume_operator().await.unwrap();
|
|
assert_eq!(repeated_resume.epoch, resumed.epoch);
|
|
|
|
stop_runtime(runtime, generation).await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn pause_waits_for_pre_cutover_admission_and_rejects_late_registration() {
|
|
let (runtime, generation) = test_runtime();
|
|
let registration = runtime.try_operator_admission().unwrap();
|
|
let pause_runtime = Arc::clone(&runtime);
|
|
let pause = tokio::spawn(async move { pause_runtime.pause_operator().await.unwrap() });
|
|
tokio::task::yield_now().await;
|
|
|
|
assert!(!pause.is_finished());
|
|
assert_eq!(
|
|
runtime.try_operator_admission().err(),
|
|
Some(super::super::ManagerError::AdmissionPaused)
|
|
);
|
|
drop(registration);
|
|
let paused = pause.await.unwrap();
|
|
assert_eq!(paused.state, OperatorLifecycleState::Paused);
|
|
|
|
stop_runtime(runtime, generation).await;
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
|
|
async fn pause_fence_leaves_no_late_admission_commits_under_scheduler_pressure() {
|
|
const ATTEMPTS: usize = 10_000;
|
|
|
|
let (runtime, generation) = test_runtime();
|
|
let start = Arc::new(Barrier::new(ATTEMPTS + 1));
|
|
let committed = Arc::new(AtomicUsize::new(0));
|
|
let mut attempts = tokio::task::JoinSet::new();
|
|
for _ in 0..ATTEMPTS {
|
|
let runtime = Arc::clone(&runtime);
|
|
let start = Arc::clone(&start);
|
|
let committed = Arc::clone(&committed);
|
|
attempts.spawn(async move {
|
|
start.wait().await;
|
|
if let Ok(_registration) = runtime.try_operator_admission() {
|
|
tokio::task::yield_now().await;
|
|
committed.fetch_add(1, Ordering::Release);
|
|
}
|
|
});
|
|
}
|
|
start.wait().await;
|
|
runtime.pause_operator().await.unwrap();
|
|
let committed_at_pause = committed.load(Ordering::Acquire);
|
|
while let Some(result) = attempts.join_next().await {
|
|
result.unwrap();
|
|
}
|
|
|
|
assert_eq!(committed.load(Ordering::Acquire), committed_at_pause);
|
|
assert!(matches!(
|
|
runtime.try_operator_admission(),
|
|
Err(super::super::ManagerError::AdmissionPaused)
|
|
));
|
|
stop_runtime(runtime, generation).await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn empty_drain_completes_gracefully_and_stays_closed_until_resume() {
|
|
let (runtime, generation) = test_runtime();
|
|
let accepted = runtime.drain_operator(Duration::from_secs(30)).await.unwrap();
|
|
assert_eq!(accepted.state, OperatorLifecycleState::Draining);
|
|
|
|
let completed = tokio::time::timeout(Duration::from_secs(1), async {
|
|
loop {
|
|
let status = runtime.operator_lifecycle_status();
|
|
if status.state == OperatorLifecycleState::Drained {
|
|
break status;
|
|
}
|
|
tokio::task::yield_now().await;
|
|
}
|
|
})
|
|
.await
|
|
.unwrap();
|
|
let drain = completed.drain.unwrap();
|
|
assert_eq!(drain.state, OperatorDrainState::Completed);
|
|
assert_eq!(drain.outcome, Some(OperatorDrainOutcome::Graceful));
|
|
assert!(!completed.admission_open);
|
|
|
|
let resumed = runtime.resume_operator().await.unwrap();
|
|
assert_eq!(resumed.state, OperatorLifecycleState::Running);
|
|
stop_runtime(runtime, generation).await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn resume_after_force_commit_cancels_wait_but_preserves_force_evidence() {
|
|
let (runtime, generation) = test_runtime();
|
|
let sequence = 7;
|
|
let cancellation = CancellationToken::new();
|
|
{
|
|
let mut inner = runtime.operator_lifecycle.inner.lock();
|
|
runtime.operator_lifecycle.admission.close();
|
|
inner.active = Some(ActiveDrain {
|
|
sequence,
|
|
cancellation,
|
|
});
|
|
inner.drain = Some(OperatorDrainStatus {
|
|
operation_id: format!("wd1.{}.{sequence:016x}", runtime.runtime_instance()),
|
|
state: OperatorDrainState::Draining,
|
|
outcome: None,
|
|
timeout_secs: 30,
|
|
started_epoch_millis: 1,
|
|
deadline_epoch_millis: 30_001,
|
|
completed_epoch_millis: None,
|
|
remaining_sessions: 1,
|
|
remaining_streams: 0,
|
|
remaining_websockets: 0,
|
|
force_close_signalled: false,
|
|
});
|
|
runtime
|
|
.operator_lifecycle
|
|
.transition_locked(&mut inner, OperatorLifecycleState::Draining);
|
|
runtime.operator_lifecycle.publish_locked(&inner);
|
|
}
|
|
assert!(runtime.operator_lifecycle.commit_force(
|
|
sequence,
|
|
WorkCounts {
|
|
sessions: 1,
|
|
streams: 0,
|
|
websockets: 0,
|
|
}
|
|
));
|
|
|
|
let resumed = runtime.resume_operator().await.unwrap();
|
|
assert_eq!(resumed.state, OperatorLifecycleState::Running);
|
|
let drain = resumed.drain.unwrap();
|
|
assert_eq!(drain.state, OperatorDrainState::Cancelled);
|
|
assert_eq!(drain.outcome, Some(OperatorDrainOutcome::Cancelled));
|
|
assert!(drain.force_close_signalled);
|
|
stop_runtime(runtime, generation).await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn reload_cannot_reopen_operator_pause_and_resume_cannot_override_disabled_config() {
|
|
let mut initial_config = ProxyConfig::default();
|
|
initial_config.web.enabled = true;
|
|
let initial = test_runtime_generation(1, initial_config);
|
|
let active = Arc::new(ArcSwap::from(Arc::clone(&initial)));
|
|
let runtime = WebProcessRuntime::start(Arc::clone(&active));
|
|
let paused = runtime.pause_operator().await.unwrap();
|
|
assert!(!paused.admission_open);
|
|
|
|
let disabled = test_runtime_generation(2, ProxyConfig::default());
|
|
active.store(Arc::clone(&disabled));
|
|
let after_reload = runtime.operator_lifecycle_status();
|
|
assert_eq!(after_reload.state, OperatorLifecycleState::Paused);
|
|
let resumed = runtime.resume_operator().await.unwrap();
|
|
assert!(resumed.admission_open);
|
|
assert!(!resumed.effective_new_work_admission);
|
|
|
|
runtime.shutdown().await;
|
|
initial.stop_sessions().await;
|
|
initial.stop_background_tasks().await;
|
|
disabled.stop_sessions().await;
|
|
disabled.stop_background_tasks().await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn shutdown_terminally_prevents_resume() {
|
|
let (runtime, generation) = test_runtime();
|
|
runtime.pause_operator().await.unwrap();
|
|
let drain = runtime.begin_shutdown();
|
|
assert!(matches!(
|
|
runtime.resume_operator().await,
|
|
Err(OperatorLifecycleError::Closed)
|
|
));
|
|
let _ = drain
|
|
.wait_until(tokio::time::Instant::now() + Duration::from_secs(1))
|
|
.await;
|
|
generation.stop_sessions().await;
|
|
generation.stop_background_tasks().await;
|
|
}
|