feat: add sampled Redis shadow validation - #105

Merged
lan17 merged 5 commits into
mainfrom
agent/shadow-validation
Jul 29, 2026
Merged

feat: add sampled Redis shadow validation#105
lan17 merged 5 commits into
mainfrom
agent/shadow-validation

Conversation

@lan17

@lan17lan17 commented Jul 29, 2026

Copy link
Copy Markdown
Owner

Summary

Adds opt-in semantic shadow validation for sensitive, invalidation-tracked Redis cache hits. A selected hit returns its cached value normally, then detached best-effort work re-reads the source of truth (SoT), independently deserializes the retained Redis payload, and compares the two application values.

Closes#104.

Architecture

flowchart LR
A[Tracked Redis hit] --> B[Return cached value]
A --> C{shadowRamp selects key?}
C -- no --> X[No shadow work]
C -- yes --> D{Metric hook and capacity?}
D -- no --> X
D -- yes --> E[Reserve exact-key flight]
E --> F[Unref setImmediate]
F --> G[Read SoT with DialCache disabled]
G --> H[serializer.load retained payload]
H --> I[Default or custom value comparator]
I --> J[Bounded outcome metric]
Loading

The hit path performs eligibility checks and slot reservation only. It does no SoT read, extra deserialization, deep comparison, payload-size-linear copy, repair, or cache mutation. All comparison work begins from an unreferenced immediate and is not awaited by the caller.

Public API and configuration

  • Adds DialCacheKeyConfig.shadowRamp?: number.
    • Omitted or 0 disables validation.
    • 100 selects every otherwise eligible exact key.
    • Partial ramps use a stable exact-key cohort with an independent :shadow discriminator.
    • Sparse runtime overlays inherit the static baseline; malformed values skip shadow work and emit the existing remote config_resolution error without changing the hit.
  • Adds DialCacheConfig.shadowMaxInFlight?: number, a positive safe integer defaulting to 1 per instance.
    • There is no queue.
    • Exact-key duplicates and global-cap overflow emit dropped.
    • Scheduled and timed-out-but-still-running work retains its slot until the underlying promise settles.
  • Adds the root-exported synchronous type ShadowComparator<T> and optional shadowComparator on cached() / getOrLoad() options.
  • Extends DialCacheMetricsAdapter with an optional shadowValidation hook, preserving existing custom adapters.
exporttypeShadowComparator<T>=(cachedValue: T,sourceValue: T,)=>boolean;

Comparison and data boundaries

  • Eligibility requires an actual successful Redis hit with trackForInvalidation: true; request-local/process-local hits, misses, read errors, and initial deserialize failures do not validate.
  • Redis reads retain the client-returned semantic string | Buffer payload internally alongside the decoded value.
  • Detached work reads the raw SoT value, then runs the same effective serializer's load() again on the retained payload to create an independent cached snapshot.
  • The object already returned to the caller is never compared, so caller mutation after the hit cannot contaminate validation.
  • The comparator receives two values of T, never a Redis payload.
  • The default is Node's util.isDeepStrictEqual; an optional per-operation comparator defines use-case-specific equality.
  • SoT is intentionally not dump/load normalized. Lossy serialization remains visible unless a custom comparator explicitly treats the normalized values as equivalent.
  • A comparator must synchronously return a boolean and must be deterministic, side-effect-free, non-mutating, and bounded. Throws and non-boolean returns emit comparison_error.
  • An accidental Promise from untyped JavaScript is consumed safely but never accepted as a comparison result; it retains the flight slot until settlement.
  • Redis framing, timestamps, TTLs, watermarks, Lua, keys, and bundled client protocol remain unchanged.

Detachment, liveness, and safety

  • Source execution starts from an unreferenced setImmediate and runs under dialcache.disable(...) so it cannot recursively satisfy itself from the same cache.
  • One monotonic, unreferenced deadline covers the source read, detached serializer load, and comparison.
  • A finite fallbackTimeoutMs is reused; fallbackTimeoutMs: null keeps normal fallbacks unbounded but gives shadow work the internal 60-second default.
  • On timeout, DialCache releases its retained payload reference and suppresses later phases. Already-running application/serializer/comparator work cannot be cancelled and continues to occupy its slot until settlement.
  • Scheduler and deadline handles do not keep an otherwise idle process alive. Completion during shutdown remains best effort.
  • Detached execution retains original cached() argument references or a getOrLoad() closure; docs require relevant inputs/captures to be immutable or snapshotted.
  • Validation is observational only: no repair, write, TTL refresh, invalidation, local eviction, payload logging, or change to the returned value.

Observability

Bounded outcomes: match, mismatch, source_error, deserialization_error, comparison_error, timeout, and dropped.

Labels/tags are limited to cache namespace, use case, key type, and outcome. IDs, cached/SoT values, payloads, Redis keys, and raw exception messages are excluded.

Built-in adapters add:

  • Prometheus: dialcache_shadow_validation_counter
  • Datadog: dialcache.shadow.count

An adapter without the optional hook disables shadow execution so DialCache never performs an unobservable SoT read. Runtime thenable rejections from the hook are consumed without being awaited.

Compatibility and operations

  • The feature is default-off and all new configuration/operation fields and the metrics hook are optional.
  • Existing cache behavior, Redis wire format, Lua scripts, keys, watermarks, serializers, and package export paths remain unchanged.
  • This is suitable for a minor release and rolling upgrade; enabling it is an operational rollout because it adds sampled SoT reads plus detached serializer/comparison CPU.
  • shadowMaxInFlight bounds one instance, not the fleet. Roll out shadowRamp gradually and monitor match, mismatch, timeout, source_error, comparison_error, and dropped.
  • The Prometheus adapter registers one additive collector family; applications that already own the exact same prefixed name with an incompatible schema must resolve that registry collision.
  • README documents serializer repeatability, custom Redis payload ownership, detached input lifetime, concurrency budgeting, shutdown behavior, and privacy boundaries.

Change footprint

The PR is test-led rather than carrying duplicate first-pass production code:

  • 22 files, +1,992 / -38 against current main
  • Tests and package-consumer proofs: +1,364 / -2 across 7 files (68% of additions)
  • Production source: +453 / -25 across 13 files (23% of additions)
  • Benchmark tooling: +92
  • README: +83 / -11

The dedicated shadow suite contains 36 scenarios covering the detached lifecycle and failure matrix. Shared setup is factored through test-only helpers; no obsolete serialized-payload comparison implementation remains.

Validation

Run on Node.js 22.22.0 at 4b8f7e4, including current main@500d5e7:

  • corepack pnpm check
    • 341 unit tests passed
    • coverage: 97.04% statements, 93.65% branches, 98.47% functions, 97.16% lines
    • typecheck, build, and packed-consumer test passed
  • corepack pnpm test:integration
    • 81 tests passed across Redis 6.2 and Valkey 8 with node-redis and GLIDE
    • includes real tracked-value semantic match/mismatch coverage with no repair
  • DIALCACHE_BENCH_ITERATIONS=20000 DIALCACHE_BENCH_FANOUT=10000 corepack pnpm benchmark:request-local
    • all eight semantic scenarios passed
    • includes tracked Redis hits with shadow omitted and deterministically ramped out
  • corepack pnpm audit --prod: no known production-dependency vulnerabilities
  • GitHub Actions: CI, CodeQL, and PR-title validation passed on 4b8f7e4
  • git diff --check

Focused shadow tests cover detachment, semantic default/custom equality, caller-mutation isolation, serializer ownership/repeatability, runtime ramp overlays, comparator type/runtime failures, monotonic deadlines, timeout slot retention, exact-key/global drops, coalescing, disabled contexts, metric failure isolation, and unreferenced handles.

Review history

  • A broad production review covered concurrency/lifecycle, API compatibility, rollout/configuration, observability, tests, and operations.
  • The cleanup round fixed low-severity coverage gaps for packed public APIs, runtime ramp enable/disable overlays, synchronous comparator deadline crossing, custom Buffer ownership, and honest benchmark reporting.
  • Two fresh post-fix reviewers independently confirmed the feature and its integration with current main with no remaining findings.

@lan17
lan17 marked this pull request as ready for review July 29, 2026 16:55
@lan17
lan17 merged commit 9f771e4 into mainJul 29, 2026
6 checks passed
@lan17
lan17 deleted the agent/shadow-validation branch July 29, 2026 16:58
Sign up for freeto 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.

Add sampled shadow validation for tracked Redis cache hits

1 participant

@lan17
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

feat: add sampled Redis shadow validation - #105

Merged
lan17 merged 5 commits into
mainfrom
agent/shadow-validation
Jul 29, 2026
Merged

feat: add sampled Redis shadow validation#105
lan17 merged 5 commits into
mainfrom
agent/shadow-validation

Conversation

@lan17

@lan17lan17 commented Jul 29, 2026

Copy link
Copy Markdown
Owner

Summary

Adds opt-in semantic shadow validation for sensitive, invalidation-tracked Redis cache hits. A selected hit returns its cached value normally, then detached best-effort work re-reads the source of truth (SoT), independently deserializes the retained Redis payload, and compares the two application values.

Closes#104.

Architecture

flowchart LR
A[Tracked Redis hit] --> B[Return cached value]
A --> C{shadowRamp selects key?}
C -- no --> X[No shadow work]
C -- yes --> D{Metric hook and capacity?}
D -- no --> X
D -- yes --> E[Reserve exact-key flight]
E --> F[Unref setImmediate]
F --> G[Read SoT with DialCache disabled]
G --> H[serializer.load retained payload]
H --> I[Default or custom value comparator]
I --> J[Bounded outcome metric]
Loading

The hit path performs eligibility checks and slot reservation only. It does no SoT read, extra deserialization, deep comparison, payload-size-linear copy, repair, or cache mutation. All comparison work begins from an unreferenced immediate and is not awaited by the caller.

Public API and configuration

  • Adds DialCacheKeyConfig.shadowRamp?: number.
    • Omitted or 0 disables validation.
    • 100 selects every otherwise eligible exact key.
    • Partial ramps use a stable exact-key cohort with an independent :shadow discriminator.
    • Sparse runtime overlays inherit the static baseline; malformed values skip shadow work and emit the existing remote config_resolution error without changing the hit.
  • Adds DialCacheConfig.shadowMaxInFlight?: number, a positive safe integer defaulting to 1 per instance.
    • There is no queue.
    • Exact-key duplicates and global-cap overflow emit dropped.
    • Scheduled and timed-out-but-still-running work retains its slot until the underlying promise settles.
  • Adds the root-exported synchronous type ShadowComparator<T> and optional shadowComparator on cached() / getOrLoad() options.
  • Extends DialCacheMetricsAdapter with an optional shadowValidation hook, preserving existing custom adapters.
exporttypeShadowComparator<T>=(cachedValue: T,sourceValue: T,)=>boolean;

Comparison and data boundaries

  • Eligibility requires an actual successful Redis hit with trackForInvalidation: true; request-local/process-local hits, misses, read errors, and initial deserialize failures do not validate.
  • Redis reads retain the client-returned semantic string | Buffer payload internally alongside the decoded value.
  • Detached work reads the raw SoT value, then runs the same effective serializer's load() again on the retained payload to create an independent cached snapshot.
  • The object already returned to the caller is never compared, so caller mutation after the hit cannot contaminate validation.
  • The comparator receives two values of T, never a Redis payload.
  • The default is Node's util.isDeepStrictEqual; an optional per-operation comparator defines use-case-specific equality.
  • SoT is intentionally not dump/load normalized. Lossy serialization remains visible unless a custom comparator explicitly treats the normalized values as equivalent.
  • A comparator must synchronously return a boolean and must be deterministic, side-effect-free, non-mutating, and bounded. Throws and non-boolean returns emit comparison_error.
  • An accidental Promise from untyped JavaScript is consumed safely but never accepted as a comparison result; it retains the flight slot until settlement.
  • Redis framing, timestamps, TTLs, watermarks, Lua, keys, and bundled client protocol remain unchanged.

Detachment, liveness, and safety

  • Source execution starts from an unreferenced setImmediate and runs under dialcache.disable(...) so it cannot recursively satisfy itself from the same cache.
  • One monotonic, unreferenced deadline covers the source read, detached serializer load, and comparison.
  • A finite fallbackTimeoutMs is reused; fallbackTimeoutMs: null keeps normal fallbacks unbounded but gives shadow work the internal 60-second default.
  • On timeout, DialCache releases its retained payload reference and suppresses later phases. Already-running application/serializer/comparator work cannot be cancelled and continues to occupy its slot until settlement.
  • Scheduler and deadline handles do not keep an otherwise idle process alive. Completion during shutdown remains best effort.
  • Detached execution retains original cached() argument references or a getOrLoad() closure; docs require relevant inputs/captures to be immutable or snapshotted.
  • Validation is observational only: no repair, write, TTL refresh, invalidation, local eviction, payload logging, or change to the returned value.

Observability

Bounded outcomes: match, mismatch, source_error, deserialization_error, comparison_error, timeout, and dropped.

Labels/tags are limited to cache namespace, use case, key type, and outcome. IDs, cached/SoT values, payloads, Redis keys, and raw exception messages are excluded.

Built-in adapters add:

  • Prometheus: dialcache_shadow_validation_counter
  • Datadog: dialcache.shadow.count

An adapter without the optional hook disables shadow execution so DialCache never performs an unobservable SoT read. Runtime thenable rejections from the hook are consumed without being awaited.

Compatibility and operations

  • The feature is default-off and all new configuration/operation fields and the metrics hook are optional.
  • Existing cache behavior, Redis wire format, Lua scripts, keys, watermarks, serializers, and package export paths remain unchanged.
  • This is suitable for a minor release and rolling upgrade; enabling it is an operational rollout because it adds sampled SoT reads plus detached serializer/comparison CPU.
  • shadowMaxInFlight bounds one instance, not the fleet. Roll out shadowRamp gradually and monitor match, mismatch, timeout, source_error, comparison_error, and dropped.
  • The Prometheus adapter registers one additive collector family; applications that already own the exact same prefixed name with an incompatible schema must resolve that registry collision.
  • README documents serializer repeatability, custom Redis payload ownership, detached input lifetime, concurrency budgeting, shutdown behavior, and privacy boundaries.

Change footprint

The PR is test-led rather than carrying duplicate first-pass production code:

  • 22 files, +1,992 / -38 against current main
  • Tests and package-consumer proofs: +1,364 / -2 across 7 files (68% of additions)
  • Production source: +453 / -25 across 13 files (23% of additions)
  • Benchmark tooling: +92
  • README: +83 / -11

The dedicated shadow suite contains 36 scenarios covering the detached lifecycle and failure matrix. Shared setup is factored through test-only helpers; no obsolete serialized-payload comparison implementation remains.

Validation

Run on Node.js 22.22.0 at 4b8f7e4, including current main@500d5e7:

  • corepack pnpm check
    • 341 unit tests passed
    • coverage: 97.04% statements, 93.65% branches, 98.47% functions, 97.16% lines
    • typecheck, build, and packed-consumer test passed
  • corepack pnpm test:integration
    • 81 tests passed across Redis 6.2 and Valkey 8 with node-redis and GLIDE
    • includes real tracked-value semantic match/mismatch coverage with no repair
  • DIALCACHE_BENCH_ITERATIONS=20000 DIALCACHE_BENCH_FANOUT=10000 corepack pnpm benchmark:request-local
    • all eight semantic scenarios passed
    • includes tracked Redis hits with shadow omitted and deterministically ramped out
  • corepack pnpm audit --prod: no known production-dependency vulnerabilities
  • GitHub Actions: CI, CodeQL, and PR-title validation passed on 4b8f7e4
  • git diff --check

Focused shadow tests cover detachment, semantic default/custom equality, caller-mutation isolation, serializer ownership/repeatability, runtime ramp overlays, comparator type/runtime failures, monotonic deadlines, timeout slot retention, exact-key/global drops, coalescing, disabled contexts, metric failure isolation, and unreferenced handles.

Review history

  • A broad production review covered concurrency/lifecycle, API compatibility, rollout/configuration, observability, tests, and operations.
  • The cleanup round fixed low-severity coverage gaps for packed public APIs, runtime ramp enable/disable overlays, synchronous comparator deadline crossing, custom Buffer ownership, and honest benchmark reporting.
  • Two fresh post-fix reviewers independently confirmed the feature and its integration with current main with no remaining findings.

@lan17
lan17 marked this pull request as ready for review July 29, 2026 16:55
@lan17
lan17 merged commit 9f771e4 into mainJul 29, 2026
6 checks passed
@lan17
lan17 deleted the agent/shadow-validation branch July 29, 2026 16:58
Sign up for freeto 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.

Add sampled shadow validation for tracked Redis cache hits

1 participant

@lan17
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat: add sampled Redis shadow validation - #105

Merged
lan17 merged 5 commits into
mainfrom
agent/shadow-validation
Jul 29, 2026
Merged

feat: add sampled Redis shadow validation#105
lan17 merged 5 commits into
mainfrom
agent/shadow-validation

Conversation

@lan17

@lan17lan17 commented Jul 29, 2026

Copy link
Copy Markdown
Owner

Summary

Adds opt-in semantic shadow validation for sensitive, invalidation-tracked Redis cache hits. A selected hit returns its cached value normally, then detached best-effort work re-reads the source of truth (SoT), independently deserializes the retained Redis payload, and compares the two application values.

Closes#104.

Architecture

flowchart LR
A[Tracked Redis hit] --> B[Return cached value]
A --> C{shadowRamp selects key?}
C -- no --> X[No shadow work]
C -- yes --> D{Metric hook and capacity?}
D -- no --> X
D -- yes --> E[Reserve exact-key flight]
E --> F[Unref setImmediate]
F --> G[Read SoT with DialCache disabled]
G --> H[serializer.load retained payload]
H --> I[Default or custom value comparator]
I --> J[Bounded outcome metric]
Loading

The hit path performs eligibility checks and slot reservation only. It does no SoT read, extra deserialization, deep comparison, payload-size-linear copy, repair, or cache mutation. All comparison work begins from an unreferenced immediate and is not awaited by the caller.

Public API and configuration

  • Adds DialCacheKeyConfig.shadowRamp?: number.
    • Omitted or 0 disables validation.
    • 100 selects every otherwise eligible exact key.
    • Partial ramps use a stable exact-key cohort with an independent :shadow discriminator.
    • Sparse runtime overlays inherit the static baseline; malformed values skip shadow work and emit the existing remote config_resolution error without changing the hit.
  • Adds DialCacheConfig.shadowMaxInFlight?: number, a positive safe integer defaulting to 1 per instance.
    • There is no queue.
    • Exact-key duplicates and global-cap overflow emit dropped.
    • Scheduled and timed-out-but-still-running work retains its slot until the underlying promise settles.
  • Adds the root-exported synchronous type ShadowComparator<T> and optional shadowComparator on cached() / getOrLoad() options.
  • Extends DialCacheMetricsAdapter with an optional shadowValidation hook, preserving existing custom adapters.
exporttypeShadowComparator<T>=(cachedValue: T,sourceValue: T,)=>boolean;

Comparison and data boundaries

  • Eligibility requires an actual successful Redis hit with trackForInvalidation: true; request-local/process-local hits, misses, read errors, and initial deserialize failures do not validate.
  • Redis reads retain the client-returned semantic string | Buffer payload internally alongside the decoded value.
  • Detached work reads the raw SoT value, then runs the same effective serializer's load() again on the retained payload to create an independent cached snapshot.
  • The object already returned to the caller is never compared, so caller mutation after the hit cannot contaminate validation.
  • The comparator receives two values of T, never a Redis payload.
  • The default is Node's util.isDeepStrictEqual; an optional per-operation comparator defines use-case-specific equality.
  • SoT is intentionally not dump/load normalized. Lossy serialization remains visible unless a custom comparator explicitly treats the normalized values as equivalent.
  • A comparator must synchronously return a boolean and must be deterministic, side-effect-free, non-mutating, and bounded. Throws and non-boolean returns emit comparison_error.
  • An accidental Promise from untyped JavaScript is consumed safely but never accepted as a comparison result; it retains the flight slot until settlement.
  • Redis framing, timestamps, TTLs, watermarks, Lua, keys, and bundled client protocol remain unchanged.

Detachment, liveness, and safety

  • Source execution starts from an unreferenced setImmediate and runs under dialcache.disable(...) so it cannot recursively satisfy itself from the same cache.
  • One monotonic, unreferenced deadline covers the source read, detached serializer load, and comparison.
  • A finite fallbackTimeoutMs is reused; fallbackTimeoutMs: null keeps normal fallbacks unbounded but gives shadow work the internal 60-second default.
  • On timeout, DialCache releases its retained payload reference and suppresses later phases. Already-running application/serializer/comparator work cannot be cancelled and continues to occupy its slot until settlement.
  • Scheduler and deadline handles do not keep an otherwise idle process alive. Completion during shutdown remains best effort.
  • Detached execution retains original cached() argument references or a getOrLoad() closure; docs require relevant inputs/captures to be immutable or snapshotted.
  • Validation is observational only: no repair, write, TTL refresh, invalidation, local eviction, payload logging, or change to the returned value.

Observability

Bounded outcomes: match, mismatch, source_error, deserialization_error, comparison_error, timeout, and dropped.

Labels/tags are limited to cache namespace, use case, key type, and outcome. IDs, cached/SoT values, payloads, Redis keys, and raw exception messages are excluded.

Built-in adapters add:

  • Prometheus: dialcache_shadow_validation_counter
  • Datadog: dialcache.shadow.count

An adapter without the optional hook disables shadow execution so DialCache never performs an unobservable SoT read. Runtime thenable rejections from the hook are consumed without being awaited.

Compatibility and operations

  • The feature is default-off and all new configuration/operation fields and the metrics hook are optional.
  • Existing cache behavior, Redis wire format, Lua scripts, keys, watermarks, serializers, and package export paths remain unchanged.
  • This is suitable for a minor release and rolling upgrade; enabling it is an operational rollout because it adds sampled SoT reads plus detached serializer/comparison CPU.
  • shadowMaxInFlight bounds one instance, not the fleet. Roll out shadowRamp gradually and monitor match, mismatch, timeout, source_error, comparison_error, and dropped.
  • The Prometheus adapter registers one additive collector family; applications that already own the exact same prefixed name with an incompatible schema must resolve that registry collision.
  • README documents serializer repeatability, custom Redis payload ownership, detached input lifetime, concurrency budgeting, shutdown behavior, and privacy boundaries.

Change footprint

The PR is test-led rather than carrying duplicate first-pass production code:

  • 22 files, +1,992 / -38 against current main
  • Tests and package-consumer proofs: +1,364 / -2 across 7 files (68% of additions)
  • Production source: +453 / -25 across 13 files (23% of additions)
  • Benchmark tooling: +92
  • README: +83 / -11

The dedicated shadow suite contains 36 scenarios covering the detached lifecycle and failure matrix. Shared setup is factored through test-only helpers; no obsolete serialized-payload comparison implementation remains.

Validation

Run on Node.js 22.22.0 at 4b8f7e4, including current main@500d5e7:

  • corepack pnpm check
    • 341 unit tests passed
    • coverage: 97.04% statements, 93.65% branches, 98.47% functions, 97.16% lines
    • typecheck, build, and packed-consumer test passed
  • corepack pnpm test:integration
    • 81 tests passed across Redis 6.2 and Valkey 8 with node-redis and GLIDE
    • includes real tracked-value semantic match/mismatch coverage with no repair
  • DIALCACHE_BENCH_ITERATIONS=20000 DIALCACHE_BENCH_FANOUT=10000 corepack pnpm benchmark:request-local
    • all eight semantic scenarios passed
    • includes tracked Redis hits with shadow omitted and deterministically ramped out
  • corepack pnpm audit --prod: no known production-dependency vulnerabilities
  • GitHub Actions: CI, CodeQL, and PR-title validation passed on 4b8f7e4
  • git diff --check

Focused shadow tests cover detachment, semantic default/custom equality, caller-mutation isolation, serializer ownership/repeatability, runtime ramp overlays, comparator type/runtime failures, monotonic deadlines, timeout slot retention, exact-key/global drops, coalescing, disabled contexts, metric failure isolation, and unreferenced handles.

Review history

  • A broad production review covered concurrency/lifecycle, API compatibility, rollout/configuration, observability, tests, and operations.
  • The cleanup round fixed low-severity coverage gaps for packed public APIs, runtime ramp enable/disable overlays, synchronous comparator deadline crossing, custom Buffer ownership, and honest benchmark reporting.
  • Two fresh post-fix reviewers independently confirmed the feature and its integration with current main with no remaining findings.

@lan17
lan17 marked this pull request as ready for review July 29, 2026 16:55
@lan17
lan17 merged commit 9f771e4 into mainJul 29, 2026
6 checks passed
@lan17
lan17 deleted the agent/shadow-validation branch July 29, 2026 16:58
Sign up for freeto 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.

Add sampled shadow validation for tracked Redis cache hits

1 participant

@lan17
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat: add sampled Redis shadow validation - #105

Merged
lan17 merged 5 commits into
mainfrom
agent/shadow-validation
Jul 29, 2026
Merged

feat: add sampled Redis shadow validation#105
lan17 merged 5 commits into
mainfrom
agent/shadow-validation

Conversation

@lan17

@lan17lan17 commented Jul 29, 2026

Copy link
Copy Markdown
Owner

Summary

Adds opt-in semantic shadow validation for sensitive, invalidation-tracked Redis cache hits. A selected hit returns its cached value normally, then detached best-effort work re-reads the source of truth (SoT), independently deserializes the retained Redis payload, and compares the two application values.

Closes#104.

Architecture

flowchart LR
A[Tracked Redis hit] --> B[Return cached value]
A --> C{shadowRamp selects key?}
C -- no --> X[No shadow work]
C -- yes --> D{Metric hook and capacity?}
D -- no --> X
D -- yes --> E[Reserve exact-key flight]
E --> F[Unref setImmediate]
F --> G[Read SoT with DialCache disabled]
G --> H[serializer.load retained payload]
H --> I[Default or custom value comparator]
I --> J[Bounded outcome metric]
Loading

The hit path performs eligibility checks and slot reservation only. It does no SoT read, extra deserialization, deep comparison, payload-size-linear copy, repair, or cache mutation. All comparison work begins from an unreferenced immediate and is not awaited by the caller.

Public API and configuration

  • Adds DialCacheKeyConfig.shadowRamp?: number.
    • Omitted or 0 disables validation.
    • 100 selects every otherwise eligible exact key.
    • Partial ramps use a stable exact-key cohort with an independent :shadow discriminator.
    • Sparse runtime overlays inherit the static baseline; malformed values skip shadow work and emit the existing remote config_resolution error without changing the hit.
  • Adds DialCacheConfig.shadowMaxInFlight?: number, a positive safe integer defaulting to 1 per instance.
    • There is no queue.
    • Exact-key duplicates and global-cap overflow emit dropped.
    • Scheduled and timed-out-but-still-running work retains its slot until the underlying promise settles.
  • Adds the root-exported synchronous type ShadowComparator<T> and optional shadowComparator on cached() / getOrLoad() options.
  • Extends DialCacheMetricsAdapter with an optional shadowValidation hook, preserving existing custom adapters.
exporttypeShadowComparator<T>=(cachedValue: T,sourceValue: T,)=>boolean;

Comparison and data boundaries

  • Eligibility requires an actual successful Redis hit with trackForInvalidation: true; request-local/process-local hits, misses, read errors, and initial deserialize failures do not validate.
  • Redis reads retain the client-returned semantic string | Buffer payload internally alongside the decoded value.
  • Detached work reads the raw SoT value, then runs the same effective serializer's load() again on the retained payload to create an independent cached snapshot.
  • The object already returned to the caller is never compared, so caller mutation after the hit cannot contaminate validation.
  • The comparator receives two values of T, never a Redis payload.
  • The default is Node's util.isDeepStrictEqual; an optional per-operation comparator defines use-case-specific equality.
  • SoT is intentionally not dump/load normalized. Lossy serialization remains visible unless a custom comparator explicitly treats the normalized values as equivalent.
  • A comparator must synchronously return a boolean and must be deterministic, side-effect-free, non-mutating, and bounded. Throws and non-boolean returns emit comparison_error.
  • An accidental Promise from untyped JavaScript is consumed safely but never accepted as a comparison result; it retains the flight slot until settlement.
  • Redis framing, timestamps, TTLs, watermarks, Lua, keys, and bundled client protocol remain unchanged.

Detachment, liveness, and safety

  • Source execution starts from an unreferenced setImmediate and runs under dialcache.disable(...) so it cannot recursively satisfy itself from the same cache.
  • One monotonic, unreferenced deadline covers the source read, detached serializer load, and comparison.
  • A finite fallbackTimeoutMs is reused; fallbackTimeoutMs: null keeps normal fallbacks unbounded but gives shadow work the internal 60-second default.
  • On timeout, DialCache releases its retained payload reference and suppresses later phases. Already-running application/serializer/comparator work cannot be cancelled and continues to occupy its slot until settlement.
  • Scheduler and deadline handles do not keep an otherwise idle process alive. Completion during shutdown remains best effort.
  • Detached execution retains original cached() argument references or a getOrLoad() closure; docs require relevant inputs/captures to be immutable or snapshotted.
  • Validation is observational only: no repair, write, TTL refresh, invalidation, local eviction, payload logging, or change to the returned value.

Observability

Bounded outcomes: match, mismatch, source_error, deserialization_error, comparison_error, timeout, and dropped.

Labels/tags are limited to cache namespace, use case, key type, and outcome. IDs, cached/SoT values, payloads, Redis keys, and raw exception messages are excluded.

Built-in adapters add:

  • Prometheus: dialcache_shadow_validation_counter
  • Datadog: dialcache.shadow.count

An adapter without the optional hook disables shadow execution so DialCache never performs an unobservable SoT read. Runtime thenable rejections from the hook are consumed without being awaited.

Compatibility and operations

  • The feature is default-off and all new configuration/operation fields and the metrics hook are optional.
  • Existing cache behavior, Redis wire format, Lua scripts, keys, watermarks, serializers, and package export paths remain unchanged.
  • This is suitable for a minor release and rolling upgrade; enabling it is an operational rollout because it adds sampled SoT reads plus detached serializer/comparison CPU.
  • shadowMaxInFlight bounds one instance, not the fleet. Roll out shadowRamp gradually and monitor match, mismatch, timeout, source_error, comparison_error, and dropped.
  • The Prometheus adapter registers one additive collector family; applications that already own the exact same prefixed name with an incompatible schema must resolve that registry collision.
  • README documents serializer repeatability, custom Redis payload ownership, detached input lifetime, concurrency budgeting, shutdown behavior, and privacy boundaries.

Change footprint

The PR is test-led rather than carrying duplicate first-pass production code:

  • 22 files, +1,992 / -38 against current main
  • Tests and package-consumer proofs: +1,364 / -2 across 7 files (68% of additions)
  • Production source: +453 / -25 across 13 files (23% of additions)
  • Benchmark tooling: +92
  • README: +83 / -11

The dedicated shadow suite contains 36 scenarios covering the detached lifecycle and failure matrix. Shared setup is factored through test-only helpers; no obsolete serialized-payload comparison implementation remains.

Validation

Run on Node.js 22.22.0 at 4b8f7e4, including current main@500d5e7:

  • corepack pnpm check
    • 341 unit tests passed
    • coverage: 97.04% statements, 93.65% branches, 98.47% functions, 97.16% lines
    • typecheck, build, and packed-consumer test passed
  • corepack pnpm test:integration
    • 81 tests passed across Redis 6.2 and Valkey 8 with node-redis and GLIDE
    • includes real tracked-value semantic match/mismatch coverage with no repair
  • DIALCACHE_BENCH_ITERATIONS=20000 DIALCACHE_BENCH_FANOUT=10000 corepack pnpm benchmark:request-local
    • all eight semantic scenarios passed
    • includes tracked Redis hits with shadow omitted and deterministically ramped out
  • corepack pnpm audit --prod: no known production-dependency vulnerabilities
  • GitHub Actions: CI, CodeQL, and PR-title validation passed on 4b8f7e4
  • git diff --check

Focused shadow tests cover detachment, semantic default/custom equality, caller-mutation isolation, serializer ownership/repeatability, runtime ramp overlays, comparator type/runtime failures, monotonic deadlines, timeout slot retention, exact-key/global drops, coalescing, disabled contexts, metric failure isolation, and unreferenced handles.

Review history

  • A broad production review covered concurrency/lifecycle, API compatibility, rollout/configuration, observability, tests, and operations.
  • The cleanup round fixed low-severity coverage gaps for packed public APIs, runtime ramp enable/disable overlays, synchronous comparator deadline crossing, custom Buffer ownership, and honest benchmark reporting.
  • Two fresh post-fix reviewers independently confirmed the feature and its integration with current main with no remaining findings.

@lan17
lan17 marked this pull request as ready for review July 29, 2026 16:55
@lan17
lan17 merged commit 9f771e4 into mainJul 29, 2026
6 checks passed
@lan17
lan17 deleted the agent/shadow-validation branch July 29, 2026 16:58
Sign up for freeto 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.

Add sampled shadow validation for tracked Redis cache hits

1 participant

@lan17
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

feat: add sampled Redis shadow validation - #105

Merged
lan17 merged 5 commits into
mainfrom
agent/shadow-validation
Jul 29, 2026
Merged

feat: add sampled Redis shadow validation#105
lan17 merged 5 commits into
mainfrom
agent/shadow-validation

Conversation

@lan17

@lan17lan17 commented Jul 29, 2026

Copy link
Copy Markdown
Owner

Summary

Adds opt-in semantic shadow validation for sensitive, invalidation-tracked Redis cache hits. A selected hit returns its cached value normally, then detached best-effort work re-reads the source of truth (SoT), independently deserializes the retained Redis payload, and compares the two application values.

Closes#104.

Architecture

flowchart LR
A[Tracked Redis hit] --> B[Return cached value]
A --> C{shadowRamp selects key?}
C -- no --> X[No shadow work]
C -- yes --> D{Metric hook and capacity?}
D -- no --> X
D -- yes --> E[Reserve exact-key flight]
E --> F[Unref setImmediate]
F --> G[Read SoT with DialCache disabled]
G --> H[serializer.load retained payload]
H --> I[Default or custom value comparator]
I --> J[Bounded outcome metric]
Loading

The hit path performs eligibility checks and slot reservation only. It does no SoT read, extra deserialization, deep comparison, payload-size-linear copy, repair, or cache mutation. All comparison work begins from an unreferenced immediate and is not awaited by the caller.

Public API and configuration

  • Adds DialCacheKeyConfig.shadowRamp?: number.
    • Omitted or 0 disables validation.
    • 100 selects every otherwise eligible exact key.
    • Partial ramps use a stable exact-key cohort with an independent :shadow discriminator.
    • Sparse runtime overlays inherit the static baseline; malformed values skip shadow work and emit the existing remote config_resolution error without changing the hit.
  • Adds DialCacheConfig.shadowMaxInFlight?: number, a positive safe integer defaulting to 1 per instance.
    • There is no queue.
    • Exact-key duplicates and global-cap overflow emit dropped.
    • Scheduled and timed-out-but-still-running work retains its slot until the underlying promise settles.
  • Adds the root-exported synchronous type ShadowComparator<T> and optional shadowComparator on cached() / getOrLoad() options.
  • Extends DialCacheMetricsAdapter with an optional shadowValidation hook, preserving existing custom adapters.
exporttypeShadowComparator<T>=(cachedValue: T,sourceValue: T,)=>boolean;

Comparison and data boundaries

  • Eligibility requires an actual successful Redis hit with trackForInvalidation: true; request-local/process-local hits, misses, read errors, and initial deserialize failures do not validate.
  • Redis reads retain the client-returned semantic string | Buffer payload internally alongside the decoded value.
  • Detached work reads the raw SoT value, then runs the same effective serializer's load() again on the retained payload to create an independent cached snapshot.
  • The object already returned to the caller is never compared, so caller mutation after the hit cannot contaminate validation.
  • The comparator receives two values of T, never a Redis payload.
  • The default is Node's util.isDeepStrictEqual; an optional per-operation comparator defines use-case-specific equality.
  • SoT is intentionally not dump/load normalized. Lossy serialization remains visible unless a custom comparator explicitly treats the normalized values as equivalent.
  • A comparator must synchronously return a boolean and must be deterministic, side-effect-free, non-mutating, and bounded. Throws and non-boolean returns emit comparison_error.
  • An accidental Promise from untyped JavaScript is consumed safely but never accepted as a comparison result; it retains the flight slot until settlement.
  • Redis framing, timestamps, TTLs, watermarks, Lua, keys, and bundled client protocol remain unchanged.

Detachment, liveness, and safety

  • Source execution starts from an unreferenced setImmediate and runs under dialcache.disable(...) so it cannot recursively satisfy itself from the same cache.
  • One monotonic, unreferenced deadline covers the source read, detached serializer load, and comparison.
  • A finite fallbackTimeoutMs is reused; fallbackTimeoutMs: null keeps normal fallbacks unbounded but gives shadow work the internal 60-second default.
  • On timeout, DialCache releases its retained payload reference and suppresses later phases. Already-running application/serializer/comparator work cannot be cancelled and continues to occupy its slot until settlement.
  • Scheduler and deadline handles do not keep an otherwise idle process alive. Completion during shutdown remains best effort.
  • Detached execution retains original cached() argument references or a getOrLoad() closure; docs require relevant inputs/captures to be immutable or snapshotted.
  • Validation is observational only: no repair, write, TTL refresh, invalidation, local eviction, payload logging, or change to the returned value.

Observability

Bounded outcomes: match, mismatch, source_error, deserialization_error, comparison_error, timeout, and dropped.

Labels/tags are limited to cache namespace, use case, key type, and outcome. IDs, cached/SoT values, payloads, Redis keys, and raw exception messages are excluded.

Built-in adapters add:

  • Prometheus: dialcache_shadow_validation_counter
  • Datadog: dialcache.shadow.count

An adapter without the optional hook disables shadow execution so DialCache never performs an unobservable SoT read. Runtime thenable rejections from the hook are consumed without being awaited.

Compatibility and operations

  • The feature is default-off and all new configuration/operation fields and the metrics hook are optional.
  • Existing cache behavior, Redis wire format, Lua scripts, keys, watermarks, serializers, and package export paths remain unchanged.
  • This is suitable for a minor release and rolling upgrade; enabling it is an operational rollout because it adds sampled SoT reads plus detached serializer/comparison CPU.
  • shadowMaxInFlight bounds one instance, not the fleet. Roll out shadowRamp gradually and monitor match, mismatch, timeout, source_error, comparison_error, and dropped.
  • The Prometheus adapter registers one additive collector family; applications that already own the exact same prefixed name with an incompatible schema must resolve that registry collision.
  • README documents serializer repeatability, custom Redis payload ownership, detached input lifetime, concurrency budgeting, shutdown behavior, and privacy boundaries.

Change footprint

The PR is test-led rather than carrying duplicate first-pass production code:

  • 22 files, +1,992 / -38 against current main
  • Tests and package-consumer proofs: +1,364 / -2 across 7 files (68% of additions)
  • Production source: +453 / -25 across 13 files (23% of additions)
  • Benchmark tooling: +92
  • README: +83 / -11

The dedicated shadow suite contains 36 scenarios covering the detached lifecycle and failure matrix. Shared setup is factored through test-only helpers; no obsolete serialized-payload comparison implementation remains.

Validation

Run on Node.js 22.22.0 at 4b8f7e4, including current main@500d5e7:

  • corepack pnpm check
    • 341 unit tests passed
    • coverage: 97.04% statements, 93.65% branches, 98.47% functions, 97.16% lines
    • typecheck, build, and packed-consumer test passed
  • corepack pnpm test:integration
    • 81 tests passed across Redis 6.2 and Valkey 8 with node-redis and GLIDE
    • includes real tracked-value semantic match/mismatch coverage with no repair
  • DIALCACHE_BENCH_ITERATIONS=20000 DIALCACHE_BENCH_FANOUT=10000 corepack pnpm benchmark:request-local
    • all eight semantic scenarios passed
    • includes tracked Redis hits with shadow omitted and deterministically ramped out
  • corepack pnpm audit --prod: no known production-dependency vulnerabilities
  • GitHub Actions: CI, CodeQL, and PR-title validation passed on 4b8f7e4
  • git diff --check

Focused shadow tests cover detachment, semantic default/custom equality, caller-mutation isolation, serializer ownership/repeatability, runtime ramp overlays, comparator type/runtime failures, monotonic deadlines, timeout slot retention, exact-key/global drops, coalescing, disabled contexts, metric failure isolation, and unreferenced handles.

Review history

  • A broad production review covered concurrency/lifecycle, API compatibility, rollout/configuration, observability, tests, and operations.
  • The cleanup round fixed low-severity coverage gaps for packed public APIs, runtime ramp enable/disable overlays, synchronous comparator deadline crossing, custom Buffer ownership, and honest benchmark reporting.
  • Two fresh post-fix reviewers independently confirmed the feature and its integration with current main with no remaining findings.

@lan17
lan17 marked this pull request as ready for review July 29, 2026 16:55
@lan17
lan17 merged commit 9f771e4 into mainJul 29, 2026
6 checks passed
@lan17
lan17 deleted the agent/shadow-validation branch July 29, 2026 16:58
Sign up for freeto 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.

Add sampled shadow validation for tracked Redis cache hits

1 participant

@lan17
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat: add sampled Redis shadow validation - #105

Merged
lan17 merged 5 commits into
mainfrom
agent/shadow-validation
Jul 29, 2026
Merged

feat: add sampled Redis shadow validation#105
lan17 merged 5 commits into
mainfrom
agent/shadow-validation

Conversation

@lan17

@lan17lan17 commented Jul 29, 2026

Copy link
Copy Markdown
Owner

Summary

Adds opt-in semantic shadow validation for sensitive, invalidation-tracked Redis cache hits. A selected hit returns its cached value normally, then detached best-effort work re-reads the source of truth (SoT), independently deserializes the retained Redis payload, and compares the two application values.

Closes#104.

Architecture

flowchart LR
A[Tracked Redis hit] --> B[Return cached value]
A --> C{shadowRamp selects key?}
C -- no --> X[No shadow work]
C -- yes --> D{Metric hook and capacity?}
D -- no --> X
D -- yes --> E[Reserve exact-key flight]
E --> F[Unref setImmediate]
F --> G[Read SoT with DialCache disabled]
G --> H[serializer.load retained payload]
H --> I[Default or custom value comparator]
I --> J[Bounded outcome metric]
Loading

The hit path performs eligibility checks and slot reservation only. It does no SoT read, extra deserialization, deep comparison, payload-size-linear copy, repair, or cache mutation. All comparison work begins from an unreferenced immediate and is not awaited by the caller.

Public API and configuration

  • Adds DialCacheKeyConfig.shadowRamp?: number.
    • Omitted or 0 disables validation.
    • 100 selects every otherwise eligible exact key.
    • Partial ramps use a stable exact-key cohort with an independent :shadow discriminator.
    • Sparse runtime overlays inherit the static baseline; malformed values skip shadow work and emit the existing remote config_resolution error without changing the hit.
  • Adds DialCacheConfig.shadowMaxInFlight?: number, a positive safe integer defaulting to 1 per instance.
    • There is no queue.
    • Exact-key duplicates and global-cap overflow emit dropped.
    • Scheduled and timed-out-but-still-running work retains its slot until the underlying promise settles.
  • Adds the root-exported synchronous type ShadowComparator<T> and optional shadowComparator on cached() / getOrLoad() options.
  • Extends DialCacheMetricsAdapter with an optional shadowValidation hook, preserving existing custom adapters.
exporttypeShadowComparator<T>=(cachedValue: T,sourceValue: T,)=>boolean;

Comparison and data boundaries

  • Eligibility requires an actual successful Redis hit with trackForInvalidation: true; request-local/process-local hits, misses, read errors, and initial deserialize failures do not validate.
  • Redis reads retain the client-returned semantic string | Buffer payload internally alongside the decoded value.
  • Detached work reads the raw SoT value, then runs the same effective serializer's load() again on the retained payload to create an independent cached snapshot.
  • The object already returned to the caller is never compared, so caller mutation after the hit cannot contaminate validation.
  • The comparator receives two values of T, never a Redis payload.
  • The default is Node's util.isDeepStrictEqual; an optional per-operation comparator defines use-case-specific equality.
  • SoT is intentionally not dump/load normalized. Lossy serialization remains visible unless a custom comparator explicitly treats the normalized values as equivalent.
  • A comparator must synchronously return a boolean and must be deterministic, side-effect-free, non-mutating, and bounded. Throws and non-boolean returns emit comparison_error.
  • An accidental Promise from untyped JavaScript is consumed safely but never accepted as a comparison result; it retains the flight slot until settlement.
  • Redis framing, timestamps, TTLs, watermarks, Lua, keys, and bundled client protocol remain unchanged.

Detachment, liveness, and safety

  • Source execution starts from an unreferenced setImmediate and runs under dialcache.disable(...) so it cannot recursively satisfy itself from the same cache.
  • One monotonic, unreferenced deadline covers the source read, detached serializer load, and comparison.
  • A finite fallbackTimeoutMs is reused; fallbackTimeoutMs: null keeps normal fallbacks unbounded but gives shadow work the internal 60-second default.
  • On timeout, DialCache releases its retained payload reference and suppresses later phases. Already-running application/serializer/comparator work cannot be cancelled and continues to occupy its slot until settlement.
  • Scheduler and deadline handles do not keep an otherwise idle process alive. Completion during shutdown remains best effort.
  • Detached execution retains original cached() argument references or a getOrLoad() closure; docs require relevant inputs/captures to be immutable or snapshotted.
  • Validation is observational only: no repair, write, TTL refresh, invalidation, local eviction, payload logging, or change to the returned value.

Observability

Bounded outcomes: match, mismatch, source_error, deserialization_error, comparison_error, timeout, and dropped.

Labels/tags are limited to cache namespace, use case, key type, and outcome. IDs, cached/SoT values, payloads, Redis keys, and raw exception messages are excluded.

Built-in adapters add:

  • Prometheus: dialcache_shadow_validation_counter
  • Datadog: dialcache.shadow.count

An adapter without the optional hook disables shadow execution so DialCache never performs an unobservable SoT read. Runtime thenable rejections from the hook are consumed without being awaited.

Compatibility and operations

  • The feature is default-off and all new configuration/operation fields and the metrics hook are optional.
  • Existing cache behavior, Redis wire format, Lua scripts, keys, watermarks, serializers, and package export paths remain unchanged.
  • This is suitable for a minor release and rolling upgrade; enabling it is an operational rollout because it adds sampled SoT reads plus detached serializer/comparison CPU.
  • shadowMaxInFlight bounds one instance, not the fleet. Roll out shadowRamp gradually and monitor match, mismatch, timeout, source_error, comparison_error, and dropped.
  • The Prometheus adapter registers one additive collector family; applications that already own the exact same prefixed name with an incompatible schema must resolve that registry collision.
  • README documents serializer repeatability, custom Redis payload ownership, detached input lifetime, concurrency budgeting, shutdown behavior, and privacy boundaries.

Change footprint

The PR is test-led rather than carrying duplicate first-pass production code:

  • 22 files, +1,992 / -38 against current main
  • Tests and package-consumer proofs: +1,364 / -2 across 7 files (68% of additions)
  • Production source: +453 / -25 across 13 files (23% of additions)
  • Benchmark tooling: +92
  • README: +83 / -11

The dedicated shadow suite contains 36 scenarios covering the detached lifecycle and failure matrix. Shared setup is factored through test-only helpers; no obsolete serialized-payload comparison implementation remains.

Validation

Run on Node.js 22.22.0 at 4b8f7e4, including current main@500d5e7:

  • corepack pnpm check
    • 341 unit tests passed
    • coverage: 97.04% statements, 93.65% branches, 98.47% functions, 97.16% lines
    • typecheck, build, and packed-consumer test passed
  • corepack pnpm test:integration
    • 81 tests passed across Redis 6.2 and Valkey 8 with node-redis and GLIDE
    • includes real tracked-value semantic match/mismatch coverage with no repair
  • DIALCACHE_BENCH_ITERATIONS=20000 DIALCACHE_BENCH_FANOUT=10000 corepack pnpm benchmark:request-local
    • all eight semantic scenarios passed
    • includes tracked Redis hits with shadow omitted and deterministically ramped out
  • corepack pnpm audit --prod: no known production-dependency vulnerabilities
  • GitHub Actions: CI, CodeQL, and PR-title validation passed on 4b8f7e4
  • git diff --check

Focused shadow tests cover detachment, semantic default/custom equality, caller-mutation isolation, serializer ownership/repeatability, runtime ramp overlays, comparator type/runtime failures, monotonic deadlines, timeout slot retention, exact-key/global drops, coalescing, disabled contexts, metric failure isolation, and unreferenced handles.

Review history

  • A broad production review covered concurrency/lifecycle, API compatibility, rollout/configuration, observability, tests, and operations.
  • The cleanup round fixed low-severity coverage gaps for packed public APIs, runtime ramp enable/disable overlays, synchronous comparator deadline crossing, custom Buffer ownership, and honest benchmark reporting.
  • Two fresh post-fix reviewers independently confirmed the feature and its integration with current main with no remaining findings.

@lan17
lan17 marked this pull request as ready for review July 29, 2026 16:55
@lan17
lan17 merged commit 9f771e4 into mainJul 29, 2026
6 checks passed
@lan17
lan17 deleted the agent/shadow-validation branch July 29, 2026 16:58
Sign up for freeto 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.

Add sampled shadow validation for tracked Redis cache hits

1 participant

@lan17
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat: add sampled Redis shadow validation - #105

Merged
lan17 merged 5 commits into
mainfrom
agent/shadow-validation
Jul 29, 2026
Merged

feat: add sampled Redis shadow validation#105
lan17 merged 5 commits into
mainfrom
agent/shadow-validation

Conversation

@lan17

@lan17lan17 commented Jul 29, 2026

Copy link
Copy Markdown
Owner

Summary

Adds opt-in semantic shadow validation for sensitive, invalidation-tracked Redis cache hits. A selected hit returns its cached value normally, then detached best-effort work re-reads the source of truth (SoT), independently deserializes the retained Redis payload, and compares the two application values.

Closes#104.

Architecture

flowchart LR
A[Tracked Redis hit] --> B[Return cached value]
A --> C{shadowRamp selects key?}
C -- no --> X[No shadow work]
C -- yes --> D{Metric hook and capacity?}
D -- no --> X
D -- yes --> E[Reserve exact-key flight]
E --> F[Unref setImmediate]
F --> G[Read SoT with DialCache disabled]
G --> H[serializer.load retained payload]
H --> I[Default or custom value comparator]
I --> J[Bounded outcome metric]
Loading

The hit path performs eligibility checks and slot reservation only. It does no SoT read, extra deserialization, deep comparison, payload-size-linear copy, repair, or cache mutation. All comparison work begins from an unreferenced immediate and is not awaited by the caller.

Public API and configuration

  • Adds DialCacheKeyConfig.shadowRamp?: number.
    • Omitted or 0 disables validation.
    • 100 selects every otherwise eligible exact key.
    • Partial ramps use a stable exact-key cohort with an independent :shadow discriminator.
    • Sparse runtime overlays inherit the static baseline; malformed values skip shadow work and emit the existing remote config_resolution error without changing the hit.
  • Adds DialCacheConfig.shadowMaxInFlight?: number, a positive safe integer defaulting to 1 per instance.
    • There is no queue.
    • Exact-key duplicates and global-cap overflow emit dropped.
    • Scheduled and timed-out-but-still-running work retains its slot until the underlying promise settles.
  • Adds the root-exported synchronous type ShadowComparator<T> and optional shadowComparator on cached() / getOrLoad() options.
  • Extends DialCacheMetricsAdapter with an optional shadowValidation hook, preserving existing custom adapters.
exporttypeShadowComparator<T>=(cachedValue: T,sourceValue: T,)=>boolean;

Comparison and data boundaries

  • Eligibility requires an actual successful Redis hit with trackForInvalidation: true; request-local/process-local hits, misses, read errors, and initial deserialize failures do not validate.
  • Redis reads retain the client-returned semantic string | Buffer payload internally alongside the decoded value.
  • Detached work reads the raw SoT value, then runs the same effective serializer's load() again on the retained payload to create an independent cached snapshot.
  • The object already returned to the caller is never compared, so caller mutation after the hit cannot contaminate validation.
  • The comparator receives two values of T, never a Redis payload.
  • The default is Node's util.isDeepStrictEqual; an optional per-operation comparator defines use-case-specific equality.
  • SoT is intentionally not dump/load normalized. Lossy serialization remains visible unless a custom comparator explicitly treats the normalized values as equivalent.
  • A comparator must synchronously return a boolean and must be deterministic, side-effect-free, non-mutating, and bounded. Throws and non-boolean returns emit comparison_error.
  • An accidental Promise from untyped JavaScript is consumed safely but never accepted as a comparison result; it retains the flight slot until settlement.
  • Redis framing, timestamps, TTLs, watermarks, Lua, keys, and bundled client protocol remain unchanged.

Detachment, liveness, and safety

  • Source execution starts from an unreferenced setImmediate and runs under dialcache.disable(...) so it cannot recursively satisfy itself from the same cache.
  • One monotonic, unreferenced deadline covers the source read, detached serializer load, and comparison.
  • A finite fallbackTimeoutMs is reused; fallbackTimeoutMs: null keeps normal fallbacks unbounded but gives shadow work the internal 60-second default.
  • On timeout, DialCache releases its retained payload reference and suppresses later phases. Already-running application/serializer/comparator work cannot be cancelled and continues to occupy its slot until settlement.
  • Scheduler and deadline handles do not keep an otherwise idle process alive. Completion during shutdown remains best effort.
  • Detached execution retains original cached() argument references or a getOrLoad() closure; docs require relevant inputs/captures to be immutable or snapshotted.
  • Validation is observational only: no repair, write, TTL refresh, invalidation, local eviction, payload logging, or change to the returned value.

Observability

Bounded outcomes: match, mismatch, source_error, deserialization_error, comparison_error, timeout, and dropped.

Labels/tags are limited to cache namespace, use case, key type, and outcome. IDs, cached/SoT values, payloads, Redis keys, and raw exception messages are excluded.

Built-in adapters add:

  • Prometheus: dialcache_shadow_validation_counter
  • Datadog: dialcache.shadow.count

An adapter without the optional hook disables shadow execution so DialCache never performs an unobservable SoT read. Runtime thenable rejections from the hook are consumed without being awaited.

Compatibility and operations

  • The feature is default-off and all new configuration/operation fields and the metrics hook are optional.
  • Existing cache behavior, Redis wire format, Lua scripts, keys, watermarks, serializers, and package export paths remain unchanged.
  • This is suitable for a minor release and rolling upgrade; enabling it is an operational rollout because it adds sampled SoT reads plus detached serializer/comparison CPU.
  • shadowMaxInFlight bounds one instance, not the fleet. Roll out shadowRamp gradually and monitor match, mismatch, timeout, source_error, comparison_error, and dropped.
  • The Prometheus adapter registers one additive collector family; applications that already own the exact same prefixed name with an incompatible schema must resolve that registry collision.
  • README documents serializer repeatability, custom Redis payload ownership, detached input lifetime, concurrency budgeting, shutdown behavior, and privacy boundaries.

Change footprint

The PR is test-led rather than carrying duplicate first-pass production code:

  • 22 files, +1,992 / -38 against current main
  • Tests and package-consumer proofs: +1,364 / -2 across 7 files (68% of additions)
  • Production source: +453 / -25 across 13 files (23% of additions)
  • Benchmark tooling: +92
  • README: +83 / -11

The dedicated shadow suite contains 36 scenarios covering the detached lifecycle and failure matrix. Shared setup is factored through test-only helpers; no obsolete serialized-payload comparison implementation remains.

Validation

Run on Node.js 22.22.0 at 4b8f7e4, including current main@500d5e7:

  • corepack pnpm check
    • 341 unit tests passed
    • coverage: 97.04% statements, 93.65% branches, 98.47% functions, 97.16% lines
    • typecheck, build, and packed-consumer test passed
  • corepack pnpm test:integration
    • 81 tests passed across Redis 6.2 and Valkey 8 with node-redis and GLIDE
    • includes real tracked-value semantic match/mismatch coverage with no repair
  • DIALCACHE_BENCH_ITERATIONS=20000 DIALCACHE_BENCH_FANOUT=10000 corepack pnpm benchmark:request-local
    • all eight semantic scenarios passed
    • includes tracked Redis hits with shadow omitted and deterministically ramped out
  • corepack pnpm audit --prod: no known production-dependency vulnerabilities
  • GitHub Actions: CI, CodeQL, and PR-title validation passed on 4b8f7e4
  • git diff --check

Focused shadow tests cover detachment, semantic default/custom equality, caller-mutation isolation, serializer ownership/repeatability, runtime ramp overlays, comparator type/runtime failures, monotonic deadlines, timeout slot retention, exact-key/global drops, coalescing, disabled contexts, metric failure isolation, and unreferenced handles.

Review history

  • A broad production review covered concurrency/lifecycle, API compatibility, rollout/configuration, observability, tests, and operations.
  • The cleanup round fixed low-severity coverage gaps for packed public APIs, runtime ramp enable/disable overlays, synchronous comparator deadline crossing, custom Buffer ownership, and honest benchmark reporting.
  • Two fresh post-fix reviewers independently confirmed the feature and its integration with current main with no remaining findings.

@lan17
lan17 marked this pull request as ready for review July 29, 2026 16:55
@lan17
lan17 merged commit 9f771e4 into mainJul 29, 2026
6 checks passed
@lan17
lan17 deleted the agent/shadow-validation branch July 29, 2026 16:58
Sign up for freeto 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.

Add sampled shadow validation for tracked Redis cache hits

1 participant

@lan17
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

feat: add sampled Redis shadow validation - #105

Merged
lan17 merged 5 commits into
mainfrom
agent/shadow-validation
Jul 29, 2026
Merged

feat: add sampled Redis shadow validation#105
lan17 merged 5 commits into
mainfrom
agent/shadow-validation

Conversation

@lan17

@lan17lan17 commented Jul 29, 2026

Copy link
Copy Markdown
Owner

Summary

Adds opt-in semantic shadow validation for sensitive, invalidation-tracked Redis cache hits. A selected hit returns its cached value normally, then detached best-effort work re-reads the source of truth (SoT), independently deserializes the retained Redis payload, and compares the two application values.

Closes#104.

Architecture

flowchart LR
A[Tracked Redis hit] --> B[Return cached value]
A --> C{shadowRamp selects key?}
C -- no --> X[No shadow work]
C -- yes --> D{Metric hook and capacity?}
D -- no --> X
D -- yes --> E[Reserve exact-key flight]
E --> F[Unref setImmediate]
F --> G[Read SoT with DialCache disabled]
G --> H[serializer.load retained payload]
H --> I[Default or custom value comparator]
I --> J[Bounded outcome metric]
Loading

The hit path performs eligibility checks and slot reservation only. It does no SoT read, extra deserialization, deep comparison, payload-size-linear copy, repair, or cache mutation. All comparison work begins from an unreferenced immediate and is not awaited by the caller.

Public API and configuration

  • Adds DialCacheKeyConfig.shadowRamp?: number.
    • Omitted or 0 disables validation.
    • 100 selects every otherwise eligible exact key.
    • Partial ramps use a stable exact-key cohort with an independent :shadow discriminator.
    • Sparse runtime overlays inherit the static baseline; malformed values skip shadow work and emit the existing remote config_resolution error without changing the hit.
  • Adds DialCacheConfig.shadowMaxInFlight?: number, a positive safe integer defaulting to 1 per instance.
    • There is no queue.
    • Exact-key duplicates and global-cap overflow emit dropped.
    • Scheduled and timed-out-but-still-running work retains its slot until the underlying promise settles.
  • Adds the root-exported synchronous type ShadowComparator<T> and optional shadowComparator on cached() / getOrLoad() options.
  • Extends DialCacheMetricsAdapter with an optional shadowValidation hook, preserving existing custom adapters.
exporttypeShadowComparator<T>=(cachedValue: T,sourceValue: T,)=>boolean;

Comparison and data boundaries

  • Eligibility requires an actual successful Redis hit with trackForInvalidation: true; request-local/process-local hits, misses, read errors, and initial deserialize failures do not validate.
  • Redis reads retain the client-returned semantic string | Buffer payload internally alongside the decoded value.
  • Detached work reads the raw SoT value, then runs the same effective serializer's load() again on the retained payload to create an independent cached snapshot.
  • The object already returned to the caller is never compared, so caller mutation after the hit cannot contaminate validation.
  • The comparator receives two values of T, never a Redis payload.
  • The default is Node's util.isDeepStrictEqual; an optional per-operation comparator defines use-case-specific equality.
  • SoT is intentionally not dump/load normalized. Lossy serialization remains visible unless a custom comparator explicitly treats the normalized values as equivalent.
  • A comparator must synchronously return a boolean and must be deterministic, side-effect-free, non-mutating, and bounded. Throws and non-boolean returns emit comparison_error.
  • An accidental Promise from untyped JavaScript is consumed safely but never accepted as a comparison result; it retains the flight slot until settlement.
  • Redis framing, timestamps, TTLs, watermarks, Lua, keys, and bundled client protocol remain unchanged.

Detachment, liveness, and safety

  • Source execution starts from an unreferenced setImmediate and runs under dialcache.disable(...) so it cannot recursively satisfy itself from the same cache.
  • One monotonic, unreferenced deadline covers the source read, detached serializer load, and comparison.
  • A finite fallbackTimeoutMs is reused; fallbackTimeoutMs: null keeps normal fallbacks unbounded but gives shadow work the internal 60-second default.
  • On timeout, DialCache releases its retained payload reference and suppresses later phases. Already-running application/serializer/comparator work cannot be cancelled and continues to occupy its slot until settlement.
  • Scheduler and deadline handles do not keep an otherwise idle process alive. Completion during shutdown remains best effort.
  • Detached execution retains original cached() argument references or a getOrLoad() closure; docs require relevant inputs/captures to be immutable or snapshotted.
  • Validation is observational only: no repair, write, TTL refresh, invalidation, local eviction, payload logging, or change to the returned value.

Observability

Bounded outcomes: match, mismatch, source_error, deserialization_error, comparison_error, timeout, and dropped.

Labels/tags are limited to cache namespace, use case, key type, and outcome. IDs, cached/SoT values, payloads, Redis keys, and raw exception messages are excluded.

Built-in adapters add:

  • Prometheus: dialcache_shadow_validation_counter
  • Datadog: dialcache.shadow.count

An adapter without the optional hook disables shadow execution so DialCache never performs an unobservable SoT read. Runtime thenable rejections from the hook are consumed without being awaited.

Compatibility and operations

  • The feature is default-off and all new configuration/operation fields and the metrics hook are optional.
  • Existing cache behavior, Redis wire format, Lua scripts, keys, watermarks, serializers, and package export paths remain unchanged.
  • This is suitable for a minor release and rolling upgrade; enabling it is an operational rollout because it adds sampled SoT reads plus detached serializer/comparison CPU.
  • shadowMaxInFlight bounds one instance, not the fleet. Roll out shadowRamp gradually and monitor match, mismatch, timeout, source_error, comparison_error, and dropped.
  • The Prometheus adapter registers one additive collector family; applications that already own the exact same prefixed name with an incompatible schema must resolve that registry collision.
  • README documents serializer repeatability, custom Redis payload ownership, detached input lifetime, concurrency budgeting, shutdown behavior, and privacy boundaries.

Change footprint

The PR is test-led rather than carrying duplicate first-pass production code:

  • 22 files, +1,992 / -38 against current main
  • Tests and package-consumer proofs: +1,364 / -2 across 7 files (68% of additions)
  • Production source: +453 / -25 across 13 files (23% of additions)
  • Benchmark tooling: +92
  • README: +83 / -11

The dedicated shadow suite contains 36 scenarios covering the detached lifecycle and failure matrix. Shared setup is factored through test-only helpers; no obsolete serialized-payload comparison implementation remains.

Validation

Run on Node.js 22.22.0 at 4b8f7e4, including current main@500d5e7:

  • corepack pnpm check
    • 341 unit tests passed
    • coverage: 97.04% statements, 93.65% branches, 98.47% functions, 97.16% lines
    • typecheck, build, and packed-consumer test passed
  • corepack pnpm test:integration
    • 81 tests passed across Redis 6.2 and Valkey 8 with node-redis and GLIDE
    • includes real tracked-value semantic match/mismatch coverage with no repair
  • DIALCACHE_BENCH_ITERATIONS=20000 DIALCACHE_BENCH_FANOUT=10000 corepack pnpm benchmark:request-local
    • all eight semantic scenarios passed
    • includes tracked Redis hits with shadow omitted and deterministically ramped out
  • corepack pnpm audit --prod: no known production-dependency vulnerabilities
  • GitHub Actions: CI, CodeQL, and PR-title validation passed on 4b8f7e4
  • git diff --check

Focused shadow tests cover detachment, semantic default/custom equality, caller-mutation isolation, serializer ownership/repeatability, runtime ramp overlays, comparator type/runtime failures, monotonic deadlines, timeout slot retention, exact-key/global drops, coalescing, disabled contexts, metric failure isolation, and unreferenced handles.

Review history

  • A broad production review covered concurrency/lifecycle, API compatibility, rollout/configuration, observability, tests, and operations.
  • The cleanup round fixed low-severity coverage gaps for packed public APIs, runtime ramp enable/disable overlays, synchronous comparator deadline crossing, custom Buffer ownership, and honest benchmark reporting.
  • Two fresh post-fix reviewers independently confirmed the feature and its integration with current main with no remaining findings.

@lan17
lan17 marked this pull request as ready for review July 29, 2026 16:55
@lan17
lan17 merged commit 9f771e4 into mainJul 29, 2026
6 checks passed
@lan17
lan17 deleted the agent/shadow-validation branch July 29, 2026 16:58
Sign up for freeto 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.

Add sampled shadow validation for tracked Redis cache hits

1 participant

@lan17