Skip to content

episodic: scheduled retention/GC - #61

Merged
mkorbi merged 1 commit into
feat/redis-driverfrom
feat/episodic-retention
Jul 28, 2026
Merged

episodic: scheduled retention/GC#61
mkorbi merged 1 commit into
feat/redis-driverfrom
feat/episodic-retention

Conversation

@mkorbi

@mkorbi mkorbi commented Jul 25, 2026

Copy link
Copy Markdown
Member

Stacked on #60 (redis driver).

Config-driven background scheduler that prunes the episodic event log by policy — the composition-root counterpart to the kv TTL sweepers. Opt-in per namespace; episodic-only.

retention: { max_age_seconds: N, max_items: N, action: hard|soft, interval_seconds: N }
  • internal/retention.Scheduler: one ticker goroutine per policy, driving the existing episodic Expire in bounded batches (age → BeforeTime; count → reverse-limit-1 max-cursor probe + BeforeCursor)
  • Sits above the service layer on raw storage namespaces → with tenant_isolation on, fans out to every tenant's namespace via a new optional NamespaceLister driver capability (memory map keys; pg SELECT DISTINCT namespace) matched back with new auth.UnqualifyNamespace
  • obs.EvictionRetention cause on memsidecar.eviction.total; lifecycle wired for LIFO shutdown
  • config RetentionConfig + validation (+tests); white-box scheduler tests; docs

Verified: unit suite + full flow (age, count, tenant fan-out) against a real Postgres episodic driver.

🤖 Generated with Claude Code

A config-driven background scheduler that prunes the episodic event log by
policy — the composition-root counterpart to the kv drivers' per-record TTL
sweepers. Opt-in per namespace; episodic only (other blocks reject a
retention block).

Policy (per namespace):
  retention:
    max_age_seconds: N   # delete events older than N
    max_items: N         # and/or keep only the newest N by cursor
    action: hard|soft    # remove (default) or tombstone
    interval_seconds: N  # sweep cadence; 0 = default 300s

The scheduler (internal/retention) runs one ticker goroutine per policy,
driving the existing episodic Expire primitive in bounded batches: age via
BeforeTime, count via a reverse-limit-1 max-cursor probe + BeforeCursor.
It lives above the service layer on raw storage namespaces, so with
tenant_isolation on it discovers and prunes every tenant's namespace via a
new optional NamespaceLister driver capability (memory: map keys; postgres:
SELECT DISTINCT namespace) matched back to the config namespace with a new
auth.UnqualifyNamespace helper. Pruned events increment
memsidecar.eviction.total{block="episodic",cause="retention"}.

Lifecycle: constructed in main after the registries, Started, and Close
registered so LIFO shutdown stops sweeps before drivers close.

- internal/retention: Scheduler + PoliciesFromConfig + white-box tests
  (age, count-keeps-newest, soft-tombstone, tenant fan-out, helpers)
- config: RetentionConfig + Enabled() + per-namespace validation (+tests)
- episodic memory/postgres: Namespaces() capability
- auth.UnqualifyNamespace; obs.EvictionRetention cause
- docs: episodic scheduled-retention section + config reference + example.yaml

Verified: unit suite green; full retention flow (age, count, tenant fan-out)
run against a real Postgres episodic driver; go build/vet, go test -race
./..., golangci-lint ./... all clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@mkorbi
mkorbi merged commit 2b4e92d into feat/redis-driver Jul 28, 2026
3 checks passed
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