mirror of
https://github.com/telemt/telemt.git
synced 2026-10-31 21:02:01 +03:00
Retired Quota State retention from surviving user recreation
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
use super::*;
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
async fn fixture() -> (tempfile::TempDir, Arc<QuotaStateOwner>) {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let owner = QuotaStateOwner::new(dir.path().join("quota.json"), Arc::new(QuotaStore::default()));
|
||||
let mut state = QuotaStateFile::default();
|
||||
state.last_reset_epoch_secs = 123;
|
||||
state.users.insert("alice".into(), quota_user_state(UserQuotaSnapshot {
|
||||
used_bytes: 700,
|
||||
last_reset_epoch_secs: 123,
|
||||
}));
|
||||
state.users.insert("bob".into(), QuotaUserState {
|
||||
used_bytes: 321,
|
||||
last_reset_epoch_secs: 111,
|
||||
schedule: QuotaScheduleCheckpoint {
|
||||
quota_period: Some(crate::config::UserQuotaPeriod::Monthly),
|
||||
period_cursor_epoch_secs: 1_793_491_200,
|
||||
},
|
||||
});
|
||||
write_state_file(owner.path(), state).await.unwrap();
|
||||
(dir, owner)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn creation_removes_only_retired_row_and_returns_owned_gate() {
|
||||
let (_dir, owner) = fixture().await;
|
||||
let before = read_state_file(owner.path()).await.unwrap().unwrap();
|
||||
let guard = owner.lock().await;
|
||||
let guard = owner.prepare_user_creation_locked("alice", guard).await.unwrap();
|
||||
assert!(owner.mutation.try_lock().is_err());
|
||||
let after = read_state_file(owner.path()).await.unwrap().unwrap();
|
||||
assert!(!after.users.contains_key("alice"));
|
||||
assert_eq!(serde_json::to_value(&after.users["bob"]).unwrap(),
|
||||
serde_json::to_value(&before.users["bob"]).unwrap());
|
||||
assert_eq!(after.last_reset_epoch_secs, before.last_reset_epoch_secs);
|
||||
drop(guard);
|
||||
assert!(owner.mutation.try_lock().is_ok());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn creation_without_checkpoint_does_not_require_a_write() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let path = dir.path().join("missing/quota.json");
|
||||
let owner = QuotaStateOwner::new(path.clone(), Arc::new(QuotaStore::default()));
|
||||
let guard = owner.lock().await;
|
||||
let _guard = owner.prepare_user_creation_locked("alice", guard).await.unwrap();
|
||||
assert!(!path.exists());
|
||||
assert!(owner.mutation.try_lock().is_err());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn creation_retries_durability_after_ambiguous_removal() {
|
||||
let (_dir, owner) = fixture().await;
|
||||
let (hook, _) = test_io::Hook::install(owner.path(), false);
|
||||
hook.fail_after.store(true, Ordering::Relaxed);
|
||||
let guard = owner.lock().await;
|
||||
assert!(owner.prepare_user_creation_locked("alice", guard).await.is_err());
|
||||
assert!(!read_state_file(owner.path()).await.unwrap().unwrap().users.contains_key("alice"));
|
||||
// The absent row does not prove that the previous directory sync succeeded.
|
||||
let guard = owner.lock().await;
|
||||
assert!(owner.prepare_user_creation_locked("alice", guard).await.is_err());
|
||||
assert_eq!(hook.writes.load(Ordering::Relaxed), 2);
|
||||
hook.fail_after.store(false, Ordering::Relaxed);
|
||||
let guard = owner.lock().await;
|
||||
let _guard = owner.prepare_user_creation_locked("alice", guard).await.unwrap();
|
||||
assert_eq!(hook.writes.load(Ordering::Relaxed), 3);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn creation_waiter_abort_keeps_gate_until_durable_removal_finishes() {
|
||||
let (_dir, owner) = fixture().await;
|
||||
let (hook, started) = test_io::Hook::install(owner.path(), true);
|
||||
let operation = owner.clone();
|
||||
let waiter = tokio::spawn(async move {
|
||||
let guard = operation.lock().await;
|
||||
operation.prepare_user_creation_locked("alice", guard).await
|
||||
});
|
||||
started.await.unwrap();
|
||||
waiter.abort();
|
||||
assert!(waiter.await.unwrap_err().is_cancelled());
|
||||
assert!(owner.mutation.try_lock().is_err());
|
||||
assert!(read_state_file(owner.path()).await.unwrap().unwrap().users.contains_key("alice"));
|
||||
hook.release();
|
||||
let _guard = tokio::time::timeout(std::time::Duration::from_secs(2), owner.lock()).await.unwrap();
|
||||
assert!(!read_state_file(owner.path()).await.unwrap().unwrap().users.contains_key("alice"));
|
||||
assert_eq!(hook.writes.load(Ordering::Relaxed), 1);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn creation_write_failure_keeps_retired_checkpoint_for_safe_retry() {
|
||||
let (_dir, owner) = fixture().await;
|
||||
let retired = owner.path().with_extension("retired");
|
||||
let (hook, started) = test_io::Hook::install(owner.path(), true);
|
||||
let operation = owner.clone();
|
||||
let writer = tokio::spawn(async move {
|
||||
let guard = operation.lock().await;
|
||||
operation.prepare_user_creation_locked("alice", guard).await
|
||||
});
|
||||
started.await.unwrap();
|
||||
std::fs::rename(owner.path(), &retired).unwrap();
|
||||
std::fs::create_dir(owner.path()).unwrap();
|
||||
hook.release();
|
||||
assert!(writer.await.unwrap().is_err());
|
||||
std::fs::remove_dir(owner.path()).unwrap();
|
||||
std::fs::rename(retired, owner.path()).unwrap();
|
||||
assert!(read_state_file(owner.path()).await.unwrap().unwrap().users.contains_key("alice"));
|
||||
let guard = owner.lock().await;
|
||||
let _guard = owner.prepare_user_creation_locked("alice", guard).await.unwrap();
|
||||
assert!(!read_state_file(owner.path()).await.unwrap().unwrap().users.contains_key("alice"));
|
||||
}
|
||||
Reference in New Issue
Block a user