Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions apexchainx_calculator/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,10 @@ pub(crate) const MAX_HISTORY_SIZE: u32 = 1000;
/// When set, overrides MAX_HISTORY_SIZE for history trimming.
pub(crate) const RETENTION_LIMIT_KEY: Symbol = symbol_short!("RETLIM");

/// Stored prune policy for scheduled retention rules. The cron expression is
/// informational and cadence is driven externally by the operator's scheduler.
const PRUNE_POLICY_KEY: Symbol = symbol_short!("PRPOL");

/// On-chain key storing the ledger sequence of the last config update. Re-exported
/// here so the storage-key namespace regression test catches any future collisions.
pub use crate::config_metadata::LAST_CFG_UPDATE_KEY;
Expand Down Expand Up @@ -715,6 +719,17 @@ impl SLACalculatorContract {
inst.set(&HISTORY_KEY, &Vec::<SLAResult>::new(env));
}

if !inst.has(&PRUNE_POLICY_KEY) {
inst.set(
&PRUNE_POLICY_KEY,
&PrunePolicy {
keep_latest: MAX_HISTORY_SIZE,
max_age_seconds: u64::MAX,
cron_expr: String::from_str(env, ""),
},
);
}

if !inst.has(&CONFIG_KEY) {
let mut configs = Map::<Symbol, SLAConfig>::new(env);
configs.set(
Expand Down Expand Up @@ -2489,6 +2504,46 @@ impl SLACalculatorContract {
.unwrap_or(MAX_HISTORY_SIZE))
}

/// Admin-only write path for a stored prune policy. The policy is
/// informational for cron scheduling, while the actual cadence remains
/// external to the contract execution path.
pub fn set_prune_policy(env: Env, caller: Address, policy: PrunePolicy) -> Result<(), SLAError> {
Self::check_version(&env)?;
Self::require_admin(&env, &caller)?;
env.storage().instance().set(&PRUNE_POLICY_KEY, &policy);
Ok(())
}

/// Returns the stored prune policy. If no policy has been explicitly set,
/// the contract falls back to the default retention profile.
pub fn get_prune_policy(env: Env) -> Result<PrunePolicy, SLAError> {
Self::check_version(&env)?;
Ok(env
.storage()
.instance()
.get(&PRUNE_POLICY_KEY)
.unwrap_or(PrunePolicy {
keep_latest: MAX_HISTORY_SIZE,
max_age_seconds: u64::MAX,
cron_expr: String::from_str(&env, ""),
}))
}

/// Admin-only application of the stored policy to the history store.
/// The current implementation applies the age filter first and then the
/// latest-count retention rule on the resulting history.
pub fn apply_prune_policy(env: Env, caller: Address) -> Result<(), SLAError> {
Self::check_version(&env)?;
Self::require_admin(&env, &caller)?;

let policy = Self::get_prune_policy(env.clone())?;
if policy.max_age_seconds != u64::MAX {
Self::prune_history_by_age(env.clone(), caller.clone(), policy.max_age_seconds)?;
}
Self::prune_history(env, caller, policy.keep_latest)?;
Ok(())
}

/// SC-021 – Migration state read helper
///
/// Returns the storage version and migration posture.
Expand Down
66 changes: 66 additions & 0 deletions apexchainx_calculator/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2924,6 +2924,72 @@ fn test_prune_by_age_operator_cannot_prune() {
client.prune_history_by_age(&actors.operator, &100);
}

#[test]
fn test_set_and_get_prune_policy_round_trips() {
let env = Env::default();
let cid = env.register_contract(None, SLACalculatorContract);
let client = SLACalculatorContractClient::new(&env, &cid);
let admin = soroban_sdk::Address::generate(&env);
let op = soroban_sdk::Address::generate(&env);
client.initialize(&admin, &op);

let policy = PrunePolicy {
keep_latest: 7,
max_age_seconds: 3600,
cron_expr: String::from_str(&env, "0 2 * * 0"),
};

client.set_prune_policy(&admin, &policy);
let stored = client.get_prune_policy();

assert_eq!(stored.keep_latest, 7);
assert_eq!(stored.max_age_seconds, 3600);
assert_eq!(stored.cron_expr, String::from_str(&env, "0 2 * * 0"));
}

#[test]
#[should_panic]
fn test_prune_policy_operator_cannot_set() {
let (_env, client, actors) = setup();
let policy = PrunePolicy {
keep_latest: 7,
max_age_seconds: 3600,
cron_expr: String::from_str(&_env, "0 2 * * 0"),
};
client.set_prune_policy(&actors.operator, &policy);
}

#[test]
fn test_apply_prune_policy_prunes_by_count_and_age() {
let env = Env::default();
env.ledger().set_timestamp(1000);

let cid = env.register_contract(None, SLACalculatorContract);
let client = SLACalculatorContractClient::new(&env, &cid);
let admin = soroban_sdk::Address::generate(&env);
let op = soroban_sdk::Address::generate(&env);
client.initialize(&admin, &op);

client.calculate_sla(&op, &symbol_short!("A"), &symbol_short!("critical"), &5);
client.calculate_sla(&op, &symbol_short!("B"), &symbol_short!("high"), &10);

env.ledger().set_timestamp(2000);
client.calculate_sla(&op, &symbol_short!("C"), &symbol_short!("low"), &10);

let policy = PrunePolicy {
keep_latest: 2,
max_age_seconds: 500,
cron_expr: String::from_str(&env, "0 2 * * 0"),
};
client.set_prune_policy(&admin, &policy);

client.apply_prune_policy(&admin);

let history = client.get_history();
assert_eq!(history.len(), 1);
assert_eq!(history.get(0).unwrap().outage_id, symbol_short!("C"));
}

#[test]
fn test_prune_by_age_emits_event() {
let env = Env::default();
Expand Down
Loading