You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
P1 — High — bound Redis read latency and avoid outage amplification before broad production adoption.
Confirmed decision
As of 2026-07-23, DialCache will enforce a caller-visible Redis read wait deadline in the core library.
Every DialCache instance configured with Redis must receive an application-selected finite RedisConfig.readTimeoutMs.
Each cached use case may override that default with static CachedOptions.redisReadTimeoutMs.
The core deadline bounds how long DialCache waits for DialCacheRedisClient.read() before failing open to the source fallback.
The deadline is a wait/publication boundary, not a claim that an already queued or dispatched Redis command was canceled.
Redis clients must still use finite native connection, retry, reconnect, offline-queue, dispatch, and response budgets to bound underlying resource lifetime.
This replaces the earlier ownership decision that all Redis deadlines remain entirely caller-client-owned. Client-native budgets remain required, but DialCache now also enforces the per-use-case wait bound visible to its callers.
Problem
DialCache fail-open behavior is currently rejection-driven. A Redis read promise that never settles prevents the source fallback from starting and retains the process flight and its followers.
After an untracked Redis read rejects, DialCache currently runs the fallback and then attempts a Redis write. During an outage, one invocation can therefore pay two failing Redis operations and emit two warnings. A timeout implementation that retained this behavior would immediately expose the request to the same unhealthy client again.
Current v0.8.0 evidence at 7e5ca9a17eaa72adb9d34a1813742928e0d4793d:
Caller-owned Redis boundary with no library deadline:
RedisConfig.readTimeoutMs is required whenever Redis is configured.
CachedOptions.redisReadTimeoutMs is optional; omission inherits the instance value.
Both values must be positive safe integers no greater than 2,147,483,647.
There is no null or unbounded escape hatch. Applications choose the value; DialCache does not invent a universal millisecond default.
The per-use-case value is captured when cached() registers the wrapper, like fallbackTimeoutMs. It is not request input, runtime DialCacheKeyConfig, or cache-key identity.
Validate the option before reserving the use-case name so a corrected registration can retry.
Requiring the instance value is a deliberate pre-1.0 compatibility change and needs an explicit migration note.
Deadline semantics
Start the deadline immediately before invoking the semantic DialCacheRedisClient.read() after the remote layer is enabled and the shared leader reaches Redis.
The measured boundary includes the semantic client promise, including its queue/reconnect/retry/response wait and adapter-level DialCache payload decoding.
It does not include key construction, config-provider resolution, ramp sampling, request/process-local reads, user serializer load, fallback execution, Redis writes, or invalidation.
Allocate one timer per actual shared Redis read leader. Cache followers allocate no timer and inherit the leader's remaining read budget.
Local hits and disabled/ramped-out remote layers allocate no Redis read timer.
Use the existing monotonic deadline behavior: performance.now(), delayed-settlement checks, early-timer re-arming, deterministic cleanup, and a referenced timer so timeout delivery is not silently skipped when it is the only active handle.
If the deadline expires, abort the optional cooperative signal first, deliver one stable timeout failure to the cache chain, and attach fulfillment/rejection handlers to the underlying read so late settlement is consumed.
A late Redis hit is ignored. It cannot populate local cache or suppress the source fallback.
A late Redis rejection produces no unhandled rejection and no second DialCache log/metric event.
fallbackTimeoutMs starts separately only if and when the source fallback begins. The total invocation may therefore include the Redis read budget plus the fallback budget.
Fail-open and publication behavior
Represent an operational remote-read failure explicitly instead of overloading status="disabled", reason="config_error".
For a Redis read timeout or any other Redis read exception:
Record/log one remote cache_read failure for the shared leader.
Run the configured source fallback.
Skip the post-fallback Redis write for both tracked and untracked keys.
For an untracked key, allow process-local population when the local layer was an active miss.
For a tracked key, suppress process-local population because the failed/timed-out tracked read did not establish invalidation-watermark safety.
Let a later invocation start a fresh Redis read and recover naturally.
A normal Redis miss still runs the fallback and writes the result according to the existing policy. A serializer-load rejection remains a refreshable miss and may replace the rejected payload; it is not a Redis read timeout.
Adapter and cancellation contract
Add an optional read context to the semantic boundary, tentatively:
DialCache always enforces its own wait deadline even when a custom adapter ignores the context.
The bundled node-redis adapter should pass the signal through its per-command options so queued work can be removed where node-redis supports it.
Aborting after dispatch does not unsend the command or prove that the server stopped executing it.
The current Valkey GLIDE invokeScript API has no per-invocation timeout/signal option. The adapter preserves the caller-created client's requestTimeout; the core deadline may return earlier while GLIDE continues until its client-level budget settles.
Custom adapters may use the context for cooperative cancellation or a resource-native timeout, but must document queued/dispatched-command behavior.
Observability
Add and export RedisReadTimeoutError with stable useCase and timeoutMs fields and no raw cache key.
Timeouts remain classified by the existing bounded metrics failure site: error="cache_read", in_fallback="false", layer="remote".
Do not add timeout values, exception names, messages, IDs, arguments, or Redis keys to metric labels.
Emit one warning/error metric per timed-out shared leader; followers and late settlement do not duplicate it.
getCoalescingState() continues to describe DialCache's foreground process flights. Client-native telemetry owns visibility into underlying reads that outlive the core wait deadline.
Implementation plan
1. Public API and validation
Add required readTimeoutMs to RedisConfig.
Add optional redisReadTimeoutMs to CachedOptions and resolve it once at wrapper registration.
Add/export RedisReadContext and RedisReadTimeoutError from the root package declarations and ESM/CJS bundles.
Add exact runtime validation and compile-time/package-consumer coverage.
Update release notes with the required Redis configuration migration.
2. Shared monotonic deadline primitive
Extract the existing fallback timer machinery into a small internal deadline helper rather than duplicating timer code.
Preserve the tested fallback semantics unchanged.
Support an optional timeout callback used to abort a Redis read signal.
Consume late fulfillment/rejection and guarantee timer cleanup.
3. Remote-read integration and result modeling
Apply the helper directly around DialCacheRedisClient.read() in the remote cache boundary.
Keep the deadline outside config/ramp/local/serializer/fallback/write/invalidation work.
Introduce an explicit internal remote-read-error result and remove the current config_error overloading.
Ensure the timeout continues through the normal source-fallback path rather than escaping to the caller when fallback succeeds.
4. Failure-aware publication
Skip every post-fallback Redis write after a read exception or timeout.
Preserve process-local population for untracked active local misses.
Suppress process-local publication for tracked read failures/timeouts.
Preserve normal miss, serializer-refresh, tracked-write fencing, and later recovery behavior.
5. Bundled adapters
Pass the cooperative signal to node-redis read and tracked-read script command options.
Verify queue-abort and post-dispatch limitations without claiming server-side cancellation.
Keep GLIDE's caller-owned requestTimeout and Script lifecycle unchanged; document that its current script invocation cannot consume the per-read signal.
Prove custom one-argument DialCacheRedisClient.read implementations remain structurally usable because the context is optional.
6. Documentation and observability
Update the README option table, Redis examples, async-liveness contract, coalescing behavior, and metrics/error documentation.
Distinguish the core wait deadline from client resource deadlines and server-side cancellation.
Explain that the read deadline plus fallbackTimeoutMs still does not create a whole-call or Redis-write deadline.
7. Validation and performance
Run typecheck, unit/coverage, build/declarations, packed ESM/CJS consumers, integration tests, and production dependency audit.
Benchmark one timer/clear pair per remote read leader and verify followers allocate none.
Re-run real standalone Redis, Valkey, and Redis Cluster integration coverage.
Required validation matrix
Instance default and per-use-case override resolve correctly.
Missing, zero, negative, fractional, nonnumeric, unsafe, and over-limit values fail before use-case reservation.
Remote hit and miss before the deadline preserve existing behavior and clean up the timer.
A never-settling read times out, starts one fallback, and clears the process flight after the fallback settles.
Same-key process and request-local followers share one read deadline and one fallback.
A follower joining late receives only the leader's remaining read/fallback lifetime.
A late Redis hit is ignored; a late rejection is consumed without duplicate observability.
A retry after timeout starts a fresh Redis read and can recover.
Timeout/error skips the Redis write for tracked and untracked keys.
Untracked local population is allowed; tracked local population is suppressed.
Normal misses and serializer-load refreshes still write Redis.
Node-redis receives the cooperative signal; abort limitations are documented and tested.
GLIDE continues to use its caller-owned request timeout and adapter lifecycle safely.
Local hits, disabled context, and disabled/ramped-out remote layers allocate no read timer.
Metrics remain bounded and contain no raw keys or timeout values.
No timeout timer or late operation creates an unhandled rejection.
Out of scope
Redis write, invalidation, serializer, config-provider, ramp-sampler, or whole-call deadlines.
Guaranteed cancellation of arbitrary promises or already-dispatched Redis commands.
Per-invocation deadlines or caller cancellation that could let one request control a shared leader.
Priority
P1 — High — bound Redis read latency and avoid outage amplification before broad production adoption.
Confirmed decision
As of 2026-07-23, DialCache will enforce a caller-visible Redis read wait deadline in the core library.
DialCacheinstance configured with Redis must receive an application-selected finiteRedisConfig.readTimeoutMs.CachedOptions.redisReadTimeoutMs.DialCacheRedisClient.read()before failing open to the source fallback.This replaces the earlier ownership decision that all Redis deadlines remain entirely caller-client-owned. Client-native budgets remain required, but DialCache now also enforces the per-use-case wait bound visible to its callers.
Problem
DialCache fail-open behavior is currently rejection-driven. A Redis read promise that never settles prevents the source fallback from starting and retains the process flight and its followers.
After an untracked Redis read rejects, DialCache currently runs the fallback and then attempts a Redis write. During an outage, one invocation can therefore pay two failing Redis operations and emit two warnings. A timeout implementation that retained this behavior would immediately expose the request to the same unhealthy client again.
Current
v0.8.0evidence at7e5ca9a17eaa72adb9d34a1813742928e0d4793d:DialCache/src/redis-client.ts
Lines 75 to 91 in 7e5ca9a
DialCache/src/dialcache.ts
Lines 550 to 568 in 7e5ca9a
DialCache/src/dialcache.ts
Lines 456 to 489 in 7e5ca9a
fallbackTimeoutMsdoes not bound a pending Redis read:DialCache/test/dialcache-liveness.test.ts
Lines 517 to 551 in 7e5ca9a
Public contract
Contract rules:
RedisConfig.readTimeoutMsis required whenever Redis is configured.CachedOptions.redisReadTimeoutMsis optional; omission inherits the instance value.nullor unbounded escape hatch. Applications choose the value; DialCache does not invent a universal millisecond default.cached()registers the wrapper, likefallbackTimeoutMs. It is not request input, runtimeDialCacheKeyConfig, or cache-key identity.Deadline semantics
DialCacheRedisClient.read()after the remote layer is enabled and the shared leader reaches Redis.load, fallback execution, Redis writes, or invalidation.performance.now(), delayed-settlement checks, early-timer re-arming, deterministic cleanup, and a referenced timer so timeout delivery is not silently skipped when it is the only active handle.fallbackTimeoutMsstarts separately only if and when the source fallback begins. The total invocation may therefore include the Redis read budget plus the fallback budget.Fail-open and publication behavior
Represent an operational remote-read failure explicitly instead of overloading
status="disabled", reason="config_error".For a Redis read timeout or any other Redis read exception:
cache_readfailure for the shared leader.A normal Redis miss still runs the fallback and writes the result according to the existing policy. A serializer-load rejection remains a refreshable miss and may replace the rejected payload; it is not a Redis read timeout.
Adapter and cancellation contract
Add an optional read context to the semantic boundary, tentatively:
invokeScriptAPI has no per-invocation timeout/signal option. The adapter preserves the caller-created client'srequestTimeout; the core deadline may return earlier while GLIDE continues until its client-level budget settles.Observability
RedisReadTimeoutErrorwith stableuseCaseandtimeoutMsfields and no raw cache key.error="cache_read",in_fallback="false",layer="remote".getCoalescingState()continues to describe DialCache's foreground process flights. Client-native telemetry owns visibility into underlying reads that outlive the core wait deadline.Implementation plan
1. Public API and validation
readTimeoutMstoRedisConfig.redisReadTimeoutMstoCachedOptionsand resolve it once at wrapper registration.RedisReadContextandRedisReadTimeoutErrorfrom the root package declarations and ESM/CJS bundles.2. Shared monotonic deadline primitive
3. Remote-read integration and result modeling
DialCacheRedisClient.read()in the remote cache boundary.config_erroroverloading.4. Failure-aware publication
5. Bundled adapters
requestTimeoutand Script lifecycle unchanged; document that its current script invocation cannot consume the per-read signal.DialCacheRedisClient.readimplementations remain structurally usable because the context is optional.6. Documentation and observability
fallbackTimeoutMsstill does not create a whole-call or Redis-write deadline.7. Validation and performance
Required validation matrix
Out of scope
Related shipped scope
fallbackTimeoutMs, late-fallback publication suppression, and process-flight observability inv0.6.0.Acceptance criteria