Job retention - #197
Conversation
a34984e to
63f3413
Compare
6c2027b to
72dc8d5
Compare
airhorns
left a comment
There was a problem hiding this comment.
- add counter metrics for each shard that count how many retention scans have been done and how many jobs have been cleaned up by the retention scanner, document in the metrics docs
|
|
||
| writer.delete(job_info_key(tenant, job_id))?; | ||
| writer.delete(job_status_key(tenant, job_id))?; | ||
| writer.delete(job_cancelled_key(tenant, job_id))?; |
There was a problem hiding this comment.
this seems like it duplicates the logic we already have for job deletion, figure out how to DRY this up
There was a problem hiding this comment.
this is still duplicative with the other delete path, which is very risky where we might let the bookkeeping diverge. unify the two paths
| } | ||
|
|
||
| txn.commit_with_options(&WriteOptions { | ||
| await_durable: true, |
There was a problem hiding this comment.
we dont actually need to await this here, its ok, if this doesnt commit the next scan will pick it up
9cbbf88 to
157feaf
Compare
157feaf to
894dbf9
Compare
894dbf9 to
aaec121
Compare
aac0913 to
331806e
Compare
| #[serde(default = "default_retention_scan_interval")] | ||
| #[serde(deserialize_with = "duration_str::deserialize_duration")] | ||
| #[serde(serialize_with = "serialize_duration")] | ||
| pub retention_scan_interval: Duration, |
There was a problem hiding this comment.
Serde default conflicts with custom deserializer for Duration
High Severity
The default_terminal_retention and retention_scan_interval fields use both #[serde(default = "...")] and #[serde(deserialize_with = "duration_str::deserialize_duration")]. When the field is absent from config, serde uses the default function (returning a Duration), so that works. But when the field is present, duration_str::deserialize_duration parses a string like "7d". The PR reviewer flagged this — the old numeric test defaults (like Duration::from_secs(86400)) work programmatically but a TOML config with a plain number (e.g., default_terminal_retention = 0) would fail deserialization since duration_str expects a string, not a number. This is a breaking change for users who might try numeric values in config.
Additional Locations (1)
Adds configurable retention for terminal jobs (succeeded, failed, cancelled). After a job's retention period elapses, a background scanner automatically deletes it and all associated records. Key changes: **Proto & storage schema** - Add `terminal_retention_s` field to EnqueueRequest, ImportJobRequest, GetJobResponse in proto/silo.proto - Add `terminal_retention_s` field to JobInfo in FlatBuffer schema **Core retention logic** (src/job_store_shard/retention.rs) - `effective_terminal_retention_s()` resolves per-job or shard default - `delete_job_records_in_txn()` atomically removes all job data (info, status, index, metadata index, attempts, counters) - `spawn_retention_scanner()` runs periodic background scan - `run_retention_scan()` scans the status/time index, skips non-terminal entries via key-only parsing, checks per-job retention, deletes expired jobs in serializable transactions - `delete_expired_job()` re-verifies terminal status within txn to guard against concurrent restart races **Config** (src/settings.rs) - `default_terminal_retention: Duration` (default 7 days) with duration_str parsing for human-readable values like "7d", "12h" - `retention_scan_interval: Duration` (default 12 hours) - Uses same duration serialization pattern as SlateDB config - Adds `duration-str` as direct dependency **Integration** - Enqueue stores effective retention in job info - Import resolves and stores retention for imported jobs - delete_job() uses transactional deletion via delete_job_records_in_txn - gRPC server validates and passes through terminal_retention_s - Query engine exposes terminal_retention_s column - TypeScript client supports terminal_retention_s field Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The concurrency tests used count_task_keys() which counts ALL task keys including background retention scanner tasks. Replace with count_request_ticket_tasks() that filters to only RequestTicket tasks. Also make retention_scan_interval configurable in test helpers so only retention-specific tests get the fast 100ms scan interval. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Fix zero retention_scan_interval panic by early-returning from scanner - Delay first retention scan to avoid expensive I/O during shard acquisition - Fix reimport to apply new terminal_retention_ms from params instead of preserving the existing job's value - Update TS proto comments to reflect millisecond units Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Wrap initial sleep in tokio::select! with cancellation token so shard close doesn't block for up to 12 hours - Fix duration serializer to produce parseable format: use whole seconds when possible, otherwise total milliseconds (not "1s+500ms") Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
331806e to
b9aafc2
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
There are 3 total unresolved issues (including 1 from previous review).
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit b9aafc2. Configure here.
| wal_close_config, | ||
| metrics, | ||
| concurrency_reconcile_interval, | ||
| default_terminal_retention_ms: default_terminal_retention.as_millis() as i64, |
There was a problem hiding this comment.
Duration as_millis cast to i64 silently overflows
Low Severity
default_terminal_retention.as_millis() returns u128, and casting it to i64 with as silently truncates/wraps on overflow. A user-configured retention exceeding ~292 million years would wrap to a negative value, which effective_terminal_retention_ms would then reject. More practically, any Duration exceeding i64::MAX milliseconds (~106 days worth of u128 range beyond i64::MAX) would produce an incorrect retention value without any error.
Reviewed by Cursor Bugbot for commit b9aafc2. Configure here.
| format!("{}ms", duration.as_millis()) | ||
| }; | ||
| serializer.serialize_str(&duration_str) | ||
| } |
There was a problem hiding this comment.
serialize_duration loses sub-millisecond Duration precision on roundtrip
Low Severity
serialize_duration checks subsec_millis() == 0 to decide between seconds and milliseconds format, but subsec_millis() only returns the millisecond component (0–999) of the sub-second portion. A duration with only microsecond or nanosecond precision (e.g. from a round-trip through duration_str parsing) would have subsec_millis() == 0 yet non-zero sub-second content, causing it to serialize as whole seconds and lose precision. The condition likely intended subsec_nanos() == 0.
Reviewed by Cursor Bugbot for commit b9aafc2. Configure here.


Summary
DeleteTerminalJobtask variant,cleanup_bufferfrom the broker, and all per-job cleanup task scheduling/clearing from cancel, lease, restart, and import pathsspawn_retention_scanner) that finds and deletes expired terminal jobs by scanning the0x03status/time indexDuration-based fields withduration_strparsing (SlateDB pattern) fordefault_terminal_retentionandretention_scan_intervalThe scanner runs at a configurable interval (default 12h), skips non-terminal entries via key-only parsing, and deletes expired jobs in serializable transactions with concurrent-restart guards. Net -122 lines.
Test plan
cargo test --test job_retention_tests— all 10 retention-specific tests pass (scanner-based deletion, restart race, explicit delete, import retention)cargo test --test job_store_shard_import_tests— all 80 import tests pass (no cleanup task assertions)cargo test -- --skip k8s_ --skip etcd_ --skip coordination_split_tests --skip turmoil— full test suite passescargo clippy— no warnings🤖 Generated with Claude Code
Note
Medium Risk
Adds a background retention scanner that deletes terminal jobs and changes job storage/proto/config surfaces to include retention; mistakes could lead to unintended job deletions or extra load from periodic scans.
Overview
Adds terminal job retention: jobs now persist a
terminal_retention_msvalue at enqueue/import time (defaulting from shard config) and the shard spawns a periodic retention scanner that scans the status/time index and deletes expired terminal jobs transactionally.Extends the API/storage surface to carry retention (
protoenqueue/import +JobInfo/flatbuffers/codec, job query schema, and gRPCGetJobresponses), updates config to supportDuration-baseddefault_terminal_retentionandretention_scan_intervalparsed viaduration-str, and introduces retention Prometheus metrics. Manualdelete_jobis refactored to reuse the same transactional record-deletion helper to keep indexes/counters consistent.Reviewed by Cursor Bugbot for commit b9aafc2. Bugbot is set up for automated code reviews on this repo. Configure here.