Summary
Provide a small, transport-agnostic primitive for invalidating matching tracked process-local cache state, while leaving cross-process event delivery entirely application-owned.
This is a narrower alternative to making DialCache own Redis Pub/Sub subscription lifecycle, reconnect health, event codecs, delivery gaps, and cluster fan-out as proposed in #106.
Applications could distribute invalidation events through Redis Pub/Sub, Kafka, NATS, database CDC, an existing internal event bus, or another transport, then apply the event to each local DialCache instance.
Motivation
invalidateRemote(keyType, id, futureBufferMs) advances the Redis watermark but intentionally does not evict process-local or request-local values. A process-local hit stops before Redis and therefore cannot observe that watermark.
Some applications want faster process-local convergence after a mutation but already have an event transport and lifecycle model. DialCache does not need to own that transport to provide the missing local mechanism.
Keeping transport outside core avoids adding:
- a dedicated subscriber connection;
- Redis Pub/Sub or Streams semantics;
- reconnect and resubscribe state;
- at-most-once delivery claims;
- event serialization/versioning;
- namespace channels and ACL requirements;
- Redis Cluster publication fan-out;
- library-owned transport health epochs;
- another adapter-specific lifecycle surface.
The library should provide cache-state mechanics; the application should choose and operate the coherence transport.
Proposed public operation
Name to decide, for example:
dialcache.invalidateProcessLocal("user_id", userId);
or:
dialcache.evictProcessLocal("user_id", userId);
The operation should:
- identify the canonical tracked identity
namespace + keyType + id;
- synchronously remove every matching tracked process-local entry across use cases and argument variants;
- prevent matching in-flight work that began before the invalidation from republishing process-local state after the eviction; and
- return without performing Redis or transport work.
Request-local memoization remains an outer-enabled-scope snapshot and is not centrally enumerable; it is explicitly out of scope.
Application-owned transport example
interface CacheInvalidationEvent {
readonly namespace: string;
readonly keyType: string;
readonly id: string;
}
bus.subscribe("cache-invalidation", (event) => {
if (event.namespace !== CACHE_NAMESPACE) return;
dialcache.invalidateProcessLocal(event.keyType, event.id);
});
await db.commitMutation();
await dialcache.invalidateRemote("user_id", userId, FUTURE_BUFFER_MS);
await bus.publish("cache-invalidation", {
namespace: CACHE_NAMESPACE,
keyType: "user_id",
id: String(userId),
});
The exact commit / remote invalidation / publication ordering remains application-owned and must be documented. The local method does not claim peer acknowledgement or global read-after-write consistency.
Repopulation race
Simple LRU deletion is insufficient by itself:
T1: local miss starts Redis/source work
T2: local invalidation deletes existing entries
T3: T1 completes and republishes the pre-invalidation value locally
The implementation therefore needs a bounded in-process publication permit or generation mechanism.
Possible internal direction:
- work that may publish tracked process-local state captures an identity generation/permit;
- local invalidation advances that identity's generation and deletes matching entries;
- the final synchronous local
set checks that its permit is still current;
- a stale permit suppresses publication but does not change the caller result.
Do not add one timer per identity. Do not retain unbounded generation state. The smallest safe bounded representation should be selected after characterizing active local entries and in-flight publication paths.
An alternative is to mark matching active process flights/publication attempts directly and retain no long-lived identity generation when no matching state exists. The design must also cover coalesce: false, where work may not be represented by the process-flight map.
Semantics
- The operation affects process-local cache state only.
- It does not modify Redis values or watermarks.
- It does not call the source of truth.
- It does not cancel or replace existing caller results.
- It does not split or cancel process coalescing flights.
- Callers may still receive a result from work that began before invalidation; only later process-local publication is suppressed.
- Request-local values remain readable within their existing outer scope.
- The operation is best-effort local convergence, not a distributed consistency protocol.
- Strict cross-pod consistency still requires disabling process-local caching or using an application-owned durable/barrier design.
Internal storage support
LocalCache likely needs focused internal operations such as:
deleteTrackedIdentity(prefix: string): number;
clearTracked(): number;
The first implementation may scan current LRU occupancy rather than maintaining a reverse index. Add a reverse index only if benchmark evidence shows the bounded scan is material.
The operation must use exact identity matching and must not evict:
- adjacent IDs;
- another namespace;
- another key type;
- untracked entries;
- unrelated tracked identities.
Observability
Reuse existing bounded observability where practical:
invalidation({ layer: CacheLayer.LOCAL, ... }) may record one local application;
- failures in injected logger/metrics adapters remain isolated;
- no ID, key, event payload, transport name, or other unbounded value enters metric labels.
The application transport owns delivery, retry, lag, disconnect, and acknowledgement metrics.
Relationship to invalidateRemote
Keep the operations separate initially:
await dialcache.invalidateRemote(...);
dialcache.invalidateProcessLocal(...);
A future convenience method that combines local and remote invalidation can be evaluated only after the standalone semantics are stable. Do not change existing invalidateRemote() behavior implicitly in the first implementation.
Constraints
- No built-in Redis Pub/Sub, Streams, Kafka, NATS, or generic event-bus client.
- No library-created or library-closed transport connections.
- No Redis protocol, frame, watermark, key, Lua, or adapter change.
- No request-local invalidation.
- No per-hit Redis lookup.
- No high-cardinality metric labels.
- No unbounded per-identity local state.
- No claim of all-process acknowledgement or linearizable coherence.
- Preserve current caller-result and process-flight semantics.
Acceptance criteria
Non-goals
- Built-in Pub/Sub coherence.
- Peer delivery or acknowledgement.
- Durable replay.
- Request-local eviction.
- Cancellation or versioning of caller-visible in-flight work.
- Strict global read-after-invalidation consistency.
Related
Summary
Provide a small, transport-agnostic primitive for invalidating matching tracked process-local cache state, while leaving cross-process event delivery entirely application-owned.
This is a narrower alternative to making DialCache own Redis Pub/Sub subscription lifecycle, reconnect health, event codecs, delivery gaps, and cluster fan-out as proposed in #106.
Applications could distribute invalidation events through Redis Pub/Sub, Kafka, NATS, database CDC, an existing internal event bus, or another transport, then apply the event to each local
DialCacheinstance.Motivation
invalidateRemote(keyType, id, futureBufferMs)advances the Redis watermark but intentionally does not evict process-local or request-local values. A process-local hit stops before Redis and therefore cannot observe that watermark.Some applications want faster process-local convergence after a mutation but already have an event transport and lifecycle model. DialCache does not need to own that transport to provide the missing local mechanism.
Keeping transport outside core avoids adding:
The library should provide cache-state mechanics; the application should choose and operate the coherence transport.
Proposed public operation
Name to decide, for example:
or:
The operation should:
namespace + keyType + id;Request-local memoization remains an outer-enabled-scope snapshot and is not centrally enumerable; it is explicitly out of scope.
Application-owned transport example
The exact commit / remote invalidation / publication ordering remains application-owned and must be documented. The local method does not claim peer acknowledgement or global read-after-write consistency.
Repopulation race
Simple LRU deletion is insufficient by itself:
The implementation therefore needs a bounded in-process publication permit or generation mechanism.
Possible internal direction:
setchecks that its permit is still current;Do not add one timer per identity. Do not retain unbounded generation state. The smallest safe bounded representation should be selected after characterizing active local entries and in-flight publication paths.
An alternative is to mark matching active process flights/publication attempts directly and retain no long-lived identity generation when no matching state exists. The design must also cover
coalesce: false, where work may not be represented by the process-flight map.Semantics
Internal storage support
LocalCachelikely needs focused internal operations such as:The first implementation may scan current LRU occupancy rather than maintaining a reverse index. Add a reverse index only if benchmark evidence shows the bounded scan is material.
The operation must use exact identity matching and must not evict:
Observability
Reuse existing bounded observability where practical:
invalidation({ layer: CacheLayer.LOCAL, ... })may record one local application;The application transport owns delivery, retry, lag, disconnect, and acknowledgement metrics.
Relationship to
invalidateRemoteKeep the operations separate initially:
A future convenience method that combines local and remote invalidation can be evaluated only after the standalone semantics are stable. Do not change existing
invalidateRemote()behavior implicitly in the first implementation.Constraints
Acceptance criteria
namespace + keyType + ididentity.coalesce: falsepublication races are covered.Non-goals
Related