This commit is contained in:
Alexey
2026-10-30 22:34:37 +03:00
parent e7e588ebe5
commit 5a5009174a
5 changed files with 242 additions and 66 deletions
+94 -23
View File
@@ -2,34 +2,66 @@ use super::*;
#[tokio::test]
async fn quota_period_failed_delete_recreate_restart_must_not_inherit_retired_calendar() {
for secret in ["0123456789abcdef0123456789abcdef", "fedcba9876543210fedcba9876543210"] {
for secret in [
"0123456789abcdef0123456789abcdef",
"fedcba9876543210fedcba9876543210",
] {
quota_period_delete_recreate_restart(true, secret).await;
}
}
#[tokio::test]
async fn quota_period_successful_delete_recreate_restart_has_fresh_calendar() {
for secret in ["0123456789abcdef0123456789abcdef", "fedcba9876543210fedcba9876543210"] {
for secret in [
"0123456789abcdef0123456789abcdef",
"fedcba9876543210fedcba9876543210",
] {
quota_period_delete_recreate_restart(false, secret).await;
}
}
async fn quota_period_delete_recreate_restart(fail_deletion: bool, secret: &str) {
let fixture = Fixture::new();
let create = |secret: &str| serde_json::from_value(serde_json::json!({
"username":"alice", "secret":secret,
"data_quota_bytes":1024, "quota_period":"monthly"
})).unwrap();
create_user(create("0123456789abcdef0123456789abcdef"), None, &fixture.shared).await.unwrap();
let create = |secret: &str| {
serde_json::from_value(serde_json::json!({
"username":"alice", "secret":secret,
"data_quota_bytes":1024, "quota_period":"monthly"
}))
.unwrap()
};
create_user(
create("0123456789abcdef0123456789abcdef"),
None,
&fixture.shared,
)
.await
.unwrap();
fixture.publish().await;
let now = crate::quota_state::now_epoch_secs();
// Model a previously processed future boundary followed by clock rollback.
let next = UserQuotaPeriod::Monthly.next(now).unwrap();
let future = UserQuotaPeriod::Monthly.next(next).unwrap();
fixture.shared.quota_state.process_schedules(future, false).await.unwrap();
fixture
.shared
.quota_state
.process_schedules(future, false)
.await
.unwrap();
fixture.store.current_handle("alice").unwrap().charge(700);
fixture.shared.quota_state.save(&std::collections::BTreeSet::from(["alice".into(), "admin".into()])).await.unwrap();
let old_checkpoint = fixture.store.current_handle("alice").unwrap().schedule_checkpoint();
fixture
.shared
.quota_state
.save(&std::collections::BTreeSet::from([
"alice".into(),
"admin".into(),
]))
.await
.unwrap();
let old_checkpoint = fixture
.store
.current_handle("alice")
.unwrap()
.schedule_checkpoint();
let quota_path = fixture.shared.quota_state.path().to_owned();
let retired_file = quota_path.with_extension("retired");
if fail_deletion {
@@ -41,30 +73,60 @@ async fn quota_period_delete_recreate_restart(fail_deletion: bool, secret: &str)
assert!(fixture.store.current_handle("alice").is_none());
if fail_deletion {
let config_before = std::fs::read(&fixture.shared.config_path).unwrap();
assert!(create_user(create(secret), None, &fixture.shared).await.is_err());
assert_eq!(std::fs::read(&fixture.shared.config_path).unwrap(), config_before);
assert!(
create_user(create(secret), None, &fixture.shared)
.await
.is_err()
);
assert_eq!(
std::fs::read(&fixture.shared.config_path).unwrap(),
config_before
);
assert!(fixture.store.current_handle("alice").is_none());
// Restore the old durable bytes to model storage recovery before retry.
std::fs::remove_dir(&quota_path).unwrap();
std::fs::rename(&retired_file, &quota_path).unwrap();
}
create_user(create(secret), None, &fixture.shared).await.unwrap();
create_user(create(secret), None, &fixture.shared)
.await
.unwrap();
assert_eq!(fixture.store.used("alice"), 0);
assert_eq!(fixture.store.current_handle("alice").unwrap().schedule_checkpoint().period_cursor_epoch_secs, 0);
assert_eq!(
fixture
.store
.current_handle("alice")
.unwrap()
.schedule_checkpoint()
.period_cursor_epoch_secs,
0
);
let cfg = Arc::new(ProxyConfig::load(&fixture.shared.config_path).unwrap());
let restart_store = Arc::new(QuotaStore::default());
let restart = QuotaStateOwner::new(quota_path, restart_store.clone());
restart.load_for_config(&cfg).await.unwrap();
let authority = UserAdmissionAuthority::new_with_quota_store(restart_store.clone());
let gate = restart.lock().await;
authority.activate_config_source(1, None, &cfg.access.users, &cfg.access.user_enabled).unwrap();
authority
.activate_config_source(1, None, &cfg.access.users, &cfg.access.user_enabled)
.unwrap();
authority.publish_quota_config(1, &cfg, true);
drop(gate);
restart.process_schedules(now, true).await.unwrap();
let actual = restart_store.current_handle("alice").unwrap().schedule_checkpoint();
let actual = restart_store
.current_handle("alice")
.unwrap()
.schedule_checkpoint();
assert!(actual.period_cursor_epoch_secs < old_checkpoint.period_cursor_epoch_secs);
assert_eq!(restart_store.schedule_deadline(), Some(next), "recreated account inherited a retired calendar watermark across restart");
assert_eq!(restart_store.used("alice"), 0, "recreated account inherited retired usage");
assert_eq!(
restart_store.schedule_deadline(),
Some(next),
"recreated account inherited a retired calendar watermark across restart"
);
assert_eq!(
restart_store.used("alice"),
0,
"recreated account inherited retired usage"
);
fixture.finish().await;
}
@@ -82,11 +144,16 @@ async fn quota_period_create_rejects_unreadable_checkpoint_without_publishing_us
let result = create_user(
serde_json::from_value(serde_json::json!({
"username":"alice", "data_quota_bytes":1024, "quota_period":"monthly"
})).unwrap(),
}))
.unwrap(),
None,
&fixture.shared,
).await;
assert!(result.is_err(), "CREATE must reject untrusted durable quota state");
)
.await;
assert!(
result.is_err(),
"CREATE must reject untrusted durable quota state"
);
assert_eq!(std::fs::read(&fixture.shared.config_path).unwrap(), before);
assert!(fixture.store.current_handle("alice").is_none());
fixture.finish().await;
@@ -103,10 +170,14 @@ async fn quota_period_create_revision_conflict_does_not_touch_retired_checkpoint
serde_json::from_value(serde_json::json!({"username":"alice"})).unwrap(),
Some("outdated-revision".into()),
&fixture.shared,
).await;
)
.await;
assert_eq!(result.err().unwrap().code, "revision_conflict");
assert_eq!(std::fs::read(&fixture.shared.config_path).unwrap(), before);
assert_eq!(std::fs::read(fixture.shared.quota_state.path()).unwrap(), retired);
assert_eq!(
std::fs::read(fixture.shared.quota_state.path()).unwrap(),
retired
);
assert!(fixture.store.current_handle("alice").is_none());
fixture.finish().await;
}