Skip to content

Job retention - #197

Open
airhorns wants to merge 4 commits into
mainfrom
retention-scanner
Open

Job retention#197
airhorns wants to merge 4 commits into
mainfrom
retention-scanner

Conversation

@airhorns

@airhorns airhorns commented Mar 19, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Removes DeleteTerminalJob task variant, cleanup_buffer from the broker, and all per-job cleanup task scheduling/clearing from cancel, lease, restart, and import paths
  • Adds a periodic background scanner (spawn_retention_scanner) that finds and deletes expired terminal jobs by scanning the 0x03 status/time index
  • Upgrades config to use Duration-based fields with duration_str parsing (SlateDB pattern) for default_terminal_retention and retention_scan_interval

The 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 passes
  • cargo 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_ms value 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 (proto enqueue/import + JobInfo/flatbuffers/codec, job query schema, and gRPC GetJob responses), updates config to support Duration-based default_terminal_retention and retention_scan_interval parsed via duration-str, and introduces retention Prometheus metrics. Manual delete_job is 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.

@airhorns airhorns changed the title Replace task-based retention with periodic background scanner Job retention Mar 19, 2026
@airhorns
airhorns marked this pull request as ready for review March 19, 2026 22:17
@airhorns
airhorns force-pushed the retention-scanner branch 2 times, most recently from a34984e to 63f3413 Compare March 19, 2026 22:43
Comment thread schema/internal_storage.fbs Outdated
@airhorns
airhorns force-pushed the retention-scanner branch 2 times, most recently from 6c2027b to 72dc8d5 Compare March 20, 2026 12:55
Comment thread src/job_store_shard/mod.rs Outdated

@airhorns airhorns left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • 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

Comment thread docs/src/content/docs/guides/cancel-restart-delete.mdx Outdated
Comment thread src/job_store_shard/enqueue.rs Outdated
Comment thread src/job_store_shard/mod.rs Outdated

writer.delete(job_info_key(tenant, job_id))?;
writer.delete(job_status_key(tenant, job_id))?;
writer.delete(job_cancelled_key(tenant, job_id))?;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this seems like it duplicates the logic we already have for job deletion, figure out how to DRY this up

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we dont actually need to await this here, its ok, if this doesnt commit the next scan will pick it up

Comment thread docs/src/content/docs/reference/server-configuration.mdx Outdated
Comment thread docs/src/content/docs/reference/server-configuration.mdx Outdated
@airhorns
airhorns force-pushed the retention-scanner branch from 9cbbf88 to 157feaf Compare March 20, 2026 16:10
Comment thread src/job_store_shard/retention.rs
@airhorns
airhorns force-pushed the retention-scanner branch from 157feaf to 894dbf9 Compare March 20, 2026 16:47
Comment thread src/job_store_shard/import.rs Outdated
@airhorns
airhorns force-pushed the retention-scanner branch from 894dbf9 to aaec121 Compare March 20, 2026 17:21
Comment thread src/job_store_shard/retention.rs
Comment thread src/job_store_shard/retention.rs Outdated
Comment thread src/settings.rs Outdated
@airhorns
airhorns force-pushed the retention-scanner branch from aac0913 to 331806e Compare March 20, 2026 19:39
Comment thread src/job_store_shard/retention.rs
Comment thread src/job_store_shard/retention.rs
Comment thread src/settings.rs
#[serde(default = "default_retention_scan_interval")]
#[serde(deserialize_with = "duration_str::deserialize_duration")]
#[serde(serialize_with = "serialize_duration")]
pub retention_scan_interval: Duration,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Fix in Cursor Fix in Web

airhorns and others added 4 commits March 23, 2026 15:01
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>
@airhorns
airhorns force-pushed the retention-scanner branch from 331806e to b9aafc2 Compare April 11, 2026 16:41

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

There are 3 total unresolved issues (including 1 from previous review).

Fix All in Cursor

❌ 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,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit b9aafc2. Configure here.

Comment thread src/settings.rs
format!("{}ms", duration.as_millis())
};
serializer.serialize_str(&duration_str)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit b9aafc2. Configure here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant