You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Datadog: dialcache.miss.count with the same bounded reason tag
Do not add a parallel reason counter or compatibility counter. A miss is one event; its cause is a dimension of that event. Existing total-miss queries can aggregate without reason, while reason-aware queries can separate ordinary absence from invalidation churn.
This is a follow-up to #141 and #143. It is also the canonical reconciliation spec for the older, now-conflicting attempt in #130; that branch predates the current client-clock protocol and must not be merged or rebased mechanically.
Problem
dialcache_miss_counter and dialcache.miss.count currently identify only cache namespace, use case, key type, and layer. Operators cannot distinguish cold, expired, or evicted entries from tracked Redis frames rejected by an invalidation watermark.
The typed result introduced by PR 143 does not itself provide that attribution. RedisWatermarkMiss means that a tracked read observed a trustworthy refill fence; it can represent an absent value, a short or unsupported frame, or a frame actually rejected by the watermark. Likewise, shadow fill_fenced describes a post-fallback refill decision, not why the original read missed. Miss cause and refill-fence capability are independent.
Confirmed design
Keep one miss instrument
Extend the backend-neutral miss callback rather than adding another callback or metric:
Keep CacheMetricLabels unchanged so reason does not leak into request, latency, size, compression, or other metric callbacks. Export CacheMissReason and MissMetricLabels from the package root.
Every existing miss emission must receive exactly one reason:
Reason
Meaning
value_absent
The layer had no retrievable value. This covers request-local/process-local absence and Redis nil, including never-populated, expired, evicted, and tracked-MGET wrong-type-as-null states. DialCache must not claim to distinguish those cases without additional evidence.
watermark_fenced
A present tracked frame with a supported version and complete header was rejected because createdAtMs <= observedWatermarkMs for a present, valid numeric watermark. This is the decisive read condition; it does not claim that the application serializer would have accepted the payload.
unclassified
The miss is real but its cause is not attributable to either category above. This includes legacy/custom-adapter null, short or unsupported frames, malformed metadata, invalid or future timestamps, and caller-side deserialization failure.
For bundled Redis reads, classification precedence is value_absent when the native value reply is nil, then watermark_fenced when the frame-versus-watermark comparison is decisive, then unclassified. Preserve existing error-versus-miss behavior; observability must not convert protocol errors into misses.
Request-local and process-local misses use value_absent. Caller-serving Redis uses layer="remote"; detached initial and confirmation reads use layer="remote_shadow". Read failures and timeouts remain bounded errors, not misses.
Carry cause independently from a write fence
The shared Redis decoder/result must preserve two orthogonal facts:
the bounded reason the read missed; and
whether the same authoritative snapshot supplied a valid observedWatermarkMs that can fence a refill.
An absent value can still carry a valid observed watermark and later produce fill_fenced. A watermark-fenced old frame can later admit a refill once the candidate client timestamp advances. Do not infer one dimension from the other.
Bundled node-redis and GLIDE adapters already have the required native GET/MGET evidence and must share one decoder path. Add no Redis command, round trip, key, frame field, watermark field, Lua path, or server-side classification. Legacy custom adapters returning null remain correct and emit unclassified; adapters can opt into precise reasons through the public decoder/result contract.
Accept the metric-schema migration
This intentionally changes the existing miss metric schema:
Prometheus keeps the same metric name but changes from four labels to five. An old four-label collector registered under the same prefix in the same Registry is incompatible and must continue to fail exact-schema validation before partial registration. Old and new DialCache versions cannot share one in-process registry/prefix.
During a mixed fleet rollout, old scraped series have no reason label and new series do. Miss/request ratios must aggregate miss series without reason before vector matching; reason dashboards should group explicitly by reason.
Datadog keeps the same metric name and adds one bounded tag. The three reasons increase steady-state custom-metric combinations by at most 3x per existing miss-label tuple; document the billing and mixed-tag-set implications.
Custom metric adapters that accept the broader CacheMetricLabels can ignore the new property, but direct callers, exact type assertions, exhaustive label snapshots, or adapters that reject/forward unknown fields may require migration.
The implementation PR must include a BREAKING CHANGE: footer and migration notes. Under the current pre-1.0 release policy this requires a minor release.
Do not retain or add a second miss counter for compatibility. The desired steady-state API is one miss event with one bounded reason.
If draft stale-on-error work in feat: add stale-on-error Redis recovery #121 lands first, its logically stale retained-frame miss must receive an honest bounded reason; unclassified is the safe default unless a separately justified reason is added.
CacheMissReason is the exact closed union value_absent | watermark_fenced | unclassified.
MissMetricLabels extends CacheMetricLabels with required reason; CacheMetricLabels itself is unchanged.
DialCacheMetricsAdapter.miss() receives MissMetricLabels, and the new types are exported from the package root.
Every existing request-local, process-local, remote, and remote_shadow miss emits exactly one reason and one total miss event; no read path double-counts.
watermark_fenced is emitted only from actual tracked frame-versus-watermark rejection, never merely because a miss carries observedWatermarkMs or a later refill is skipped.
A Redis nil value emits value_absent whether its paired watermark is absent or valid; a valid observed watermark remains available independently for PR 143's conditional refill decision.
Legacy/custom Redis clients returning null preserve their existing fail-open/refill behavior and emit unclassified, not cache_read and not fabricated absence.
node-redis and GLIDE derive reasons through the same semantic decoder and add no Redis operations.
Prometheus retains dialcache_miss_counter, adds required reason, rejects a pre-registered old-schema collector transactionally, and reuses a compatible new-schema collector.
Datadog retains dialcache.miss.count and adds the exact bounded reason tag.
Existing total-miss queries are documented as aggregations without reason; mixed-version Prometheus vector matching and Datadog cardinality migration are documented.
Tests cover all three reasons, every metric layer, caller/shadow parity, reason/write-fence orthogonality, throwing/rejecting custom metric adapters, exhaustive bounded-value guards, and packed ESM/CJS/TypeScript consumers.
Real Redis/Valkey coverage distinguishes an absent value from a present watermark-fenced frame across node-redis and GLIDE without adding commands.
The implementation PR includes an explicit breaking-change release note and updates both first-party metric tables and custom-adapter guidance.
Out of scope
Distinguishing never-created, expired, evicted, deleted, and tracked-MGET wrong-type values; Redis returns the same nil evidence for these cases.
Emitting timestamps, watermarks, cache keys, IDs, raw errors, or other unbounded values as metric labels.
Adding miss events for paths currently represented only as read errors/timeouts.
Changing cache serving, refill, invalidation, stale-recovery, or shadow-verdict behavior.
Adding a parallel reason metric or a compatibility counter.
Summary
Add one required, bounded
reasondimension directly to DialCache's existing cache-miss event and first-party miss metrics:dialcache_miss_counter{cache_namespace,use_case,key_type,layer,reason}dialcache.miss.countwith the same boundedreasontagDo not add a parallel reason counter or compatibility counter. A miss is one event; its cause is a dimension of that event. Existing total-miss queries can aggregate without
reason, while reason-aware queries can separate ordinary absence from invalidation churn.This is a follow-up to #141 and #143. It is also the canonical reconciliation spec for the older, now-conflicting attempt in #130; that branch predates the current client-clock protocol and must not be merged or rebased mechanically.
Problem
dialcache_miss_counteranddialcache.miss.countcurrently identify only cache namespace, use case, key type, and layer. Operators cannot distinguish cold, expired, or evicted entries from tracked Redis frames rejected by an invalidation watermark.The typed result introduced by PR 143 does not itself provide that attribution.
RedisWatermarkMissmeans that a tracked read observed a trustworthy refill fence; it can represent an absent value, a short or unsupported frame, or a frame actually rejected by the watermark. Likewise, shadowfill_fenceddescribes a post-fallback refill decision, not why the original read missed. Miss cause and refill-fence capability are independent.Confirmed design
Keep one miss instrument
Extend the backend-neutral miss callback rather than adding another callback or metric:
Keep
CacheMetricLabelsunchanged soreasondoes not leak into request, latency, size, compression, or other metric callbacks. ExportCacheMissReasonandMissMetricLabelsfrom the package root.Every existing miss emission must receive exactly one reason:
value_absentnil, including never-populated, expired, evicted, and tracked-MGETwrong-type-as-null states. DialCache must not claim to distinguish those cases without additional evidence.watermark_fencedcreatedAtMs <= observedWatermarkMsfor a present, valid numeric watermark. This is the decisive read condition; it does not claim that the application serializer would have accepted the payload.unclassifiednull, short or unsupported frames, malformed metadata, invalid or future timestamps, and caller-side deserialization failure.For bundled Redis reads, classification precedence is
value_absentwhen the native value reply isnil, thenwatermark_fencedwhen the frame-versus-watermark comparison is decisive, thenunclassified. Preserve existing error-versus-miss behavior; observability must not convert protocol errors into misses.Request-local and process-local misses use
value_absent. Caller-serving Redis useslayer="remote"; detached initial and confirmation reads uselayer="remote_shadow". Read failures and timeouts remain bounded errors, not misses.Carry cause independently from a write fence
The shared Redis decoder/result must preserve two orthogonal facts:
observedWatermarkMsthat can fence a refill.An absent value can still carry a valid observed watermark and later produce
fill_fenced. A watermark-fenced old frame can later admit a refill once the candidate client timestamp advances. Do not infer one dimension from the other.Bundled node-redis and GLIDE adapters already have the required native
GET/MGETevidence and must share one decoder path. Add no Redis command, round trip, key, frame field, watermark field, Lua path, or server-side classification. Legacy custom adapters returningnullremain correct and emitunclassified; adapters can opt into precise reasons through the public decoder/result contract.Accept the metric-schema migration
This intentionally changes the existing miss metric schema:
Registryis incompatible and must continue to fail exact-schema validation before partial registration. Old and new DialCache versions cannot share one in-process registry/prefix.reasonlabel and new series do. Miss/request ratios must aggregate miss series withoutreasonbefore vector matching; reason dashboards should group explicitly byreason.CacheMetricLabelscan ignore the new property, but direct callers, exact type assertions, exhaustive label snapshots, or adapters that reject/forward unknown fields may require migration.BREAKING CHANGE:footer and migration notes. Under the current pre-1.0 release policy this requires a minor release.Do not retain or add a second miss counter for compatibility. The desired steady-state API is one miss event with one bounded reason.
Coordination
unclassifiedis the safe default unless a separately justified reason is added.Acceptance criteria
CacheMissReasonis the exact closed unionvalue_absent | watermark_fenced | unclassified.MissMetricLabels extends CacheMetricLabelswith requiredreason;CacheMetricLabelsitself is unchanged.DialCacheMetricsAdapter.miss()receivesMissMetricLabels, and the new types are exported from the package root.remote_shadowmiss emits exactly one reason and one total miss event; no read path double-counts.watermark_fencedis emitted only from actual tracked frame-versus-watermark rejection, never merely because a miss carriesobservedWatermarkMsor a later refill is skipped.nilvalue emitsvalue_absentwhether its paired watermark is absent or valid; a valid observed watermark remains available independently for PR 143's conditional refill decision.nullpreserve their existing fail-open/refill behavior and emitunclassified, notcache_readand not fabricated absence.dialcache_miss_counter, adds requiredreason, rejects a pre-registered old-schema collector transactionally, and reuses a compatible new-schema collector.dialcache.miss.countand adds the exact boundedreasontag.reason; mixed-version Prometheus vector matching and Datadog cardinality migration are documented.Out of scope
MGETwrong-type values; Redis returns the samenilevidence for these cases.