-
Notifications
You must be signed in to change notification settings - Fork 37
Description
Bug
KvBucket.create(k, data, markerTTL) silently drops the per-message TTL (Nats-Msg-TTL header) when the key has an existing DEL or PURGE marker.
Root Cause
create() in kv/src/kv.ts has two code paths:
-
Happy path (line 592):
this._put(k, data, { previousSeq: 0 }, markerTTL)— correctly forwardsmarkerTTL, so theNats-Msg-TTLheader is set on the published message. -
Fallback path (line 611): When
previousSeq: 0fails because a DEL/PURGE marker exists, it callsthis.update(k, data, rev)—markerTTLis not forwarded.update()callsput()which calls_put()without themarkerTTLargument, so no TTL header is set.
Lines 606 to 617 in d273721
| let rev = 0; | |
| try { | |
| const e = await this.get(k); | |
| if (e?.operation === "DEL" || e?.operation === "PURGE") { | |
| rev = e !== null ? e.revision : 0; | |
| return this.update(k, data, rev); | |
| } else { | |
| return Promise.reject(firstErr); | |
| } | |
| } catch (err) { | |
| return Promise.reject(err); | |
| } |
Impact
Any caller using kv.create(key, data, ttl) for short-lived cache entries will lose the per-message TTL the second time a key is written (after the first entry expires and leaves a delete marker). The entry then persists until the bucket-level max_age instead of the intended TTL.
We observed this in production: cache entries written with a "14s" TTL were persisting for 7 days (the bucket max_age). Inspecting the stored message headers confirmed only Nats-Expected-Last-Subject-Sequence was present — no Nats-Msg-TTL.
Suggested Fix
Replace line 611:
return this.update(k, data, rev);with:
return this._put(k, data, { previousSeq: rev }, markerTTL);This calls _put() directly (same as the happy path) with previousSeq set to the revision of the existing delete/purge marker, and forwards markerTTL so the TTL header is set.
Reproduction
- Create a KV bucket with
markerTTLenabled (to allow per-message TTL) kv.create("key", data, "10s")— succeeds, message hasNats-Msg-TTL: 10sheader ✅- Wait for TTL to expire (key is deleted, delete marker briefly exists)
kv.create("key", data, "10s")during the delete marker window — succeeds, but message has noNats-Msg-TTLheader ❌- Entry persists until bucket
max_ageinstead of 10s
Environment
@nats-io/kv3.3.0 and 3.3.1 (also present onmain)- nats-server 2.11+