running-process 4.5.5

Subprocess and PTY runtime for the running-process project
Documentation
#![cfg(feature = "client")]

use std::time::{Duration, Instant};

use running_process::broker::server::{
    acquire_spawn_lock, BackendKey, BrokerInstanceKey, SpawnBeginError, SpawnBudgetConfig,
    SpawnCoordinator, SpawnLockError, SpawnOutcome, DEFAULT_SPAWN_ATTEMPTS_PER_WINDOW,
    DEFAULT_SPAWN_BUDGET_WINDOW,
};

fn key(version: &str) -> BackendKey {
    BackendKey::new(BrokerInstanceKey::Shared, "zccache", version)
}

#[test]
fn spawn_budget_exhaustion_is_per_backend_key() {
    let now = Instant::now();
    let mut coordinator =
        SpawnCoordinator::with_config(SpawnBudgetConfig::new(2, Duration::from_secs(10)));
    let first_key = key("1.11.20");
    let second_key = key("1.11.21");

    coordinator.try_begin(first_key.clone(), now).unwrap();
    coordinator.finish(&first_key, SpawnOutcome::Failed, now);
    coordinator.try_begin(first_key.clone(), now).unwrap();
    coordinator.finish(&first_key, SpawnOutcome::Failed, now);

    let err = coordinator.try_begin(first_key, now).unwrap_err();
    assert!(matches!(
        err,
        SpawnBeginError::BudgetExhausted {
            retry_after,
            remaining: 0
        } if retry_after == Duration::from_secs(10)
    ));

    let permit = coordinator.try_begin(second_key, now).unwrap();
    assert_eq!(permit.attempt_number, 1);
}

#[test]
fn cooldown_resets_spawn_budget() {
    let now = Instant::now();
    let mut coordinator =
        SpawnCoordinator::with_config(SpawnBudgetConfig::new(1, Duration::from_secs(10)));
    let key = key("1.11.20");

    coordinator.try_begin(key.clone(), now).unwrap();
    coordinator.finish(&key, SpawnOutcome::Failed, now);
    assert!(matches!(
        coordinator.try_begin(key.clone(), now),
        Err(SpawnBeginError::BudgetExhausted { .. })
    ));

    let permit = coordinator
        .try_begin(key, now + Duration::from_secs(10))
        .unwrap();
    assert_eq!(permit.attempt_number, 1);
}

#[test]
fn single_flight_blocks_duplicate_spawn_for_same_key() {
    let now = Instant::now();
    let mut coordinator = SpawnCoordinator::new();
    let key = key("1.11.20");

    coordinator.try_begin(key.clone(), now).unwrap();

    assert_eq!(
        coordinator.try_begin(key, now),
        Err(SpawnBeginError::AlreadyInProgress)
    );
}

#[test]
fn finishing_success_resets_failure_budget() {
    let now = Instant::now();
    let mut coordinator =
        SpawnCoordinator::with_config(SpawnBudgetConfig::new(2, Duration::from_secs(10)));
    let key = key("1.11.20");

    coordinator.try_begin(key.clone(), now).unwrap();
    coordinator.finish(&key, SpawnOutcome::Failed, now);
    coordinator.try_begin(key.clone(), now).unwrap();
    coordinator.finish(&key, SpawnOutcome::Success, now);

    let snapshot = coordinator.snapshot(key, now);
    assert_eq!(snapshot.attempts_used, 0);
    assert_eq!(snapshot.remaining, 2);
    assert!(!snapshot.in_flight);
    assert_eq!(snapshot.retry_after, None);
}

#[test]
fn snapshot_reports_retry_after_when_budget_is_empty() {
    let now = Instant::now();
    let mut coordinator =
        SpawnCoordinator::with_config(SpawnBudgetConfig::new(1, Duration::from_secs(10)));
    let key = key("1.11.20");

    coordinator.try_begin(key.clone(), now).unwrap();
    coordinator.finish(&key, SpawnOutcome::Failed, now);
    let snapshot = coordinator.snapshot(key, now + Duration::from_secs(3));

    assert_eq!(snapshot.attempts_used, 1);
    assert_eq!(snapshot.remaining, 0);
    assert_eq!(snapshot.retry_after, Some(Duration::from_secs(7)));
}

#[test]
fn default_budget_is_three_attempts_per_thirty_seconds() {
    let now = Instant::now();
    let mut coordinator = SpawnCoordinator::new();
    let key = key("1.11.20");

    assert_eq!(DEFAULT_SPAWN_ATTEMPTS_PER_WINDOW, 3);
    assert_eq!(DEFAULT_SPAWN_BUDGET_WINDOW, Duration::from_secs(30));

    for attempt_number in 1..=3 {
        let permit = coordinator.try_begin(key.clone(), now).unwrap();
        assert_eq!(permit.attempt_number, attempt_number);
        coordinator.finish(&key, SpawnOutcome::Failed, now);
    }

    assert!(matches!(
        coordinator.try_begin(key.clone(), now + Duration::from_secs(29)),
        Err(SpawnBeginError::BudgetExhausted {
            retry_after,
            remaining: 0
        }) if retry_after == Duration::from_secs(1)
    ));

    let permit = coordinator
        .try_begin(key, now + DEFAULT_SPAWN_BUDGET_WINDOW)
        .unwrap();
    assert_eq!(permit.attempt_number, 1);
}

#[test]
fn acquire_spawn_lock_reports_lock_conflict() {
    let tmp = tempfile::tempdir().unwrap();
    let lock_path = tmp.path().join("zccache-1.11.20.spawn.lock");
    let _guard = acquire_spawn_lock(&lock_path).unwrap();

    let err = acquire_spawn_lock(&lock_path).unwrap_err();

    assert!(matches!(err, SpawnLockError::AlreadyLocked { path } if path == lock_path));
}

#[test]
fn spawn_lock_guard_releases_lock_on_drop() {
    let tmp = tempfile::tempdir().unwrap();
    let lock_path = tmp.path().join("zccache-1.11.20.spawn.lock");
    let guard = acquire_spawn_lock(&lock_path).unwrap();
    assert_eq!(guard.path(), lock_path.as_path());
    assert!(lock_path.exists());

    drop(guard);

    let _next_guard = acquire_spawn_lock(&lock_path).unwrap();
}