Skip to content

fix: wire the metrics option live — Prometheus module becomes the implementation (LAB-517) - #75

Open
27Bslash6 wants to merge 9 commits into
mainfrom
lab-517-wire-metrics
Open

fix: wire the metrics option live — Prometheus module becomes the implementation (LAB-517)#75
27Bslash6 wants to merge 9 commits into
mainfrom
lab-517-wire-metrics

Conversation

@27Bslash6

@27Bslash6 27Bslash6 commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Closes LAB-517.

Problem

createCache({ metrics }) was accepted by the types and defaulted to true by every production intent preset — then silently ignored. CacheImpl never read it, and the entire Prometheus module (src/metrics/prometheus.ts) was dead code: unexported, referenced only by its own tests. A user who wired Prometheus per the types got no metrics and no error. Found by the LAB-275 cross-SDK audit; this is the LAB-388 pattern as a runtime surface.

What changed

The dead module is now the live implementation (per the issue's AC — wire it, don't reject it):

  • CacheImpl threads metrics through get/set/delete/wrap:
    • cachekit_hits_total{layer=l1|l2} — including the wrap() SWR fast path
    • cachekit_misses_total
    • cachekit_operations_total{operation,status} — recorded per backend attempt (inside retry), the honest reading of an operations counter
    • cachekit_errors_total{error_type}
    • cachekit_operation_duration_seconds{operation}
    • cachekit_l1_entries / cachekit_l1_memory_bytes gauges (updated on every L1 mutation)
    • cachekit_circuit_breaker_state gauge (published after each reliability-stack execution)
  • metrics now accepts boolean | MetricsConfig. Two latent bugs in the module fixed on the way:
    • MetricsConfig.registry was accepted-and-ignored (the same bug class this ticket is about) — now honored.
    • Two caches sharing a prefix+registry used to kill init with prom-client's duplicate-registration error — metrics are now reused via getSingleMetric.
  • L1 hit/miss counters actually increment (live counters in CacheImpl), and the SaaS X-CacheKit-L1-* telemetry metricsProvider is auto-wired from them for CachekitIO config backends. An explicit user-supplied provider still wins.
  • No silent middle: metrics: true without the optional prom-client peer dep reports once through the library logger with an actionable message ("install prom-client or set metrics: false") and degrades to no-ops. Decision per the AC's "throw or warn loudly": warn loudly, because intent presets default metrics: true and throwing would break every existing preset user who never opted in.
  • Pluggable logger hook (setLogger): all ad-hoc console.error sites (cache.ts, background-refresh.ts, redis.ts, redis-channel.ts, metrics fallback) route through it. Default remains console.error — zero behavior change unless opted in.
  • Exports: CacheMetrics, NoopMetrics, createMetrics, MetricsCollector, MetricsConfig, setLogger, CachekitLogger from the package index.

Non-goals (per issue)

OpenTelemetry tracing; Python's enable_prometheus_metrics() DX helper.

Testing

  • 17 new tests: cache.metrics.test.ts drives real prom-client through CacheImpl end-to-end (no mocks on the metrics path) — hits/misses/ops/errors/durations/gauges, wrap() SWR path, shared-registry reuse, custom prefix, disabled-by-default; cachekitio.test.ts covers telemetry header auto-wire + user override + L1-disabled; logger.test.ts covers the hook incl. a live background-refresh failure.
  • Full suite: 573 passed, lint clean, type-check clean, ESM+CJS build clean.

Docs pass (mandatory gate)

  • packages/cachekit/README.md Observability section: full metric list (was missing gauges), missing-peer-dep behavior, setLogger, SaaS telemetry auto-wire.
  • docs.cachekit.io: TS configuration reference gains a metrics row + Observability section — PR follows in cachekit-io/docs.
  • Feature matrix: the Observability section lives on unmerged protocol PR chore(deps): bump actions/upload-artifact from 7.0.0 to 7.0.1 #29 (LAB-275), whose TS cells record this exact bug — commented there with the cells to flip once this merges.

Summary by CodeRabbit

  • New Features
    • Added stampede protection with in-process single-flight and optional cross-process distributed locking.
    • Added Prometheus metrics enhancements (custom registries/prefixes, config via metrics option) plus CachekitIO live L1 telemetry headers.
    • Added configurable library-wide error logging via setLogger, with public metrics exports on Node.
  • Bug Fixes
    • Improved native binding selection/version enforcement and WASI fallback behaviour.
    • Redis, invalidation, and background-refresh errors now report through the configured logger.
    • Improved metrics initialisation/degradation and reduced duplicate-metric registration issues.
  • Documentation
    • Updated README to document stampede protection and metrics behaviour.
  • Tests
    • Added coverage for single-flight, distributed locking, telemetry headers, and metrics.

…lementation (LAB-517)

createCache({ metrics }) was accepted by the types and every intent preset,
then silently ignored: CacheImpl never read it and the entire Prometheus
module was dead code. Silent config no-ops are trust bugs.

- CacheImpl now threads metrics through get/set/delete/wrap: hits (l1/l2),
  misses, per-attempt operation counters, error counters, duration
  histograms, L1 entry/memory gauges, and the circuit-breaker state gauge.
- metrics accepts boolean | MetricsConfig (prefix, defaultLabels, registry,
  onError). The registry option was previously accepted-and-ignored — now
  honored; metrics are reused via getSingleMetric so multiple caches sharing
  a registry no longer kill initialization with duplicate-registration.
- Live L1/L2 hit and miss counters feed the SaaS X-CacheKit-L1-* telemetry
  headers: the CachekitIO metricsProvider is auto-wired from them (an
  explicit user-supplied provider still wins).
- If metrics is enabled without the optional prom-client peer dependency,
  the SDK reports once through the library logger and degrades to no-ops —
  loud, never silent.
- New pluggable logger hook (setLogger): all ad-hoc console.error sites
  (invalidation channel, background refresh, Redis error events, metrics
  fallback) now route through it; default remains console.error.
- CacheMetrics/NoopMetrics/createMetrics/MetricsCollector/MetricsConfig and
  setLogger/CachekitLogger exported from the package index.

Co-authored-by: multica-agent <github@multica.ai>
@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

This PR updates native binding loading, adds in-process and distributed-lock stampede protection, expands metrics configuration and telemetry, introduces reusable Prometheus metrics, and provides pluggable internal error logging.

Changes

Native binding loading

Layer / File(s) Summary
Native binding resolution
packages/cachekit-core-ts/index.js
Updates binding version enforcement to 0.1.2, adjusts platform guards, adds tri-state WASI handling, and chains native-load errors through cause.

Cache protection and observability

Layer / File(s) Summary
Cache configuration and runtime wiring
packages/cachekit/src/types/cache.ts, packages/cachekit/src/constants.ts, packages/cachekit/src/cache.ts, packages/cachekit/src/exports-common.ts, packages/cachekit/src/index.ts, packages/cachekit/src/intents-core.ts
Adds stampede and metrics configuration, runtime hooks, public exports, lock defaults, and lock-capable CachekitIO backend selection.
Single-flight and cache instrumentation
packages/cachekit/src/cache-core.ts, packages/cachekit/src/cache.single-flight.test.ts, packages/cachekit/README.md
Adds live L1 telemetry, centralised operation instrumentation, per-key single-flight, optional distributed-lock coordination, validation, and documentation.
Metrics and backend telemetry
packages/cachekit/src/metrics/prometheus.ts, packages/cachekit/src/metrics/prometheus.test.ts, packages/cachekit/src/cache.metrics.test.ts, packages/cachekit/src/backends/cachekitio.test.ts, packages/cachekit/README.md
Reuses registered metrics, supports no-op degradation, wires L1 telemetry headers, and adds metrics and integration coverage.
Pluggable internal error reporting
packages/cachekit/src/logger.ts, packages/cachekit/src/logger.test.ts, packages/cachekit/src/backends/redis.ts, packages/cachekit/src/invalidation/redis-channel.ts, packages/cachekit/src/cache/background-refresh.ts
Adds configurable error logging and routes Redis, invalidation, background-refresh, and metrics errors through the shared logger.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant CacheImpl
  participant LockableBackend
  participant MetricsCollector
  Caller->>CacheImpl: wrap key
  CacheImpl->>LockableBackend: check and coordinate cold miss
  LockableBackend-->>CacheImpl: cached value or lock result
  CacheImpl->>MetricsCollector: record hit, miss, or failure
  CacheImpl-->>Caller: return value
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly reflects the main metrics wiring and Prometheus implementation change in the PR.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch lab-517-wire-metrics

Comment @coderabbitai help to get the list of available commands.

@27Bslash6 27Bslash6 changed the title LAB-517: wire the metrics option live — Prometheus module becomes the implementation fix: wire the metrics option live — Prometheus module becomes the implementation (LAB-517) Jul 23, 2026
@27Bslash6

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

27Bslash6 and others added 4 commits July 24, 2026 00:37
Expert-panel review found exists() was the only public op left
uninstrumented: L1 hits went uncounted and an L2 backend error degraded
to a silent false with no errors_total trace — invisible in the very
metrics this PR wires live. Root cause: the instrument()-inside /
execute()-outside pairing was copy-pasted per method, so exists() simply
drifted out of the set. Extract it into a single run() helper that every
op routes through, making the omission structurally impossible.

Co-authored-by: multica-agent <github@multica.ai>
…LAB-519)

A cold L1+L2 miss previously executed the wrapped function once per
concurrent caller — N cold callers meant N upstream calls, N L2 GETs
(N billed misses under metered-misses), and N L2 writes.

In-process (always on): concurrent wrap() calls for one cache key share
a single in-flight promise covering the L2 read, compute, and write.
The L2 read must be inside the flight — the GET-miss is the billed
event, so deduping only the compute would still bill N misses.

Cross-process (opt-in, stampede.distributedLock): wires the previously
caller-less LockableBackend capability around the miss path, mirroring
cachekit-py's acquire_lock flow — acquire, double-check L2, compute,
write, release. Contested waiters retry the lock on an interval rather
than polling get() (a poll GET against a still-cold key is itself a
billed miss), then fall through to computing after lockWaitMs: the
lease is best-effort mitigation, never a correctness gate. Lock calls
stay outside the reliability executor so lock failures neither stack
retry latency nor open the circuit breaker for data operations.

Deliberately no general admission-control cap beyond L1's
maxConcurrentRefreshes: on Node's single-threaded event loop concurrent
misses don't compete for threads, single-flight collapses the per-key
herd, and distinct-key floods are bounded by backend timeouts plus the
circuit breaker. Decision recorded on StampedeConfig.

Co-authored-by: multica-agent <github@multica.ai>
Port LAB-519 single-flight + distributed-lock cold path onto the
cache-core.ts extraction from LAB-595 (#78): CacheImpl moved to the
shared core, so the single-flight map, stampede config validation, and
resolveMiss/resolveUnderLock/computeAndStore land there (now shared
with the Workers entrypoint). Backend construction stays in the
runtimes — CacheRuntime.resolveBackend grows an optional stampede
param so the Node runtime can select cachekitioWithLocking for apiKey
configs; the Workers runtime is deliberately untouched (lock-capable
backends remain an explicit construction there). StampedeConfig export
moves to exports-common.ts (platform-neutral surface).
…lab-517-wire-metrics

Stacks LAB-517 on LAB-519 per the LAB-761 merge plan (metrics observe
the cache, so they land on top; when #77 merges this PR's diff
collapses to metrics-only). Ports the metrics wiring onto the
cache-core.ts extraction from LAB-595:

- Instrumentation (run/execute/instrument, hit/miss/L1-stats recording)
  moves into the shared CacheImpl in cache-core.ts, composing with the
  LAB-519 single-flight cold path — followers share one recorded miss.
- cache-core imports metrics/prometheus.js TYPES only (prom-client is
  Node-only and dynamic import still enters esbuild's resolve graph);
  the collector is injected via a new optional CacheRuntime.createMetrics
  hook, with an inline no-op fallback, so Workers degrade to no-ops the
  same way a missing prom-client peer does on Node.
- resolveBackend gains an l1Telemetry getter param so the Node runtime
  auto-wires the SaaS X-CacheKit-L1-* metricsProvider from live
  counters, composed with the LAB-519 lockable-wrapper selection.
- setLogger/CachekitLogger move to exports-common.ts: logger.js is in
  the shared graph (background-refresh, invalidation) and Workers apps
  need the same error sink control. Prometheus exports stay Node-only.
- intents metrics?: boolean | MetricsConfig widening ports to
  intents-core.ts (type-only import, workers-safe).
@27Bslash6

Copy link
Copy Markdown
Contributor Author

Un-dirtied per the LAB-761 merge plan ("#75 rebases on #77"): this branch now stacks on lab-519-cold-miss-single-flight (which itself contains main). The diff will show #77's single-flight changes until #77 merges — review this PR after #77 lands and the diff collapses to metrics-only.

Port notes (main's LAB-595 refactor moved CacheImpl to the shared cache-core.ts):

  • Instrumentation (run/execute/instrument, hit/miss/L1-stats recording) lives in cache-core.ts and composes with single-flight — N cold followers share one recorded miss, matching metered-misses semantics.
  • cache-core imports metrics/prometheus.js types only (prom-client is Node-only; even a dynamic import enters esbuild's resolve graph). The collector is injected via a new optional CacheRuntime.createMetrics hook with a no-op fallback — Workers degrade to no-ops exactly like a missing prom-client peer does on Node.
  • resolveBackend gains an l1Telemetry getter so the Node runtime auto-wires the SaaS X-CacheKit-L1-* metricsProvider from live counters, composed with feat(cache): cold-miss single-flight + opt-in cross-process locking (LAB-519) #77's lockable-wrapper selection.
  • setLogger/CachekitLogger moved to exports-common.ts (logger.js is in the shared graph via background-refresh/invalidation); Prometheus value exports stay Node-only in index.ts.
  • metrics?: boolean | MetricsConfig widening ported to intents-core.ts.

Verified locally: type-check, lint, format, full vitest (588 passed — metrics + single-flight suites together), Workers test lane (54 passed), and check:workers-bundle (no prom-client/node:*/NAPI in the workers graph).

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
packages/cachekit/src/metrics/prometheus.ts (1)

148-214: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

defaultLabels need to be added to each metric’s label set.

record*(), startTimer() and update*() all pass this.defaultLabels into prom-client, but the metrics are initialised with fixed labelNames that don’t include those keys. Any non-empty defaultLabels will make these calls throw at runtime, so no metrics get recorded. If per-instance labels are intended, include Object.keys(this.defaultLabels) in each metric’s labelNames; if not, move them to registry-level default labels.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cachekit/src/metrics/prometheus.ts` around lines 148 - 214, Update
the metric initialization around getOrCreate and each metric definition to
include Object.keys(this.defaultLabels) in every labelNames array, preserving
each metric’s existing labels. Ensure defaultLabels keys are supported by
operationsCounter, hitsCounter, missesCounter, errorsCounter, durationHistogram,
l1EntriesGauge, l1MemoryGauge, and circuitBreakerGauge so record*, startTimer,
and update* calls remain valid.
packages/cachekit/src/cache-core.ts (1)

363-413: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Thread skipL1 through the cold path
wrap() already skips the SWR read, but resolveMiss()/resolveUnderLock() still call getEntry(), and computeAndStore() repopulates L1 unconditionally. Pass options.skipL1 into the cold-path read and suppress the L1 write-back as well, otherwise callers can still see or repopulate L1 after opting out.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cachekit/src/cache-core.ts` around lines 363 - 413, Thread the
skipL1 option through resolveMiss() and resolveUnderLock() into getEntry(), so
cold-path reads bypass L1 when requested. Update computeAndStore() and the
getEntry() L1 population path to suppress write-back under skipL1, while
preserving existing behavior when the option is false.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/cachekit/src/cache-core.ts`:
- Around line 283-320: Guard the metrics error-handling callback used by the
MetricsCollector so exceptions from onError cannot escape or create unhandled
rejections. Update the metrics initialization or configuration around the
recordHit, recordMiss, recordFailure, publishL1Stats, and execute methods to
wrap this.errorHandler invocation in a safe try/catch, preserving best-effort
metric behavior.

---

Outside diff comments:
In `@packages/cachekit/src/cache-core.ts`:
- Around line 363-413: Thread the skipL1 option through resolveMiss() and
resolveUnderLock() into getEntry(), so cold-path reads bypass L1 when requested.
Update computeAndStore() and the getEntry() L1 population path to suppress
write-back under skipL1, while preserving existing behavior when the option is
false.

In `@packages/cachekit/src/metrics/prometheus.ts`:
- Around line 148-214: Update the metric initialization around getOrCreate and
each metric definition to include Object.keys(this.defaultLabels) in every
labelNames array, preserving each metric’s existing labels. Ensure defaultLabels
keys are supported by operationsCounter, hitsCounter, missesCounter,
errorsCounter, durationHistogram, l1EntriesGauge, l1MemoryGauge, and
circuitBreakerGauge so record*, startTimer, and update* calls remain valid.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 283a9dd4-0474-4e5a-a927-8a3e0804b5fd

📥 Commits

Reviewing files that changed from the base of the PR and between d70d225 and a56bf71.

📒 Files selected for processing (20)
  • packages/cachekit-core-ts/index.js
  • packages/cachekit/README.md
  • packages/cachekit/src/backends/cachekitio.test.ts
  • packages/cachekit/src/backends/redis.ts
  • packages/cachekit/src/cache-core.ts
  • packages/cachekit/src/cache.metrics.test.ts
  • packages/cachekit/src/cache.single-flight.test.ts
  • packages/cachekit/src/cache.ts
  • packages/cachekit/src/cache/background-refresh.ts
  • packages/cachekit/src/constants.ts
  • packages/cachekit/src/exports-common.ts
  • packages/cachekit/src/index.test.ts
  • packages/cachekit/src/index.ts
  • packages/cachekit/src/intents-core.ts
  • packages/cachekit/src/invalidation/redis-channel.ts
  • packages/cachekit/src/logger.test.ts
  • packages/cachekit/src/logger.ts
  • packages/cachekit/src/metrics/prometheus.test.ts
  • packages/cachekit/src/metrics/prometheus.ts
  • packages/cachekit/src/types/cache.ts

Comment thread packages/cachekit/src/cache-core.ts
A user-supplied onError that throws escaped handleError() and rejected the
collector's promise, turning the cache layer's fire-and-forget
void this.metrics.*() calls into unhandled rejections. Both call sites
(handleError and the initialize failure path) now invoke the handler through
a guarded helper that reports the handler's own failure via the library
logger — metrics stay best-effort, the never-reject invariant holds.

CodeRabbit-Resolved: cache-core.ts:320:Guard the metrics error han
Co-authored-by: multica-agent <github@multica.ai>
@27Bslash6

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

coderabbitai[bot]
coderabbitai Bot previously approved these changes Jul 24, 2026
@27Bslash6

Copy link
Copy Markdown
Contributor Author

Auto-rebase sweep found this PR conflicted against main in packages/cachekit/README.md and packages/cachekit/src/cache-core.ts (2 hunks). The source conflict overlaps this branch's metrics instrumentation (this.recordHit('l1')) with main's SWR-on-Workers refactor (new swrRequiresWaitUntil field + gated L1 read path) in the same L1-hit code path — resolving correctly requires understanding both refactors together, not a safe mechanical union. Left for a human. Merge attempt aborted, branch untouched.

@27Bslash6

Copy link
Copy Markdown
Contributor Author

Auto-rebase attempted a merge of origin/main. Two files conflict:

  • packages/cachekit/README.md — doc conflict, not evaluated further since the code conflict below already requires a human call.
  • packages/cachekit/src/cache-core.ts — this branch adds this.recordHit('l1'); return swrResult.value as TResult; immediately after the swrResult.value !== null guard fails (i.e. on an L1 miss), while origin/main has nothing there. Keeping this branch's lines would record a hit metric on a miss and skip the cold-path single-flight logic below it — that's a real behavior question, not a textual union, so left for a human call. A second conflict adds a metrics/telemetry field block (this branch) alongside a swrRequiresWaitUntil field (origin/main) — that one looks like a safe union but wasn't applied standalone since the file has an unresolved second conflict.

@27Bslash6

Copy link
Copy Markdown
Contributor Author

Auto-rebase: conflicts in packages/cachekit/README.md and packages/cachekit/src/cache-core.ts — both add distinct fields/logic to the same class location (metrics/telemetry counters on this branch vs swrRequiresWaitUntil on main, plus a hit-recording call this branch adds that main's side doesn't have). Resolving requires understanding how the two features interact, not a pure non-overlapping union — needs a human call.

@27Bslash6

Copy link
Copy Markdown
Contributor Author

Auto-rebase: still conflicted against main in packages/cachekit/README.md, packages/cachekit/src/cache-core.ts, and (new this round) packages/cachekit/src/intents-core.ts.

The new conflict is structural, not textual: main changed ProductionOptions/SecureOptions from interface extends BaseIntentOptions to type = BaseIntentOptions & IntentBackendOptions & {...} (adding backend/URL selection), while this branch independently extended the same interfaces with metrics?: boolean | MetricsConfig. Taking either side drops the other's feature — this needs a human merge of the two option-type designs, not a mechanical union. Same call as prior rounds on the cache-core.ts metrics-vs-SWR conflict. Left for a human. Merge attempt aborted, branch untouched.

@27Bslash6

Copy link
Copy Markdown
Contributor Author

Auto-rebase attempted: merging origin/main produced conflicts in 3 files. README.md (both sides added distinct new sections — safe union) was fine, but src/cache-core.ts and src/intents-core.ts are real logic conflicts, not textual unions:

  • cache-core.ts: HEAD's SWR L1-hit branch (this.recordHit('l1'); return swrResult.value as TResult;) sits in a code path that, after main's swrRequiresWaitUntil restructuring, appears reachable only when swrResult.value === null — keeping it as-is risks recording a hit on a miss and returning null instead of falling through to compute.
  • intents-core.ts: ProductionOptions/SecureOptions were independently restructured on both sides — HEAD added metrics?: boolean | MetricsConfig (Prometheus) to the old Redis-only shape, main replaced that shape with a new IntentBackendOptions union (backend abstraction). Reconciling these needs someone who knows which structural direction is current, not a mechanical merge.

Left for a human to reconcile. Merge aborted, branch untouched.

@27Bslash6

Copy link
Copy Markdown
Contributor Author

Conflicts with main in packages/cachekit/src/cache-core.ts, packages/cachekit/src/intents-core.ts (3 separate hunks in core intent-preset logic), and a large block in packages/cachekit/README.md. These are genuine competing edits to intent-preset construction/metrics wiring, not formatting or additive-only changes — needs a human call on how the two feature sets should compose.

Conflicts resolved:
- cache-core.ts: keep both new fields (metrics/telemetry + swrRequiresWaitUntil);
  re-seat recordHit('l1') inside the restructured SWR-hit return (LAB-751) and
  add it to the new no-waitUntil plain L1 read — both paths are L1 hits.
- intents-core.ts: main's IntentBackendOptions shape + this branch's
  metrics: boolean | MetricsConfig union and doc comment (Production/Secure;
  IO merged clean with the union intact).
- README.md: keep both new sections (Stampede Protection + Backends).
@kodus-27b

This comment has been minimized.

Comment thread packages/cachekit/src/backends/cachekitio.test.ts Outdated
Comment thread packages/cachekit/src/cache.metrics.test.ts Outdated
- cache.metrics.test.ts: drop the wholesale 'as unknown as' reshape of
  getMetricsAsJSON() — use prom-client's declared types end to end,
  narrowing only the undocumented per-sample metricName field that
  prom-client emits at runtime for histogram sub-series but omits from
  MetricValue. The circuit-breaker gauge check needs no cast at all.
- cachekitio.test.ts: hoist the fixture credential into FAKE_API_KEY
  ('ck_test_fake-not-a-secret', detect-secrets allowlisted) so the value
  is unmistakably fake; an env-sourced key would add a hidden test
  dependency for zero security gain, so deliberately not process.env.
- background-refresh.ts: route the waitUntil-registration failure log
  (arrived from main, LAB-750 era) through logError like every other
  internal error site — keeps this PR's setLogger invariant true.

Kody-Resolved: cachekitio.test.ts:380:Hardcoded-looking API key
Kody-Resolved: cache.metrics.test.ts:61:Double type assertion
@kodus-27b

kodus-27b Bot commented Jul 28, 2026

Copy link
Copy Markdown

Kody Review Complete

Great news! 🎉
No issues were found that match your current review configurations.

Keep up the excellent work! 🚀

Kody Guide: Usage and Configuration
Interacting with Kody
  • Request a Review: Ask Kody to review your PR manually by adding a comment with the @kody start-review command at the root of your PR.

  • Validate Business Logic: Ask Kody to validate your code against business rules by adding a comment with the @kody -v business-logic command.

  • Provide Feedback: Help Kody learn and improve by reacting to its comments with a 👍 for helpful suggestions or a 👎 if improvements are needed.

Current Kody Configuration
Review Options

The following review options are enabled or disabled:

Options Enabled
Bug
Performance
Security
Business Logic

Access your configuration settings here.

coderabbitai[bot]
coderabbitai Bot previously approved these changes Jul 28, 2026
@27Bslash6

Copy link
Copy Markdown
Contributor Author

@kody start-review

kodus-27b[bot]
kodus-27b Bot previously approved these changes Jul 28, 2026
@27Bslash6

Copy link
Copy Markdown
Contributor Author

Auto-rebased onto main: only conflict was the `generated_at` timestamp in `.secrets.baseline` (cosmetic scan metadata, secret entries auto-merged clean); Cargo.lock/Cargo.toml and test fixtures auto-merged with no marker conflicts. Lint + format-check + type-check pass. CI will re-run.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
packages/cachekit/src/cache-core.ts (2)

526-538: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Record L2 outcomes for exists().

When L1 misses, the backend result bypasses both recordHit('l2') and recordMiss(). L2-only existence checks therefore under-report hit/miss counters and SaaS L1 telemetry.

Proposed fix
-    return this.run('exists', false, () => this.backend.exists(key));
+    return this.run('exists', false, async () => {
+      const exists = await this.backend.exists(key);
+      if (exists) this.recordHit('l2');
+      else this.recordMiss();
+      return exists;
+    });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cachekit/src/cache-core.ts` around lines 526 - 538, Update exists()
so that after the L1 lookup misses, the backend existence result records
recordHit('l2') when true and recordMiss() when false before returning it.
Preserve the existing L1 hit handling and use the existing telemetry methods
rather than adding new counters.

493-501: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Protect L2 from stale refresh writes before relying on marker expiry.

L1 version tokens do not protect the unconditional L2 write, so overlapping or slower refreshes can overwrite newer data and later repopulate stale L1 values.

  • packages/cachekit/src/cache-core.ts#L493-L501: replace the unconditional refresh write with a conditional/versioned write or serialise it with explicit writes.
  • packages/cachekit/src/cache-core.ts#L799-L812: ensure computeAndStore uses that protected write path.
  • packages/cachekit/src/constants.ts#L152-L161: revise the “benign duplicate” guarantee or retain markers until refresh completion.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cachekit/src/cache-core.ts` around lines 493 - 501, Protect L2 from
stale refresh writes by replacing the unconditional backend.set path near
cache-core.ts:493-501 with a conditional/versioned write or serialization with
explicit writes, while preserving L1 version-token behavior. Update
computeAndStore near cache-core.ts:799-812 to use the protected write path, and
revise the benign-duplicate guarantee in constants.ts:152-161 or retain markers
through refresh completion so stale refreshes cannot repopulate L1.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/cachekit/src/metrics/prometheus.ts`:
- Around line 240-258: Make the shared logError invocation non-throwing so
user-provided logger failures cannot escape metrics error handling or violate
the collector’s no-rejection guarantee; update the error-reporting path around
invokeErrorHandler and handleError in
packages/cachekit/src/metrics/prometheus.ts (lines 240-258). In
packages/cachekit/src/cache/background-refresh.ts (lines 87-116), ensure refresh
cleanup, including cancelRefresh, still executes when error reporting fails, and
verify async paths produce no unhandled promise rejections.

---

Outside diff comments:
In `@packages/cachekit/src/cache-core.ts`:
- Around line 526-538: Update exists() so that after the L1 lookup misses, the
backend existence result records recordHit('l2') when true and recordMiss() when
false before returning it. Preserve the existing L1 hit handling and use the
existing telemetry methods rather than adding new counters.
- Around line 493-501: Protect L2 from stale refresh writes by replacing the
unconditional backend.set path near cache-core.ts:493-501 with a
conditional/versioned write or serialization with explicit writes, while
preserving L1 version-token behavior. Update computeAndStore near
cache-core.ts:799-812 to use the protected write path, and revise the
benign-duplicate guarantee in constants.ts:152-161 or retain markers through
refresh completion so stale refreshes cannot repopulate L1.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: e9926f5a-f3b6-475e-ab7e-d89d40ab898a

📥 Commits

Reviewing files that changed from the base of the PR and between a56bf71 and eaf0f9e.

📒 Files selected for processing (11)
  • .secrets.baseline
  • packages/cachekit/README.md
  • packages/cachekit/src/backends/cachekitio.test.ts
  • packages/cachekit/src/cache-core.ts
  • packages/cachekit/src/cache.metrics.test.ts
  • packages/cachekit/src/cache/background-refresh.ts
  • packages/cachekit/src/constants.ts
  • packages/cachekit/src/index.ts
  • packages/cachekit/src/intents-core.ts
  • packages/cachekit/src/metrics/prometheus.test.ts
  • packages/cachekit/src/metrics/prometheus.ts

Comment on lines +240 to +258
private invokeErrorHandler(err: Error): boolean {
if (!this.errorHandler) return false;
try {
this.errorHandler(err);
} catch (handlerError) {
logError('[cachekit] metrics onError handler threw:', handlerError);
}
return true;
}

/**
* Handle errors from async metric operations.
* m5 Fix: Proper error handling instead of silent failures.
*/
private handleError(error: unknown, context: string): void {
const err = error instanceof Error ? error : new Error(String(error));

if (this.errorHandler) {
this.errorHandler(err);
} else {
// eslint-disable-next-line no-console
console.error(`[cachekit] Metrics error (${context}):`, err.message);
if (!this.invokeErrorHandler(err)) {
logError(`[cachekit] Metrics error (${context}):`, err.message);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Make the shared logger non-throwing.

logError directly calls the active, user-pluggable logger. If that logger throws, metrics methods reject despite their fire-and-forget contract; background refreshes reject and skip cancelRefresh, leaving the L1 refresh marker until expiry. Guard logger invocation centrally.

  • packages/cachekit/src/metrics/prometheus.ts#L240-L258: preserve the collector’s no-rejection guarantee when error reporting itself fails.
  • packages/cachekit/src/cache/background-refresh.ts#L87-L116: ensure refresh cleanup still runs when reporting an error fails.

As per path instructions, “Verify async error handling — no unhandled promise rejections.”

📍 Affects 2 files
  • packages/cachekit/src/metrics/prometheus.ts#L240-L258 (this comment)
  • packages/cachekit/src/cache/background-refresh.ts#L87-L116
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cachekit/src/metrics/prometheus.ts` around lines 240 - 258, Make the
shared logError invocation non-throwing so user-provided logger failures cannot
escape metrics error handling or violate the collector’s no-rejection guarantee;
update the error-reporting path around invokeErrorHandler and handleError in
packages/cachekit/src/metrics/prometheus.ts (lines 240-258). In
packages/cachekit/src/cache/background-refresh.ts (lines 87-116), ensure refresh
cleanup, including cancelRefresh, still executes when error reporting fails, and
verify async paths produce no unhandled promise rejections.

Source: Path instructions

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant