Skip to content

Proposal: Add BCAST + PREFIX + NOLOOP tracking modes to Client-Side Caching #4500

@vrubal

Description

@vrubal

Proposal: Add BCAST + PREFIX + NOLOOP tracking modes to Client-Side Caching

Type: Feature proposal
Component: redis.clients.jedis.csc (Client-Side Caching)
Target version: Jedis 8.0.0
Server requirement: Redis 7.4+ with RESP3
Status: Proposal — seeking maintainer feedback before PR


1. Summary

Jedis Client-Side Caching (CSC) currently issues CLIENT TRACKING ON in its
default per-key tracking mode only. The Redis server, however, supports two
additional tracking modifiers that are very useful for production cache designs:

Modifier What it does
BCAST Server broadcasts invalidations for any key matching one of the registered prefixes, instead of remembering per-client read sets. Drastically lower server memory at the cost of more invalidation traffic.
PREFIX Restricts BCAST to the given key prefixes (mandatory companion to BCAST in any non-trivial setup).
NOLOOP Suppresses self-invalidations: a client that mutates a tracked key does not receive an invalidation push for its own write.

This proposal adds first-class, builder-friendly support for these modes to
CacheConfig and the CSC pipeline, in a fully backward-compatible way.

A working reference implementation (with unit tests, integration tests, an
example, and a standalone consumer demo) already exists; this proposal is to
get design alignment with the maintainers before opening the PR.


2. Motivation

Why BCAST?

Default-mode tracking requires the server to remember, per connection, every
key that connection has read. For applications that:

  • hold a small, hot working set (sessions, profile lookups, feature flags),
  • shard caching across many app instances,
  • or have very high read fan-out,

the server-side bookkeeping (the invalidation table) becomes a real cost.
BCAST PREFIX user: flips the model: the server no longer tracks individual
keys per client; it just blasts an invalidation whenever anything matching
user:* is mutated. This trades a small amount of network noise for
significantly lower server memory, and is the recommended mode for most
high-fan-out caches per the
Redis Client-Side Caching docs.

Why NOLOOP?

In any setup where the same connection both reads and writes the cached keys,
the server's default behavior is to invalidate the caller's own write. That
either (a) wastes the cache entry the caller just refreshed, or (b) forces
applications to wrap every write in cache-update logic. NOLOOP is the
standard server-side fix for this, and other clients (Lettuce, redis-py, etc.)
already expose it.

Comparable client support

Client BCAST PREFIX NOLOOP
Lettuce
redis-py
node-redis
Jedis

Jedis is currently the odd one out among first-class Redis clients.


3. Goals / Non-goals

Goals

  1. Let users configure BCAST mode with one or more PREFIX filters via
    CacheConfig.Builder.
  2. Let users enable NOLOOP via CacheConfig.Builder.
  3. Have the resulting CLIENT TRACKING ON … arguments emitted correctly by
    CacheConnection.
  4. Be 100 % backward compatible: existing CacheConfig users see no behavior
    or API change.
  5. Ship unit tests for argument-building plus integration tests against a real
    Redis 7.4+ instance.
  6. Ship a runnable example under io.redis.examples.

Non-goals

  • Implementing OPTIN / OPTOUT modes (they belong in a follow-up; the
    internal hooks added here make them straightforward later).
  • Supporting REDIRECT <client-id> (separate feature; orthogonal).
  • Changing the on-cache eviction policy / sizing semantics.

4. Proposed public API

4.1 CacheConfig.Builder additions

public static class Builder {
    // …existing builder methods…

    /** Enable BCAST tracking mode (CLIENT TRACKING ON BCAST). */
    public Builder bcast();

    /**
     * Set the PREFIX filters used in BCAST mode.
     * Each prefix is emitted as a separate "PREFIX <p>" pair.
     */
    public Builder prefixes(String... prefixes);

    /** Enable NOLOOP — server suppresses self-invalidations. */
    public Builder noloop();
}

Open question for maintainers: naming.
Options considered: bcast() / noloop() (matches Redis CLI keywords) vs
broadcastTracking() / noLoop() (more idiomatic Java / camelCase, matches
Lettuce). I have a slight preference for the CLI-keyword names because they
read identically to the resulting wire protocol, but I'll defer to the
maintainers — happy to expose both forms.

4.2 CacheConfig accessors

public boolean isBroadcastMode();
public String[] getPrefixes();
public boolean isNoLoop();

4.3 No change to the Cache public interface

Note vs current local prototype: the prototype added default methods
isBroadcastMode() / getPrefixes() / isNoLoop() to the Cache
interface. Per the discussion in §6 below, the final PR will keep these
on CacheConfig only and have CacheConnection.buildTrackingArgs(...)
read directly from the config, leaving the Cache interface untouched.

4.4 Example usage

CacheConfig cfg = CacheConfig.builder()
    .bcast()
    .prefixes("user:", "order:")
    .noloop()
    .maxSize(10_000)
    .build();

try (RedisClient client = RedisClient.builder()
        .hostAndPort(new HostAndPort("localhost", 6379))
        .cacheConfig(cfg)
        .build()) {
    // …reads of user:* and order:* are now cached client-side,
    // server pushes invalidations on writes matching those prefixes,
    // self-writes from `client` do not evict the local entry.
}

5. Wire-level behavior

CacheConnection (which owns HELLO 3 + CLIENT TRACKING ON …) is extended
to assemble the arg vector from the CacheConfig:

default mode:                CLIENT TRACKING ON
bcast, no prefix:           CLIENT TRACKING ON BCAST
bcast + prefixes(a, b):      CLIENT TRACKING ON BCAST PREFIX a PREFIX b
bcast + prefixes + noloop:   CLIENT TRACKING ON BCAST PREFIX a PREFIX b NOLOOP
default + noloop:            CLIENT TRACKING ON NOLOOP

Validation

  • prefixes(...) without bcast() → throws IllegalArgumentException at
    build() time (Redis would reject PREFIX without BCAST anyway; failing
    fast in Java gives a friendlier error).
  • Empty / null prefix entries are filtered out with a warning.
  • Calling bcast() against a Redis < 7.4 instance still works (the feature
    has been in Redis for a while), but RESP3 push delivery requires Redis 6+;
    documentation will note both.

6. Implementation notes & design choices

6.1 Backward compatibility

  • All new builder methods are additive; existing CacheConfig callers compile
    and behave identically.
  • AbstractCache and DefaultCache get new constructor overloads, not
    modified ones. The original constructors still exist and still work.
  • CacheFactory uses reflective constructor lookup with fallback chains so
    that user-supplied custom Cache classes that haven't been recompiled
    against the new signatures continue to instantiate.

6.2 Where the tracking flags live

The prototype put the flags both on CacheConfig and on Cache (via
default methods). In the proposed final design they live on CacheConfig
only. Rationale:

  • CacheConnection already has the CacheConfig available.
  • Keeping the Cache interface stable avoids adding default methods to a
    public extension point.
  • Custom Cache implementations don't need to know the tracking mode — that
    concern belongs to the connection layer.

6.3 Error handling

If the server rejects the CLIENT TRACKING … command (e.g. on a Redis
version that doesn't support BCAST), the existing connection-handshake
error path surfaces the failure to the caller; no new exception types added.


7. Testing plan

7.1 Unit tests —

Pure tests against CacheConnection.buildTrackingArgs(CacheConfig). 14 cases
covering every valid combination plus the validation paths:

  • default → [ON]
  • bcast only → [ON, BCAST]
  • bcast + 1 prefix → [ON, BCAST, PREFIX, user:]
  • bcast + N prefixes → emits PREFIX p per entry, in order
  • bcast + noloop → [ON, BCAST, NOLOOP]
  • default + noloop → [ON, NOLOOP]
  • prefixes without bcast → IllegalArgumentException
  • null / empty prefixes filtered out
  • (and order-stability + idempotency cases)

7.2 Integration tests:

Runs against the Docker-managed Redis 7.4 instance from make start version=7.4:

  1. Invalidation on prefixed key from another connection — a side
    Jedis mutates user:1; the cached client observes its user:1 entry
    evicted within the timeout.
  2. No invalidation for non-prefixed key — same setup, mutation is on
    other:1; cached entry remains.
  3. NOLOOP self-write — same connection writes its own cached key; with
    noloop() enabled the entry stays, without it the entry is evicted.

Endpoints will be obtained via the project's existing
HostAndPorts / endpoint-config machinery — no hardcoded localhost:6379.




I'm ready to open the PR once the API shape and validation policy are agreed.



Filed by: @vrubal

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions