Fail closed when shadow policy is enabled without shadow observability #151

Description

@lan17

Summary

Make positive shadow policy visibly fail closed when the configured metrics adapter cannot record shadow outcomes.

Today, scheduleShadowValidation() silently returns when metrics.shadowValidation is absent. A use case can therefore resolve a positive shadow.ramp while performing no shadow read, comparison, fill, or outcome observation, with no indication that the rollout policy is ineffective.

The intended behavior should remain: do not run detached shadow traffic invisibly. The improvement is to surface the configuration error rather than silently treating the policy as disabled.

Current behavior

Shadow work requires all of the following:

  • valid remote policy;
  • positive shadow ramp;
  • deterministic ramp admission;
  • available per-instance shadow capacity; and
  • metrics.shadowValidation to exist.

The observability hook is currently checked after shadow policy validation, and its absence causes an unconditional return. This couples behavior to metrics wiring without a diagnostic.

Problem

A deployment can:

  1. configure shadow.ramp > 0 in a static default or runtime provider;
  2. omit the optional shadowValidation metrics hook, use an older custom adapter, or accidentally wire the wrong adapter;
  3. observe no shadow traffic and no shadow outcomes; and
  4. incorrectly assume that Redis is being validated or warmed before serving ramp increases.

This is especially risky because shadow mode exists specifically as a controlled rollout and validation mechanism. Silent non-execution defeats that purpose.

Proposed behavior

Static/default policy

When cached() registration or getOrLoad() invocation receives a positive effective static shadow.ramp and the instance metrics adapter has no shadowValidation hook, reject the configuration clearly before any shadow-capable cache execution begins.

Possible error:

DialCache shadow.ramp requires metrics.shadowValidation observability

For cached(), fail at registration. For getOrLoad(), fail during its normal enabled-path option/policy validation. Calls outside an enabled scope should retain true pass-through behavior and should not validate cache policy solely to reject shadow configuration.

If throwing for static getOrLoad() policy conflicts with established fail-open config semantics, the implementation may use the runtime behavior below consistently instead; the decision must be explicit and tested.

Runtime-provider policy

A runtime provider can enable shadow after registration. For a positive resolved runtime shadow ramp without metrics.shadowValidation:

  • disable shadow work for that invocation;
  • preserve normal caller cache behavior;
  • record one bounded config_resolution error when an error hook exists;
  • emit a safe warning that identifies only bounded definition metadata such as namespace, use case, and key type;
  • avoid warning once per invocation through per-use-case deduplication or rate limiting.

Do not synthesize shadow outcomes when no shadow job was admitted.

Zero or omitted policy

shadow omitted, shadow.ramp omitted, or shadow.ramp: 0 remains inert and requires no shadow observer.

Why fail closed

Running detached source/Redis traffic without verdict observability is operationally unsafe:

  • mismatch rates cannot be measured;
  • fill/error/timeout/drop outcomes are invisible;
  • operators cannot know whether a serving-ramp increase is justified;
  • background load can exist without its intended control signal.

Disabling only shadow work preserves application correctness and the source-of-truth fallback while preventing invisible rollout traffic.

Alternative considered

Run shadow work even without shadowValidation and rely on logging or other metrics.

Rejected: the primary bounded outcome series is part of the shadow feature's safety contract. Logging every result is unsuitable, and generic request/error metrics cannot represent match, mismatch, superseded, filled, timeout, or dropped outcomes.

Compatibility

This changes only misconfigured positive shadow policy:

  • configurations with shadow omitted or zero are unchanged;
  • configurations with a working shadowValidation hook are unchanged;
  • configurations that currently request shadow without the hook move from silent no-op to an explicit configuration failure/diagnostic;
  • normal cache serving remains fail open where runtime policy is malformed or unsupported.

Because positive policy that previously did nothing may now reject static registration or emit new diagnostics, document the behavior change in release notes.

Acceptance criteria

  • Positive static/default shadow policy without metrics.shadowValidation cannot remain a silent no-op.
  • The chosen static behavior—registration rejection or explicit fail-closed diagnostic—is consistent across cached() and enabled getOrLoad() and is documented.
  • Positive runtime-provider shadow policy without the hook disables only shadow work and preserves normal caller behavior.
  • Runtime misconfiguration records bounded config_resolution observability where possible.
  • Runtime warnings are deduplicated or rate-limited by bounded definition identity and cannot amplify an incident per invocation.
  • No cache key, ID, source value, cached value, raw error, or other unbounded data is logged or labeled.
  • Omitted and zero shadow policies remain inert without requiring the hook.
  • A metrics adapter with shadowValidation continues to receive exactly one terminal outcome per admitted job.
  • Missing observability does not consume shadow capacity, issue Redis commands, invoke the source, or schedule detached work.
  • Tests cover static defaults, sparse runtime overlays, runtime enable/disable transitions, old custom metrics adapters, logger/metrics failures, and packed ESM/CommonJS consumers.
  • README shadow-rollout guidance states that shadow outcomes are mandatory for positive shadow policy.

Non-goals

  • Making every optional metrics hook mandatory.
  • Adding a second shadow observer or callback.
  • Running shadow work without verdict observability.
  • Changing shadow ramp sampling, capacity, deadlines, comparisons, fills, or caller behavior.

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

      , '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

      Fail closed when shadow policy is enabled without shadow observability #151

      Description

      @lan17

      Summary

      Make positive shadow policy visibly fail closed when the configured metrics adapter cannot record shadow outcomes.

      Today, scheduleShadowValidation() silently returns when metrics.shadowValidation is absent. A use case can therefore resolve a positive shadow.ramp while performing no shadow read, comparison, fill, or outcome observation, with no indication that the rollout policy is ineffective.

      The intended behavior should remain: do not run detached shadow traffic invisibly. The improvement is to surface the configuration error rather than silently treating the policy as disabled.

      Current behavior

      Shadow work requires all of the following:

      • valid remote policy;
      • positive shadow ramp;
      • deterministic ramp admission;
      • available per-instance shadow capacity; and
      • metrics.shadowValidation to exist.

      The observability hook is currently checked after shadow policy validation, and its absence causes an unconditional return. This couples behavior to metrics wiring without a diagnostic.

      Problem

      A deployment can:

      1. configure shadow.ramp > 0 in a static default or runtime provider;
      2. omit the optional shadowValidation metrics hook, use an older custom adapter, or accidentally wire the wrong adapter;
      3. observe no shadow traffic and no shadow outcomes; and
      4. incorrectly assume that Redis is being validated or warmed before serving ramp increases.

      This is especially risky because shadow mode exists specifically as a controlled rollout and validation mechanism. Silent non-execution defeats that purpose.

      Proposed behavior

      Static/default policy

      When cached() registration or getOrLoad() invocation receives a positive effective static shadow.ramp and the instance metrics adapter has no shadowValidation hook, reject the configuration clearly before any shadow-capable cache execution begins.

      Possible error:

      DialCache shadow.ramp requires metrics.shadowValidation observability
      

      For cached(), fail at registration. For getOrLoad(), fail during its normal enabled-path option/policy validation. Calls outside an enabled scope should retain true pass-through behavior and should not validate cache policy solely to reject shadow configuration.

      If throwing for static getOrLoad() policy conflicts with established fail-open config semantics, the implementation may use the runtime behavior below consistently instead; the decision must be explicit and tested.

      Runtime-provider policy

      A runtime provider can enable shadow after registration. For a positive resolved runtime shadow ramp without metrics.shadowValidation:

      • disable shadow work for that invocation;
      • preserve normal caller cache behavior;
      • record one bounded config_resolution error when an error hook exists;
      • emit a safe warning that identifies only bounded definition metadata such as namespace, use case, and key type;
      • avoid warning once per invocation through per-use-case deduplication or rate limiting.

      Do not synthesize shadow outcomes when no shadow job was admitted.

      Zero or omitted policy

      shadow omitted, shadow.ramp omitted, or shadow.ramp: 0 remains inert and requires no shadow observer.

      Why fail closed

      Running detached source/Redis traffic without verdict observability is operationally unsafe:

      • mismatch rates cannot be measured;
      • fill/error/timeout/drop outcomes are invisible;
      • operators cannot know whether a serving-ramp increase is justified;
      • background load can exist without its intended control signal.

      Disabling only shadow work preserves application correctness and the source-of-truth fallback while preventing invisible rollout traffic.

      Alternative considered

      Run shadow work even without shadowValidation and rely on logging or other metrics.

      Rejected: the primary bounded outcome series is part of the shadow feature's safety contract. Logging every result is unsuitable, and generic request/error metrics cannot represent match, mismatch, superseded, filled, timeout, or dropped outcomes.

      Compatibility

      This changes only misconfigured positive shadow policy:

      • configurations with shadow omitted or zero are unchanged;
      • configurations with a working shadowValidation hook are unchanged;
      • configurations that currently request shadow without the hook move from silent no-op to an explicit configuration failure/diagnostic;
      • normal cache serving remains fail open where runtime policy is malformed or unsupported.

      Because positive policy that previously did nothing may now reject static registration or emit new diagnostics, document the behavior change in release notes.

      Acceptance criteria

      • Positive static/default shadow policy without metrics.shadowValidation cannot remain a silent no-op.
      • The chosen static behavior—registration rejection or explicit fail-closed diagnostic—is consistent across cached() and enabled getOrLoad() and is documented.
      • Positive runtime-provider shadow policy without the hook disables only shadow work and preserves normal caller behavior.
      • Runtime misconfiguration records bounded config_resolution observability where possible.
      • Runtime warnings are deduplicated or rate-limited by bounded definition identity and cannot amplify an incident per invocation.
      • No cache key, ID, source value, cached value, raw error, or other unbounded data is logged or labeled.
      • Omitted and zero shadow policies remain inert without requiring the hook.
      • A metrics adapter with shadowValidation continues to receive exactly one terminal outcome per admitted job.
      • Missing observability does not consume shadow capacity, issue Redis commands, invoke the source, or schedule detached work.
      • Tests cover static defaults, sparse runtime overlays, runtime enable/disable transitions, old custom metrics adapters, logger/metrics failures, and packed ESM/CommonJS consumers.
      • README shadow-rollout guidance states that shadow outcomes are mandatory for positive shadow policy.

      Non-goals

      • Making every optional metrics hook mandatory.
      • Adding a second shadow observer or callback.
      • Running shadow work without verdict observability.
      • Changing shadow ramp sampling, capacity, deadlines, comparisons, fills, or caller behavior.

      Related

      Metadata

      Metadata

      Assignees

      No one assigned

        Labels

        No labels
        No labels

        Projects

        No projects

          Milestone

          No milestone

          Relationships

          None yet

          Development

          No branches or pull requests

          Issue actions

          , '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

          Fail closed when shadow policy is enabled without shadow observability #151

          Description

          @lan17

          Summary

          Make positive shadow policy visibly fail closed when the configured metrics adapter cannot record shadow outcomes.

          Today, scheduleShadowValidation() silently returns when metrics.shadowValidation is absent. A use case can therefore resolve a positive shadow.ramp while performing no shadow read, comparison, fill, or outcome observation, with no indication that the rollout policy is ineffective.

          The intended behavior should remain: do not run detached shadow traffic invisibly. The improvement is to surface the configuration error rather than silently treating the policy as disabled.

          Current behavior

          Shadow work requires all of the following:

          • valid remote policy;
          • positive shadow ramp;
          • deterministic ramp admission;
          • available per-instance shadow capacity; and
          • metrics.shadowValidation to exist.

          The observability hook is currently checked after shadow policy validation, and its absence causes an unconditional return. This couples behavior to metrics wiring without a diagnostic.

          Problem

          A deployment can:

          1. configure shadow.ramp > 0 in a static default or runtime provider;
          2. omit the optional shadowValidation metrics hook, use an older custom adapter, or accidentally wire the wrong adapter;
          3. observe no shadow traffic and no shadow outcomes; and
          4. incorrectly assume that Redis is being validated or warmed before serving ramp increases.

          This is especially risky because shadow mode exists specifically as a controlled rollout and validation mechanism. Silent non-execution defeats that purpose.

          Proposed behavior

          Static/default policy

          When cached() registration or getOrLoad() invocation receives a positive effective static shadow.ramp and the instance metrics adapter has no shadowValidation hook, reject the configuration clearly before any shadow-capable cache execution begins.

          Possible error:

          DialCache shadow.ramp requires metrics.shadowValidation observability
          

          For cached(), fail at registration. For getOrLoad(), fail during its normal enabled-path option/policy validation. Calls outside an enabled scope should retain true pass-through behavior and should not validate cache policy solely to reject shadow configuration.

          If throwing for static getOrLoad() policy conflicts with established fail-open config semantics, the implementation may use the runtime behavior below consistently instead; the decision must be explicit and tested.

          Runtime-provider policy

          A runtime provider can enable shadow after registration. For a positive resolved runtime shadow ramp without metrics.shadowValidation:

          • disable shadow work for that invocation;
          • preserve normal caller cache behavior;
          • record one bounded config_resolution error when an error hook exists;
          • emit a safe warning that identifies only bounded definition metadata such as namespace, use case, and key type;
          • avoid warning once per invocation through per-use-case deduplication or rate limiting.

          Do not synthesize shadow outcomes when no shadow job was admitted.

          Zero or omitted policy

          shadow omitted, shadow.ramp omitted, or shadow.ramp: 0 remains inert and requires no shadow observer.

          Why fail closed

          Running detached source/Redis traffic without verdict observability is operationally unsafe:

          • mismatch rates cannot be measured;
          • fill/error/timeout/drop outcomes are invisible;
          • operators cannot know whether a serving-ramp increase is justified;
          • background load can exist without its intended control signal.

          Disabling only shadow work preserves application correctness and the source-of-truth fallback while preventing invisible rollout traffic.

          Alternative considered

          Run shadow work even without shadowValidation and rely on logging or other metrics.

          Rejected: the primary bounded outcome series is part of the shadow feature's safety contract. Logging every result is unsuitable, and generic request/error metrics cannot represent match, mismatch, superseded, filled, timeout, or dropped outcomes.

          Compatibility

          This changes only misconfigured positive shadow policy:

          • configurations with shadow omitted or zero are unchanged;
          • configurations with a working shadowValidation hook are unchanged;
          • configurations that currently request shadow without the hook move from silent no-op to an explicit configuration failure/diagnostic;
          • normal cache serving remains fail open where runtime policy is malformed or unsupported.

          Because positive policy that previously did nothing may now reject static registration or emit new diagnostics, document the behavior change in release notes.

          Acceptance criteria

          • Positive static/default shadow policy without metrics.shadowValidation cannot remain a silent no-op.
          • The chosen static behavior—registration rejection or explicit fail-closed diagnostic—is consistent across cached() and enabled getOrLoad() and is documented.
          • Positive runtime-provider shadow policy without the hook disables only shadow work and preserves normal caller behavior.
          • Runtime misconfiguration records bounded config_resolution observability where possible.
          • Runtime warnings are deduplicated or rate-limited by bounded definition identity and cannot amplify an incident per invocation.
          • No cache key, ID, source value, cached value, raw error, or other unbounded data is logged or labeled.
          • Omitted and zero shadow policies remain inert without requiring the hook.
          • A metrics adapter with shadowValidation continues to receive exactly one terminal outcome per admitted job.
          • Missing observability does not consume shadow capacity, issue Redis commands, invoke the source, or schedule detached work.
          • Tests cover static defaults, sparse runtime overlays, runtime enable/disable transitions, old custom metrics adapters, logger/metrics failures, and packed ESM/CommonJS consumers.
          • README shadow-rollout guidance states that shadow outcomes are mandatory for positive shadow policy.

          Non-goals

          • Making every optional metrics hook mandatory.
          • Adding a second shadow observer or callback.
          • Running shadow work without verdict observability.
          • Changing shadow ramp sampling, capacity, deadlines, comparisons, fills, or caller behavior.

          Related

          Metadata

          Metadata

          Assignees

          No one assigned

            Labels

            No labels
            No labels

            Projects

            No projects

              Milestone

              No milestone

              Relationships

              None yet

              Development

              No branches or pull requests

              Issue actions

              , '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

              Fail closed when shadow policy is enabled without shadow observability #151

              Description

              @lan17

              Summary

              Make positive shadow policy visibly fail closed when the configured metrics adapter cannot record shadow outcomes.

              Today, scheduleShadowValidation() silently returns when metrics.shadowValidation is absent. A use case can therefore resolve a positive shadow.ramp while performing no shadow read, comparison, fill, or outcome observation, with no indication that the rollout policy is ineffective.

              The intended behavior should remain: do not run detached shadow traffic invisibly. The improvement is to surface the configuration error rather than silently treating the policy as disabled.

              Current behavior

              Shadow work requires all of the following:

              • valid remote policy;
              • positive shadow ramp;
              • deterministic ramp admission;
              • available per-instance shadow capacity; and
              • metrics.shadowValidation to exist.

              The observability hook is currently checked after shadow policy validation, and its absence causes an unconditional return. This couples behavior to metrics wiring without a diagnostic.

              Problem

              A deployment can:

              1. configure shadow.ramp > 0 in a static default or runtime provider;
              2. omit the optional shadowValidation metrics hook, use an older custom adapter, or accidentally wire the wrong adapter;
              3. observe no shadow traffic and no shadow outcomes; and
              4. incorrectly assume that Redis is being validated or warmed before serving ramp increases.

              This is especially risky because shadow mode exists specifically as a controlled rollout and validation mechanism. Silent non-execution defeats that purpose.

              Proposed behavior

              Static/default policy

              When cached() registration or getOrLoad() invocation receives a positive effective static shadow.ramp and the instance metrics adapter has no shadowValidation hook, reject the configuration clearly before any shadow-capable cache execution begins.

              Possible error:

              DialCache shadow.ramp requires metrics.shadowValidation observability
              

              For cached(), fail at registration. For getOrLoad(), fail during its normal enabled-path option/policy validation. Calls outside an enabled scope should retain true pass-through behavior and should not validate cache policy solely to reject shadow configuration.

              If throwing for static getOrLoad() policy conflicts with established fail-open config semantics, the implementation may use the runtime behavior below consistently instead; the decision must be explicit and tested.

              Runtime-provider policy

              A runtime provider can enable shadow after registration. For a positive resolved runtime shadow ramp without metrics.shadowValidation:

              • disable shadow work for that invocation;
              • preserve normal caller cache behavior;
              • record one bounded config_resolution error when an error hook exists;
              • emit a safe warning that identifies only bounded definition metadata such as namespace, use case, and key type;
              • avoid warning once per invocation through per-use-case deduplication or rate limiting.

              Do not synthesize shadow outcomes when no shadow job was admitted.

              Zero or omitted policy

              shadow omitted, shadow.ramp omitted, or shadow.ramp: 0 remains inert and requires no shadow observer.

              Why fail closed

              Running detached source/Redis traffic without verdict observability is operationally unsafe:

              • mismatch rates cannot be measured;
              • fill/error/timeout/drop outcomes are invisible;
              • operators cannot know whether a serving-ramp increase is justified;
              • background load can exist without its intended control signal.

              Disabling only shadow work preserves application correctness and the source-of-truth fallback while preventing invisible rollout traffic.

              Alternative considered

              Run shadow work even without shadowValidation and rely on logging or other metrics.

              Rejected: the primary bounded outcome series is part of the shadow feature's safety contract. Logging every result is unsuitable, and generic request/error metrics cannot represent match, mismatch, superseded, filled, timeout, or dropped outcomes.

              Compatibility

              This changes only misconfigured positive shadow policy:

              • configurations with shadow omitted or zero are unchanged;
              • configurations with a working shadowValidation hook are unchanged;
              • configurations that currently request shadow without the hook move from silent no-op to an explicit configuration failure/diagnostic;
              • normal cache serving remains fail open where runtime policy is malformed or unsupported.

              Because positive policy that previously did nothing may now reject static registration or emit new diagnostics, document the behavior change in release notes.

              Acceptance criteria

              • Positive static/default shadow policy without metrics.shadowValidation cannot remain a silent no-op.
              • The chosen static behavior—registration rejection or explicit fail-closed diagnostic—is consistent across cached() and enabled getOrLoad() and is documented.
              • Positive runtime-provider shadow policy without the hook disables only shadow work and preserves normal caller behavior.
              • Runtime misconfiguration records bounded config_resolution observability where possible.
              • Runtime warnings are deduplicated or rate-limited by bounded definition identity and cannot amplify an incident per invocation.
              • No cache key, ID, source value, cached value, raw error, or other unbounded data is logged or labeled.
              • Omitted and zero shadow policies remain inert without requiring the hook.
              • A metrics adapter with shadowValidation continues to receive exactly one terminal outcome per admitted job.
              • Missing observability does not consume shadow capacity, issue Redis commands, invoke the source, or schedule detached work.
              • Tests cover static defaults, sparse runtime overlays, runtime enable/disable transitions, old custom metrics adapters, logger/metrics failures, and packed ESM/CommonJS consumers.
              • README shadow-rollout guidance states that shadow outcomes are mandatory for positive shadow policy.

              Non-goals

              • Making every optional metrics hook mandatory.
              • Adding a second shadow observer or callback.
              • Running shadow work without verdict observability.
              • Changing shadow ramp sampling, capacity, deadlines, comparisons, fills, or caller behavior.

              Related

              Metadata

              Metadata

              Assignees

              No one assigned

                Labels

                No labels
                No labels

                Projects

                No projects

                  Milestone

                  No milestone

                  Relationships

                  None yet

                  Development

                  No branches or pull requests

                  Issue actions

                  , '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

                  Fail closed when shadow policy is enabled without shadow observability #151

                  Description

                  @lan17

                  Summary

                  Make positive shadow policy visibly fail closed when the configured metrics adapter cannot record shadow outcomes.

                  Today, scheduleShadowValidation() silently returns when metrics.shadowValidation is absent. A use case can therefore resolve a positive shadow.ramp while performing no shadow read, comparison, fill, or outcome observation, with no indication that the rollout policy is ineffective.

                  The intended behavior should remain: do not run detached shadow traffic invisibly. The improvement is to surface the configuration error rather than silently treating the policy as disabled.

                  Current behavior

                  Shadow work requires all of the following:

                  • valid remote policy;
                  • positive shadow ramp;
                  • deterministic ramp admission;
                  • available per-instance shadow capacity; and
                  • metrics.shadowValidation to exist.

                  The observability hook is currently checked after shadow policy validation, and its absence causes an unconditional return. This couples behavior to metrics wiring without a diagnostic.

                  Problem

                  A deployment can:

                  1. configure shadow.ramp > 0 in a static default or runtime provider;
                  2. omit the optional shadowValidation metrics hook, use an older custom adapter, or accidentally wire the wrong adapter;
                  3. observe no shadow traffic and no shadow outcomes; and
                  4. incorrectly assume that Redis is being validated or warmed before serving ramp increases.

                  This is especially risky because shadow mode exists specifically as a controlled rollout and validation mechanism. Silent non-execution defeats that purpose.

                  Proposed behavior

                  Static/default policy

                  When cached() registration or getOrLoad() invocation receives a positive effective static shadow.ramp and the instance metrics adapter has no shadowValidation hook, reject the configuration clearly before any shadow-capable cache execution begins.

                  Possible error:

                  DialCache shadow.ramp requires metrics.shadowValidation observability
                  

                  For cached(), fail at registration. For getOrLoad(), fail during its normal enabled-path option/policy validation. Calls outside an enabled scope should retain true pass-through behavior and should not validate cache policy solely to reject shadow configuration.

                  If throwing for static getOrLoad() policy conflicts with established fail-open config semantics, the implementation may use the runtime behavior below consistently instead; the decision must be explicit and tested.

                  Runtime-provider policy

                  A runtime provider can enable shadow after registration. For a positive resolved runtime shadow ramp without metrics.shadowValidation:

                  • disable shadow work for that invocation;
                  • preserve normal caller cache behavior;
                  • record one bounded config_resolution error when an error hook exists;
                  • emit a safe warning that identifies only bounded definition metadata such as namespace, use case, and key type;
                  • avoid warning once per invocation through per-use-case deduplication or rate limiting.

                  Do not synthesize shadow outcomes when no shadow job was admitted.

                  Zero or omitted policy

                  shadow omitted, shadow.ramp omitted, or shadow.ramp: 0 remains inert and requires no shadow observer.

                  Why fail closed

                  Running detached source/Redis traffic without verdict observability is operationally unsafe:

                  • mismatch rates cannot be measured;
                  • fill/error/timeout/drop outcomes are invisible;
                  • operators cannot know whether a serving-ramp increase is justified;
                  • background load can exist without its intended control signal.

                  Disabling only shadow work preserves application correctness and the source-of-truth fallback while preventing invisible rollout traffic.

                  Alternative considered

                  Run shadow work even without shadowValidation and rely on logging or other metrics.

                  Rejected: the primary bounded outcome series is part of the shadow feature's safety contract. Logging every result is unsuitable, and generic request/error metrics cannot represent match, mismatch, superseded, filled, timeout, or dropped outcomes.

                  Compatibility

                  This changes only misconfigured positive shadow policy:

                  • configurations with shadow omitted or zero are unchanged;
                  • configurations with a working shadowValidation hook are unchanged;
                  • configurations that currently request shadow without the hook move from silent no-op to an explicit configuration failure/diagnostic;
                  • normal cache serving remains fail open where runtime policy is malformed or unsupported.

                  Because positive policy that previously did nothing may now reject static registration or emit new diagnostics, document the behavior change in release notes.

                  Acceptance criteria

                  • Positive static/default shadow policy without metrics.shadowValidation cannot remain a silent no-op.
                  • The chosen static behavior—registration rejection or explicit fail-closed diagnostic—is consistent across cached() and enabled getOrLoad() and is documented.
                  • Positive runtime-provider shadow policy without the hook disables only shadow work and preserves normal caller behavior.
                  • Runtime misconfiguration records bounded config_resolution observability where possible.
                  • Runtime warnings are deduplicated or rate-limited by bounded definition identity and cannot amplify an incident per invocation.
                  • No cache key, ID, source value, cached value, raw error, or other unbounded data is logged or labeled.
                  • Omitted and zero shadow policies remain inert without requiring the hook.
                  • A metrics adapter with shadowValidation continues to receive exactly one terminal outcome per admitted job.
                  • Missing observability does not consume shadow capacity, issue Redis commands, invoke the source, or schedule detached work.
                  • Tests cover static defaults, sparse runtime overlays, runtime enable/disable transitions, old custom metrics adapters, logger/metrics failures, and packed ESM/CommonJS consumers.
                  • README shadow-rollout guidance states that shadow outcomes are mandatory for positive shadow policy.

                  Non-goals

                  • Making every optional metrics hook mandatory.
                  • Adding a second shadow observer or callback.
                  • Running shadow work without verdict observability.
                  • Changing shadow ramp sampling, capacity, deadlines, comparisons, fills, or caller behavior.

                  Related

                  Metadata

                  Metadata

                  Assignees

                  No one assigned

                    Labels

                    No labels
                    No labels

                    Projects

                    No projects

                      Milestone

                      No milestone

                      Relationships

                      None yet

                      Development

                      No branches or pull requests

                      Issue actions

                      , '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

                      Fail closed when shadow policy is enabled without shadow observability #151

                      Description

                      @lan17

                      Summary

                      Make positive shadow policy visibly fail closed when the configured metrics adapter cannot record shadow outcomes.

                      Today, scheduleShadowValidation() silently returns when metrics.shadowValidation is absent. A use case can therefore resolve a positive shadow.ramp while performing no shadow read, comparison, fill, or outcome observation, with no indication that the rollout policy is ineffective.

                      The intended behavior should remain: do not run detached shadow traffic invisibly. The improvement is to surface the configuration error rather than silently treating the policy as disabled.

                      Current behavior

                      Shadow work requires all of the following:

                      • valid remote policy;
                      • positive shadow ramp;
                      • deterministic ramp admission;
                      • available per-instance shadow capacity; and
                      • metrics.shadowValidation to exist.

                      The observability hook is currently checked after shadow policy validation, and its absence causes an unconditional return. This couples behavior to metrics wiring without a diagnostic.

                      Problem

                      A deployment can:

                      1. configure shadow.ramp > 0 in a static default or runtime provider;
                      2. omit the optional shadowValidation metrics hook, use an older custom adapter, or accidentally wire the wrong adapter;
                      3. observe no shadow traffic and no shadow outcomes; and
                      4. incorrectly assume that Redis is being validated or warmed before serving ramp increases.

                      This is especially risky because shadow mode exists specifically as a controlled rollout and validation mechanism. Silent non-execution defeats that purpose.

                      Proposed behavior

                      Static/default policy

                      When cached() registration or getOrLoad() invocation receives a positive effective static shadow.ramp and the instance metrics adapter has no shadowValidation hook, reject the configuration clearly before any shadow-capable cache execution begins.

                      Possible error:

                      DialCache shadow.ramp requires metrics.shadowValidation observability
                      

                      For cached(), fail at registration. For getOrLoad(), fail during its normal enabled-path option/policy validation. Calls outside an enabled scope should retain true pass-through behavior and should not validate cache policy solely to reject shadow configuration.

                      If throwing for static getOrLoad() policy conflicts with established fail-open config semantics, the implementation may use the runtime behavior below consistently instead; the decision must be explicit and tested.

                      Runtime-provider policy

                      A runtime provider can enable shadow after registration. For a positive resolved runtime shadow ramp without metrics.shadowValidation:

                      • disable shadow work for that invocation;
                      • preserve normal caller cache behavior;
                      • record one bounded config_resolution error when an error hook exists;
                      • emit a safe warning that identifies only bounded definition metadata such as namespace, use case, and key type;
                      • avoid warning once per invocation through per-use-case deduplication or rate limiting.

                      Do not synthesize shadow outcomes when no shadow job was admitted.

                      Zero or omitted policy

                      shadow omitted, shadow.ramp omitted, or shadow.ramp: 0 remains inert and requires no shadow observer.

                      Why fail closed

                      Running detached source/Redis traffic without verdict observability is operationally unsafe:

                      • mismatch rates cannot be measured;
                      • fill/error/timeout/drop outcomes are invisible;
                      • operators cannot know whether a serving-ramp increase is justified;
                      • background load can exist without its intended control signal.

                      Disabling only shadow work preserves application correctness and the source-of-truth fallback while preventing invisible rollout traffic.

                      Alternative considered

                      Run shadow work even without shadowValidation and rely on logging or other metrics.

                      Rejected: the primary bounded outcome series is part of the shadow feature's safety contract. Logging every result is unsuitable, and generic request/error metrics cannot represent match, mismatch, superseded, filled, timeout, or dropped outcomes.

                      Compatibility

                      This changes only misconfigured positive shadow policy:

                      • configurations with shadow omitted or zero are unchanged;
                      • configurations with a working shadowValidation hook are unchanged;
                      • configurations that currently request shadow without the hook move from silent no-op to an explicit configuration failure/diagnostic;
                      • normal cache serving remains fail open where runtime policy is malformed or unsupported.

                      Because positive policy that previously did nothing may now reject static registration or emit new diagnostics, document the behavior change in release notes.

                      Acceptance criteria

                      • Positive static/default shadow policy without metrics.shadowValidation cannot remain a silent no-op.
                      • The chosen static behavior—registration rejection or explicit fail-closed diagnostic—is consistent across cached() and enabled getOrLoad() and is documented.
                      • Positive runtime-provider shadow policy without the hook disables only shadow work and preserves normal caller behavior.
                      • Runtime misconfiguration records bounded config_resolution observability where possible.
                      • Runtime warnings are deduplicated or rate-limited by bounded definition identity and cannot amplify an incident per invocation.
                      • No cache key, ID, source value, cached value, raw error, or other unbounded data is logged or labeled.
                      • Omitted and zero shadow policies remain inert without requiring the hook.
                      • A metrics adapter with shadowValidation continues to receive exactly one terminal outcome per admitted job.
                      • Missing observability does not consume shadow capacity, issue Redis commands, invoke the source, or schedule detached work.
                      • Tests cover static defaults, sparse runtime overlays, runtime enable/disable transitions, old custom metrics adapters, logger/metrics failures, and packed ESM/CommonJS consumers.
                      • README shadow-rollout guidance states that shadow outcomes are mandatory for positive shadow policy.

                      Non-goals

                      • Making every optional metrics hook mandatory.
                      • Adding a second shadow observer or callback.
                      • Running shadow work without verdict observability.
                      • Changing shadow ramp sampling, capacity, deadlines, comparisons, fills, or caller behavior.

                      Related

                      Metadata

                      Metadata

                      Assignees

                      No one assigned

                        Labels

                        No labels
                        No labels

                        Projects

                        No projects

                          Milestone

                          No milestone

                          Relationships

                          None yet

                          Development

                          No branches or pull requests

                          Issue actions

                          , '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

                          Fail closed when shadow policy is enabled without shadow observability #151

                          Description

                          @lan17

                          Summary

                          Make positive shadow policy visibly fail closed when the configured metrics adapter cannot record shadow outcomes.

                          Today, scheduleShadowValidation() silently returns when metrics.shadowValidation is absent. A use case can therefore resolve a positive shadow.ramp while performing no shadow read, comparison, fill, or outcome observation, with no indication that the rollout policy is ineffective.

                          The intended behavior should remain: do not run detached shadow traffic invisibly. The improvement is to surface the configuration error rather than silently treating the policy as disabled.

                          Current behavior

                          Shadow work requires all of the following:

                          • valid remote policy;
                          • positive shadow ramp;
                          • deterministic ramp admission;
                          • available per-instance shadow capacity; and
                          • metrics.shadowValidation to exist.

                          The observability hook is currently checked after shadow policy validation, and its absence causes an unconditional return. This couples behavior to metrics wiring without a diagnostic.

                          Problem

                          A deployment can:

                          1. configure shadow.ramp > 0 in a static default or runtime provider;
                          2. omit the optional shadowValidation metrics hook, use an older custom adapter, or accidentally wire the wrong adapter;
                          3. observe no shadow traffic and no shadow outcomes; and
                          4. incorrectly assume that Redis is being validated or warmed before serving ramp increases.

                          This is especially risky because shadow mode exists specifically as a controlled rollout and validation mechanism. Silent non-execution defeats that purpose.

                          Proposed behavior

                          Static/default policy

                          When cached() registration or getOrLoad() invocation receives a positive effective static shadow.ramp and the instance metrics adapter has no shadowValidation hook, reject the configuration clearly before any shadow-capable cache execution begins.

                          Possible error:

                          DialCache shadow.ramp requires metrics.shadowValidation observability
                          

                          For cached(), fail at registration. For getOrLoad(), fail during its normal enabled-path option/policy validation. Calls outside an enabled scope should retain true pass-through behavior and should not validate cache policy solely to reject shadow configuration.

                          If throwing for static getOrLoad() policy conflicts with established fail-open config semantics, the implementation may use the runtime behavior below consistently instead; the decision must be explicit and tested.

                          Runtime-provider policy

                          A runtime provider can enable shadow after registration. For a positive resolved runtime shadow ramp without metrics.shadowValidation:

                          • disable shadow work for that invocation;
                          • preserve normal caller cache behavior;
                          • record one bounded config_resolution error when an error hook exists;
                          • emit a safe warning that identifies only bounded definition metadata such as namespace, use case, and key type;
                          • avoid warning once per invocation through per-use-case deduplication or rate limiting.

                          Do not synthesize shadow outcomes when no shadow job was admitted.

                          Zero or omitted policy

                          shadow omitted, shadow.ramp omitted, or shadow.ramp: 0 remains inert and requires no shadow observer.

                          Why fail closed

                          Running detached source/Redis traffic without verdict observability is operationally unsafe:

                          • mismatch rates cannot be measured;
                          • fill/error/timeout/drop outcomes are invisible;
                          • operators cannot know whether a serving-ramp increase is justified;
                          • background load can exist without its intended control signal.

                          Disabling only shadow work preserves application correctness and the source-of-truth fallback while preventing invisible rollout traffic.

                          Alternative considered

                          Run shadow work even without shadowValidation and rely on logging or other metrics.

                          Rejected: the primary bounded outcome series is part of the shadow feature's safety contract. Logging every result is unsuitable, and generic request/error metrics cannot represent match, mismatch, superseded, filled, timeout, or dropped outcomes.

                          Compatibility

                          This changes only misconfigured positive shadow policy:

                          • configurations with shadow omitted or zero are unchanged;
                          • configurations with a working shadowValidation hook are unchanged;
                          • configurations that currently request shadow without the hook move from silent no-op to an explicit configuration failure/diagnostic;
                          • normal cache serving remains fail open where runtime policy is malformed or unsupported.

                          Because positive policy that previously did nothing may now reject static registration or emit new diagnostics, document the behavior change in release notes.

                          Acceptance criteria

                          • Positive static/default shadow policy without metrics.shadowValidation cannot remain a silent no-op.
                          • The chosen static behavior—registration rejection or explicit fail-closed diagnostic—is consistent across cached() and enabled getOrLoad() and is documented.
                          • Positive runtime-provider shadow policy without the hook disables only shadow work and preserves normal caller behavior.
                          • Runtime misconfiguration records bounded config_resolution observability where possible.
                          • Runtime warnings are deduplicated or rate-limited by bounded definition identity and cannot amplify an incident per invocation.
                          • No cache key, ID, source value, cached value, raw error, or other unbounded data is logged or labeled.
                          • Omitted and zero shadow policies remain inert without requiring the hook.
                          • A metrics adapter with shadowValidation continues to receive exactly one terminal outcome per admitted job.
                          • Missing observability does not consume shadow capacity, issue Redis commands, invoke the source, or schedule detached work.
                          • Tests cover static defaults, sparse runtime overlays, runtime enable/disable transitions, old custom metrics adapters, logger/metrics failures, and packed ESM/CommonJS consumers.
                          • README shadow-rollout guidance states that shadow outcomes are mandatory for positive shadow policy.

                          Non-goals

                          • Making every optional metrics hook mandatory.
                          • Adding a second shadow observer or callback.
                          • Running shadow work without verdict observability.
                          • Changing shadow ramp sampling, capacity, deadlines, comparisons, fills, or caller behavior.

                          Related

                          Metadata

                          Metadata

                          Assignees

                          No one assigned

                            Labels

                            No labels
                            No labels

                            Projects

                            No projects

                              Milestone

                              No milestone

                              Relationships

                              None yet

                              Development

                              No branches or pull requests

                              Issue actions

                              , '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

                              Fail closed when shadow policy is enabled without shadow observability #151

                              Description

                              @lan17

                              Summary

                              Make positive shadow policy visibly fail closed when the configured metrics adapter cannot record shadow outcomes.

                              Today, scheduleShadowValidation() silently returns when metrics.shadowValidation is absent. A use case can therefore resolve a positive shadow.ramp while performing no shadow read, comparison, fill, or outcome observation, with no indication that the rollout policy is ineffective.

                              The intended behavior should remain: do not run detached shadow traffic invisibly. The improvement is to surface the configuration error rather than silently treating the policy as disabled.

                              Current behavior

                              Shadow work requires all of the following:

                              • valid remote policy;
                              • positive shadow ramp;
                              • deterministic ramp admission;
                              • available per-instance shadow capacity; and
                              • metrics.shadowValidation to exist.

                              The observability hook is currently checked after shadow policy validation, and its absence causes an unconditional return. This couples behavior to metrics wiring without a diagnostic.

                              Problem

                              A deployment can:

                              1. configure shadow.ramp > 0 in a static default or runtime provider;
                              2. omit the optional shadowValidation metrics hook, use an older custom adapter, or accidentally wire the wrong adapter;
                              3. observe no shadow traffic and no shadow outcomes; and
                              4. incorrectly assume that Redis is being validated or warmed before serving ramp increases.

                              This is especially risky because shadow mode exists specifically as a controlled rollout and validation mechanism. Silent non-execution defeats that purpose.

                              Proposed behavior

                              Static/default policy

                              When cached() registration or getOrLoad() invocation receives a positive effective static shadow.ramp and the instance metrics adapter has no shadowValidation hook, reject the configuration clearly before any shadow-capable cache execution begins.

                              Possible error:

                              DialCache shadow.ramp requires metrics.shadowValidation observability
                              

                              For cached(), fail at registration. For getOrLoad(), fail during its normal enabled-path option/policy validation. Calls outside an enabled scope should retain true pass-through behavior and should not validate cache policy solely to reject shadow configuration.

                              If throwing for static getOrLoad() policy conflicts with established fail-open config semantics, the implementation may use the runtime behavior below consistently instead; the decision must be explicit and tested.

                              Runtime-provider policy

                              A runtime provider can enable shadow after registration. For a positive resolved runtime shadow ramp without metrics.shadowValidation:

                              • disable shadow work for that invocation;
                              • preserve normal caller cache behavior;
                              • record one bounded config_resolution error when an error hook exists;
                              • emit a safe warning that identifies only bounded definition metadata such as namespace, use case, and key type;
                              • avoid warning once per invocation through per-use-case deduplication or rate limiting.

                              Do not synthesize shadow outcomes when no shadow job was admitted.

                              Zero or omitted policy

                              shadow omitted, shadow.ramp omitted, or shadow.ramp: 0 remains inert and requires no shadow observer.

                              Why fail closed

                              Running detached source/Redis traffic without verdict observability is operationally unsafe:

                              • mismatch rates cannot be measured;
                              • fill/error/timeout/drop outcomes are invisible;
                              • operators cannot know whether a serving-ramp increase is justified;
                              • background load can exist without its intended control signal.

                              Disabling only shadow work preserves application correctness and the source-of-truth fallback while preventing invisible rollout traffic.

                              Alternative considered

                              Run shadow work even without shadowValidation and rely on logging or other metrics.

                              Rejected: the primary bounded outcome series is part of the shadow feature's safety contract. Logging every result is unsuitable, and generic request/error metrics cannot represent match, mismatch, superseded, filled, timeout, or dropped outcomes.

                              Compatibility

                              This changes only misconfigured positive shadow policy:

                              • configurations with shadow omitted or zero are unchanged;
                              • configurations with a working shadowValidation hook are unchanged;
                              • configurations that currently request shadow without the hook move from silent no-op to an explicit configuration failure/diagnostic;
                              • normal cache serving remains fail open where runtime policy is malformed or unsupported.

                              Because positive policy that previously did nothing may now reject static registration or emit new diagnostics, document the behavior change in release notes.

                              Acceptance criteria

                              • Positive static/default shadow policy without metrics.shadowValidation cannot remain a silent no-op.
                              • The chosen static behavior—registration rejection or explicit fail-closed diagnostic—is consistent across cached() and enabled getOrLoad() and is documented.
                              • Positive runtime-provider shadow policy without the hook disables only shadow work and preserves normal caller behavior.
                              • Runtime misconfiguration records bounded config_resolution observability where possible.
                              • Runtime warnings are deduplicated or rate-limited by bounded definition identity and cannot amplify an incident per invocation.
                              • No cache key, ID, source value, cached value, raw error, or other unbounded data is logged or labeled.
                              • Omitted and zero shadow policies remain inert without requiring the hook.
                              • A metrics adapter with shadowValidation continues to receive exactly one terminal outcome per admitted job.
                              • Missing observability does not consume shadow capacity, issue Redis commands, invoke the source, or schedule detached work.
                              • Tests cover static defaults, sparse runtime overlays, runtime enable/disable transitions, old custom metrics adapters, logger/metrics failures, and packed ESM/CommonJS consumers.
                              • README shadow-rollout guidance states that shadow outcomes are mandatory for positive shadow policy.

                              Non-goals

                              • Making every optional metrics hook mandatory.
                              • Adding a second shadow observer or callback.
                              • Running shadow work without verdict observability.
                              • Changing shadow ramp sampling, capacity, deadlines, comparisons, fills, or caller behavior.

                              Related

                              Metadata

                              Metadata

                              Assignees

                              No one assigned

                                Labels

                                No labels
                                No labels

                                Projects

                                No projects

                                  Milestone

                                  No milestone

                                  Relationships

                                  None yet

                                  Development

                                  No branches or pull requests

                                  Issue actions