Skip to content

KV per-key TTL (parity with nats.go #1864)#1000

Merged
AdamPayzant merged 1 commit into
nats-io:mainfrom
NormB:feature/kv-per-key-ttl
Jun 30, 2026
Merged

KV per-key TTL (parity with nats.go #1864)#1000
AdamPayzant merged 1 commit into
nats-io:mainfrom
NormB:feature/kv-per-key-ttl

Conversation

@NormB

@NormB NormB commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Bring nats.c to parity with nats.go #1864 ("KeyValue per key TTL support and limit markers") by adding a KV-level per-key TTL write surface on top of the existing per-message TTL primitives (#863):

  • kvConfig.LimitMarkerTTL: at bucket create, enable AllowMsgTTL + SubjectDeleteMarkerTTL on the backing stream.
  • kvStore_CreateWithTTL / kvStore_CreateStringWithTTL: per-key TTL on create, reusing the marker-aware re-create retry (a re-create over a DEL/PURGE/MaxAge tombstone succeeds and keeps the TTL).
  • PurgeTTL: kvPurgeOptions.TTL threads a per-message TTL onto the purge marker so the purge tombstone auto-expires.
  • kvStore_Create / kvStore_Update now delegate to internal _kvCreateWithTTL / _kvUpdateWithTTL with ttl=0, so existing behavior is unchanged.
  • test_KeyValueCreateWithTTL (test/test.c + test/list_test.txt): create-with-TTL, get-before-expiry, create-over-live-key-fails, server-side expiry, re-create over the expired marker, and Purge+PurgeTTL. JS_SETUP(2,11,0) skips it on < 2.11; verified green on nats-server 2.12.11 & 2.14.2.

Bring nats.c to parity with nats.go #1864 ("KeyValue per key TTL support and
limit markers") by adding a KV-level per-key TTL write surface on top of the
existing per-message TTL primitives (nats-io#863):

- kvConfig.LimitMarkerTTL: at bucket create, enable AllowMsgTTL +
  SubjectDeleteMarkerTTL on the backing stream.
- kvStore_CreateWithTTL / kvStore_CreateStringWithTTL: per-key TTL on create,
  reusing the marker-aware re-create retry (a re-create over a DEL/PURGE/MaxAge
  tombstone succeeds and keeps the TTL).
- kvStore_UpdateWithTTL / kvStore_UpdateStringWithTTL: per-key TTL on a CAS update.
- PurgeTTL: kvPurgeOptions.TTL threads a per-message TTL onto the purge marker
  so the purge tombstone auto-expires.
- kvStore_Create / kvStore_Update delegate to the *WithTTL entry points with
  ttl=0, so existing behavior is unchanged.
- test_KeyValueCreateWithTTL (test/test.c + test/list_test.txt): create-with-TTL,
  get-before-expiry, create-over-live-key-fails, server-side expiry, re-create
  over the expired marker, update-with-TTL (binary + string), and Purge+PurgeTTL.
  JS_SETUP(2,11,0) skips it on < 2.11; verified green on nats-server 2.11.10,
  2.12.11 & 2.14.2.

@AdamPayzant AdamPayzant left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Hi Norm. Thank you for the contribution! I've added some notes

Comment thread test/list_test.txt Outdated
Comment thread src/kv.c Outdated
Comment thread src/kv.c Outdated
Comment thread src/kv.c Outdated
@NormB

NormB commented Jun 29, 2026

Copy link
Copy Markdown
Contributor Author

@AdamPayzant Thanks for the quick review. Will take care of the first three comments. For the last one, will make it public.

Added kvStore_UpdateStringWithTTL to mirror the CreateString* pair

A couple of things I ran into while expanding the tests:

  • TTL granularity — I left this server-enforced rather than validating client-side: the server requires ≥ 1s (JSMessageTTLInvalidErr on a sub-second value), but a non-whole-second value like 1500ms is accepted. Documented on both functions, with tests for both cases. Glad to add a client-side check instead if you'd prefer.

  • On the create path a rejected TTL comes back with an empty error string — the marker-aware re-create runs a _getEntry that clears the last error; the update path preserves the server message. I assert the server text (invalid per-message TTL) on the update path. I can look at preserving it through the create retry as a follow-up if it's worth it.

@NormB
NormB force-pushed the feature/kv-per-key-ttl branch from 3c33759 to baf603c Compare June 30, 2026 01:24

@AdamPayzant AdamPayzant left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM

@AdamPayzant
AdamPayzant merged commit 04f1291 into nats-io:main Jun 30, 2026
35 of 36 checks passed
github-actions Bot pushed a commit that referenced this pull request Jun 30, 2026
@NormB

NormB commented Jun 30, 2026

Copy link
Copy Markdown
Contributor Author

@AdamPayzant Any thoughts on the two items I mentioned above? TTL granularity and Propagating errors to the caller?

@AdamPayzant

Copy link
Copy Markdown
Collaborator

I thought it was fine without, personally, but it would be a pretty quick PR if you want to add them

@NormB
NormB deleted the feature/kv-per-key-ttl branch June 30, 2026 13:58
NormB added a commit to NormB/nats.c that referenced this pull request Jun 30, 2026
Follow-ups to the KV per-key TTL work (PR nats-io#1000):

- Validate the per-key TTL client-side at the kvStore_*WithTTL chokepoint:
  reject 0 < ttl < 1000ms with NATS_INVALID_ARG (kvErrInvalidTTL) before any
  publish, mirroring the server's 1s minimum (JSMessageTTLInvalidErr). This
  covers create, update, their String variants, and the purge marker TTL
  (kvPurgeOptions.TTL).

- Preserve the create-path error through the marker-aware retry: short-circuit
  a client-side NATS_INVALID_ARG, and snapshot/restore the original failure
  around the _getEntry tombstone re-check (which overwrites the thread-local
  last error). A rejected create no longer returns an empty error string -- the
  server message (e.g. "invalid per-message TTL", "per-message TTL is disabled")
  now reaches the caller.

- Tests: sub-second create/update with preserved messages, boundary cases
  (999ms/1ms reject, 1000ms/1500ms accept), sub-second PurgeTTL, and a
  TTL-disabled-bucket create that exercises the snapshot/restore branch.

- Docs: public-API docs note the client-side 1s validation and that the server
  stores TTL with whole-second granularity (fractional values are truncated, so
  1500ms is applied as 1s).
NormB added a commit to NormB/nats.c that referenced this pull request Jun 30, 2026
Follow-ups to the KV per-key TTL work (PR nats-io#1000):

- Validate the per-key TTL client-side at the kvStore_*WithTTL chokepoint:
  reject 0 < ttl < 1000ms with NATS_INVALID_ARG (kvErrInvalidTTL) before any
  publish, mirroring the server's 1s minimum (JSMessageTTLInvalidErr). This
  covers create, update, their String variants, and the purge marker TTL
  (kvPurgeOptions.TTL).

- Preserve the create-path error through the marker-aware retry: short-circuit
  a client-side NATS_INVALID_ARG, and snapshot/restore the original failure
  around the _getEntry tombstone re-check (which overwrites the thread-local
  last error). A rejected create no longer returns an empty error string -- the
  server message (e.g. "invalid per-message TTL", "per-message TTL is disabled")
  now reaches the caller.

- Tests: sub-second create/update with preserved messages, boundary cases
  (999ms/1ms reject, 1000ms/1500ms accept), sub-second PurgeTTL, and a
  TTL-disabled-bucket create that exercises the snapshot/restore branch.

- Docs: public-API docs note the client-side 1s validation and that the server
  stores TTL with whole-second granularity (fractional values are truncated, so
  1500ms is applied as 1s).
NormB added a commit to NormB/nats.c that referenced this pull request Jul 1, 2026
…re retry

Follow-up to the KV per-key TTL work (PR nats-io#1000). Per review, TTL granularity
is left entirely to the server (no client-side check), so a future server-side
change to the minimum needs no client update.

- kvStore_CreateWithTTL: the marker-aware retry calls _getEntry, which
  overwrites the thread-local last error, so a failed create returned an empty
  error string. This is pre-existing behavior inherited from kvStore_Create.
  Suppress error-stack updates across the _getEntry probe (nats_doNotUpdateErrStack)
  and restore the original failure on return; re-enable and clear only when we
  actually re-create over a tombstone, so that path reports its own error. The
  server message (e.g. "invalid per-message TTL", "per-message TTL is disabled")
  now reaches the caller.

- Remove the client-side TTL granularity check (kvStore_*WithTTL and the purge
  marker TTL) and kvErrInvalidTTL; the server enforces the 1s minimum.

- Tests: sub-second create/update now assert the server's error survives the
  retry, plus a TTL-disabled-bucket create that exercises the same path.

- Docs: note the server's 1s minimum without asserting a client-side rejection.
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.

2 participants