///|
/// Controls how many historical snapshots remain after compaction.
/// Daily and weekly windows are measured backwards from the newest snapshot.
pub(all) struct HistoryRetentionPolicy {
format_version : Int
keep_latest : Int
keep_daily_days : Int
keep_weekly_weeks : Int
maximum_observations : Int
} derive(Eq, Debug, ToJson, FromJson)
///|
pub(all) struct RetentionDecision {
original_observations : Int
kept_observations : Int
removed_observations : Int
kept_created_at : Array[Int64]
removed_created_at : Array[Int64]
} derive(Eq, Debug, ToJson, FromJson)
///|
pub fn HistoryRetentionPolicy::default() -> HistoryRetentionPolicy {
HistoryRetentionPolicy::{
format_version: 1,
keep_latest: 10,
keep_daily_days: 30,
keep_weekly_weeks: 26,
maximum_observations: 100,
}
}
///|
pub fn validate_retention_policy(
policy : HistoryRetentionPolicy,
) -> Array[String] {
let issues : Array[String] = []
if policy.format_version != 1 {
issues.push("Unsupported retention policy version.")
}
if policy.keep_latest < 1 {
issues.push("keep_latest must be at least one.")
}
if policy.keep_daily_days < 0 {
issues.push("keep_daily_days cannot be negative.")
}
if policy.keep_weekly_weeks < 0 {
issues.push("keep_weekly_weeks cannot be negative.")
}
if policy.maximum_observations < 1 {
issues.push("maximum_observations must be at least one.")
}
if policy.keep_latest > policy.maximum_observations {
issues.push("keep_latest cannot exceed maximum_observations.")
}
issues
}
///|
fn retention_day(timestamp : Int64) -> Int64 {
timestamp / 86400L
}
///|
fn retention_week(timestamp : Int64) -> Int64 {
timestamp / 604800L
}
///|
fn selected_count(selected : Map[Int64, Bool]) -> Int {
let mut count = 0
for _, keep in selected {
if keep {
count += 1
}
}
count
}
///|
fn choose_retention_time(
selected : Map[Int64, Bool],
created_at : Int64,
maximum : Int,
) -> Unit {
if selected_count(selected) < maximum {
selected[created_at] = true
}
}
///|
/// Select snapshots deterministically. The newest observations are protected
/// first, followed by one newest observation for each daily and weekly bucket.
pub fn plan_history_retention(
history : SnapshotHistory,
policy : HistoryRetentionPolicy,
) -> RetentionDecision raise {
let issues = validate_retention_policy(policy)
if !issues.is_empty() {
raise Failure("Invalid retention policy: " + issues[0])
}
let selected : Map[Int64, Bool] = Map([])
let seen_days : Map[Int64, Bool] = Map([])
let seen_weeks : Map[Int64, Bool] = Map([])
let length = history.snapshots.length()
let newest_time = if length == 0 {
0L
} else {
history.snapshots[length - 1].created_at
}
for offset = 0; offset < length; offset = offset + 1 {
let index = length - 1 - offset
let timestamp = history.snapshots[index].created_at
if offset < policy.keep_latest {
choose_retention_time(selected, timestamp, policy.maximum_observations)
seen_days[retention_day(timestamp)] = true
seen_weeks[retention_week(timestamp)] = true
continue
}
let age = newest_time - timestamp
if age <= policy.keep_daily_days.to_int64() * 86400L {
let day = retention_day(timestamp)
if !seen_days.contains(day) {
seen_days[day] = true
choose_retention_time(selected, timestamp, policy.maximum_observations)
}
} else if age <= policy.keep_weekly_weeks.to_int64() * 604800L {
let week = retention_week(timestamp)
if !seen_weeks.contains(week) {
seen_weeks[week] = true
choose_retention_time(selected, timestamp, policy.maximum_observations)
}
}
}
let kept : Array[Int64] = []
let removed : Array[Int64] = []
for snapshot in history.snapshots {
if selected.contains(snapshot.created_at) {
kept.push(snapshot.created_at)
} else {
removed.push(snapshot.created_at)
}
}
RetentionDecision::{
original_observations: length,
kept_observations: kept.length(),
removed_observations: removed.length(),
kept_created_at: kept,
removed_created_at: removed,
}
}
///|
pub fn compact_history(
history : SnapshotHistory,
decision : RetentionDecision,
) -> SnapshotHistory {
let selected : Map[Int64, Bool] = Map([])
for timestamp in decision.kept_created_at {
selected[timestamp] = true
}
let snapshots = history.snapshots.filter(fn(snapshot) {
selected.contains(snapshot.created_at)
})
{ ..history, snapshots, }
}
///|
pub fn render_retention_decision(decision : RetentionDecision) -> String {
[
"MoonDisk history retention plan",
"Original observations: " + decision.original_observations.to_string(),
"Kept observations: " + decision.kept_observations.to_string(),
"Removed observations: " + decision.removed_observations.to_string(),
"Oldest kept timestamp: " +
(if decision.kept_created_at.is_empty() {
"none"
} else {
decision.kept_created_at[0].to_string()
}),
"Newest kept timestamp: " +
(if decision.kept_created_at.is_empty() {
"none"
} else {
decision.kept_created_at[decision.kept_created_at.length() - 1].to_string()
}),
].join("\n")
}
///|
pub fn retention_policy_to_json(policy : HistoryRetentionPolicy) -> String {
ToJson::to_json(policy).stringify(indent=2)
}
///|
pub fn retention_policy_from_json(
source : String,
) -> HistoryRetentionPolicy raise {
let policy : HistoryRetentionPolicy = @json.from_json(@json.parse(source))
let issues = validate_retention_policy(policy)
if !issues.is_empty() {
raise Failure("Invalid retention policy: " + issues[0])
}
policy
}
///|
pub async fn write_retention_policy(
path : String,
policy : HistoryRetentionPolicy,
) -> Unit {
@fs.write_file(path, retention_policy_to_json(policy))
}
///|
pub async fn read_retention_policy(path : String) -> HistoryRetentionPolicy {
retention_policy_from_json(@fs.read_file(path).text())
}