Skip to content

feat(kv): add DiscardPolicy knob (default New, opt-in Old) — v0.7.2#12

Merged
jaredLunde merged 1 commit into
mainfrom
feat/discard-policy
Jun 16, 2026
Merged

feat(kv): add DiscardPolicy knob (default New, opt-in Old) — v0.7.2#12
jaredLunde merged 1 commit into
mainfrom
feat/discard-policy

Conversation

@jaredLunde

Copy link
Copy Markdown
Contributor

What

Adds a DiscardPolicy knob to StoreConfig so a bucket can choose what it does at max_bytes:

  • New (default, unchanged) — reject writes when full (discard:new). Right for config buckets (certs/configs read as the source of truth) where silently dropping a live value is unacceptable. But it freezes writers at capacity (err 10077), so pair with max_age.
  • Old (opt-in) — evict the oldest when full (discard:old): a hard size ceiling that never rejects. Right for high-churn log buckets whose consumers hold the durable fold (routing origins) — the bucket is a bounded change-feed, not the source of truth, so an evicted entry is recovered from the fold + CursorExpired resync.

Why

The routing-origins buckets (edge-origins/tunnel-origins) are high-churn logs: every deploy/evacuation writes a new route key and removes the old, so distinct keys (and their delete markers) grow monotonically. With the hardcoded discard:new and no trim, they grow to max_bytes and then reject all writes → traffic-routing writes freeze fleet-wide. max_age is the wrong lever (bucket size becomes a function of churn rate). The right fix is a size-bounded, evict-oldest log — which discard:old provides, and which is safe precisely because slipstream consumers keep the durable fold.

How

  • StoreConfig.discard: DiscardPolicy, default New — every ..Default::default() caller is unchanged.
  • async-nats' kv::Config has no discard field, so the normal create path can only produce discard:new. For Old, store_with_config skips it and uses the raw stream API (the only place the policy is expressible).
  • Integration test (discard_policy_old_evicts_default_rejects) asserts a created Old bucket reads back discard:old and the default stays discard:new. All 45 integration tests pass against a real nats-server; clippy clean.

Versioning

0.7.1 -> 0.7.2. Additive (new field with Default, new enum, new export) — stays within consumers' ^0.7 requirement so they pick it up automatically.

🤖 Generated with Claude Code

…kets

NATS KV buckets are created `discard:new` (hardcoded) — reject writes when full.
That's correct for config buckets (certs/configs read as source of truth), where
silently dropping a live value is unacceptable. But it FREEZES writers at capacity
(err 10077), which is wrong for high-churn LOG buckets (e.g. routing origins) whose
consumers hold the durable fold: there the bucket is a bounded change-feed, not the
source of truth, so it should bound by SIZE and evict the oldest, never reject.

- StoreConfig gains `discard: DiscardPolicy` (New | Old), default New (back-compat:
  every `..Default::default()` caller is unchanged).
- async-nats' `kv::Config` exposes no discard field, so the normal create path can
  only produce `discard:new`. For `discard:old`, `store_with_config` skips it and
  uses the raw stream API (the only place the policy is expressible).
- New integration test asserts a created `Old` bucket reads back `discard:old` and
  the default stays `discard:new`.

Bumps 0.7.1 -> 0.7.2 (additive; stays within consumers' ^0.7 requirement).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@jaredLunde jaredLunde merged commit ab021f4 into main Jun 16, 2026
1 check passed
@jaredLunde jaredLunde deleted the feat/discard-policy branch June 16, 2026 04:42
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