Skip to content

Add batched remote invalidation with cluster-aware adapter routing #46

Description

@lan17

Priority

P3 — Low — later or demand-driven enhancement.

Problem

Remote invalidation is scalar at both the public API and semantic Redis boundary. Applications invalidating many independent identities therefore make one semantic call and normally one client/script submission per identity.

Implementation is in #114.

Confirmed API

interface RemoteInvalidationTarget {
  readonly keyType: string;
  readonly id: string | number | bigint;
}

await dialcache.invalidateRemoteMany(
  [
    { keyType: "user_id", id: "123" },
    { keyType: "organization_id", id: "456" },
  ],
  FUTURE_BUFFER_MS,
);
  • Inputs are explicit { keyType, id } pairs, not parallel arrays or a cross product.
  • One shared futureBufferMs applies to the batch.
  • IDs canonicalize with String(id) and duplicate canonical pairs collapse in first-occurrence order.
  • The shared buffer and every target are validated before dispatch. An empty list is a no-op after buffer validation.
  • This is one non-atomic semantic operation, not a cross-key transaction or a throughput guarantee.
  • The scalar invalidateRemote API, watermark protocol, key format, remote-only behavior, and application-owned future-buffer contract remain unchanged.

Semantic Redis boundary

The semantic client gains an optional capability:

invalidateMany?(
  requests: readonly RedisInvalidationRequest[],
): Awaitable<void>;

Core submits one semantic adapter call when the capability exists. Existing custom DialCacheRedisClient implementations remain compatible through an all-settled scalar invalidate() fallback that launches every target concurrently; custom adapters own any additional dispatch limits. Cluster topology, slot calculation, native batch APIs, chunking, and client-specific errors stay inside adapters.

Standalone and Cluster execution

A literal Redis MULTI/EXEC cannot cover arbitrary Cluster slots. Every tracked (namespace, keyType, id) receives its own hash tag so the value and watermark colocate, while different identities normally occupy different slots. Each bundled adapter therefore uses a non-atomic chunking strategy.

node-redis 4.7

  • Use a private fixed ceiling of 1,000 invalidation commands per execution chunk; no public setting is added.
  • Standalone Redis submits ceil(targets / 1,000) sequential pipelines.
  • Calculate each watermark key slot with a private Redis-spec CRC16/XMODEM helper.
  • Snapshot the current node-redis slot table and group mapped requests by slots[slot].master.id.
  • Run targeted primary-owner groups concurrently and each owner's first-key-routed pipeline chunks sequentially. node-redis remains responsible for selecting the node connection.
  • The Cluster pipeline count is sum(ceil(ownerTargets / 1,000)); it equals the number of targeted primaries when every owner has at most 1,000 targets.
  • Route unmapped slots through registered scalar scripts in sequential windows of at most 1,000 concurrent calls rather than isolated one-command pipelines.
  • If an owner chunk ultimately fails with a direct MOVED or ASK, retry only that chunk through independently routed scalar commands. Preserve all non-redirection errors unchanged.
  • Scalar recovery starts only after node-redis exhausts its configured maxCommandRedirections budget, 16 retries by default.
  • If a narrow structural wrapper has no multi, use the bounded scalar compatibility path.
  • The ceiling bounds DialCache's queued contribution per chunk, but cannot guarantee admission with a lower or already-occupied caller commandsQueueMaxLength.
  • Retain the standalone pipeline for bounded dispatch, reply-count validation, and efficient cold-script recovery. Warm concurrent scalar calls are already auto-pipelined, so this is not a general steady-state throughput claim.

Valkey GLIDE

  • Apply a private fixed ceiling of 1,000 commands to native Batch(false) / ClusterBatch(false) dispatch and scalar-only compatibility wrappers.
  • Submit native chunks sequentially and let GLIDE route each non-atomic Cluster chunk across its target nodes.
  • Submit scalar-wrapper calls in sequential windows of at most 1,000 concurrent invocations and settle every launched call in a window.
  • Use EVALSHA when the script handle exposes getHash().
  • After a confirmed script-cache miss, retry only the affected idempotent chunk once with EVAL; unrelated errors are unchanged.
  • Enable documented retriable server/connection behavior only on the detected native Cluster path.
  • A failed native chunk or scalar window prevents later chunks from starting.
  • Retain scalar fallback for narrow wrappers without native batch capabilities.
  • The ceiling bounds DialCache's contribution but cannot guarantee scalar-path admission against a lower or already-occupied GLIDE in-flight request limit.
  • Treat the observed GLIDE 2.4.2 script-miss message as an intentional version-coupled matcher and fail closed on unrelated formats.

No public Cluster abstraction, topology option, new slot dependency, or public pipeline-size configuration is introduced.

Failure and observability semantics

  • Derive all watermark requests before Redis dispatch.
  • Emit one invalidation metric per attempted unique canonical target.
  • On failure, log once and emit one bounded error per distinct targeted key type.
  • Start independent node-redis primary-owner and unmapped-scalar groups concurrently.
  • Process bundled-adapter chunks within one execution group sequentially. A failed chunk prevents later chunks in that group from being submitted, while every already-started node-redis group settles before rejection.
  • Settle every launched scalar call in core's custom-adapter fallback and GLIDE's current scalar window.
  • Preserve one failure identity; aggregate multiple failures deterministically.
  • Cross-primary, native-batch, and scalar-window execution can completely, partially, or ambiguously apply before rejection. There is no rollback.
  • Retrying the canonical target list is safe because each script advances its watermark monotonically.

Acceptance criteria

  • Add the typed batch API while retaining scalar invalidateRemote.
  • Preserve the scalar watermark protocol and key format.
  • Use an optional semantic adapter batch capability with scalar custom-adapter fallback.
  • Bound node-redis pipeline and scalar execution chunks to 1,000 commands.
  • Bound GLIDE native batches and scalar-wrapper windows to 1,000 commands.
  • Keep topology and chunking logic inside adapters.
  • Group node-redis Cluster requests by targeted current primary and retain client-selected routing.
  • Run primary-owner groups concurrently and chunks within an owner sequentially.
  • Recover stale-topology MOVED/ASK failures through bounded per-key routing.
  • Use registered scalar routing for unmapped Cluster slots.
  • Use GLIDE native cross-slot batching with per-chunk warm EVALSHA and cold EVAL recovery.
  • Verify slot calculation against live CLUSTER KEYSLOT, including hash tags and UTF-8.
  • Cover validation, canonical deduplication, metrics, reply validation, partial failures, all-settled rejection, chunk boundaries, and narrow-wrapper compatibility.
  • Extend the shared three-primary Redis 7 fixture for both node-redis and GLIDE Cluster.
  • Prove two distinct slots on one primary share one node-redis pipeline and another primary creates the second pipeline.
  • Preserve all-primary script-cache recovery, watermark advancement, tracked refresh, and no CROSSSLOT errors.
  • Benchmark node-redis and Valkey GLIDE at 10, 100, and 1,000 targets with repeated alternating samples, semantic assertions, and no timing threshold.
  • Document non-atomic completion, retry safety, custom-adapter fallback, chunk failure semantics, adapter-dependent dispatch, and Cluster routing.

Out of scope

  • Batch reads, loaders, misses, ordering, serializers, cache layers, and cache-definition handles.
  • Public topology or routing configuration.
  • A public or caller-configurable batch-size policy.
  • Imposing a new bounded concurrency policy on third-party custom adapters.
  • A new Redis slot dependency.
  • A separate Cluster benchmark fixture.
  • Coordinated process-local eviction from Add opt-in Redis Pub/Sub coherence for tracked process-local caching #106.

Validation snapshot

At PR head c8869e4 on Node.js 22.22.0:

  • 449 unit tests passed with 97.17% statement coverage.
  • Build, declarations, and packed-consumer tests passed.
  • 99 live integration tests passed across Redis 6.2, Valkey 8, node-redis, GLIDE, and a three-primary Redis 7 Cluster.
  • Repeated node-redis and Valkey GLIDE standalone benchmark watermark assertions passed for 10, 100, and 1,000 targets; timings remain directional and carry no threshold.

Related work

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