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
- Let users configure
BCAST mode with one or more PREFIX filters via
CacheConfig.Builder.
- Let users enable
NOLOOP via CacheConfig.Builder.
- Have the resulting
CLIENT TRACKING ON … arguments emitted correctly by
CacheConnection.
- Be 100 % backward compatible: existing
CacheConfig users see no behavior
or API change.
- Ship unit tests for argument-building plus integration tests against a real
Redis 7.4+ instance.
- 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:
- 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.
- No invalidation for non-prefixed key — same setup, mutation is on
other:1; cached entry remains.
- 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
Proposal: Add BCAST + PREFIX + NOLOOP tracking modes to Client-Side Caching
1. Summary
Jedis Client-Side Caching (CSC) currently issues
CLIENT TRACKING ONin itsdefault per-key tracking mode only. The Redis server, however, supports two
additional tracking modifiers that are very useful for production cache designs:
BCASTPREFIXBCASTin any non-trivial setup).NOLOOPThis proposal adds first-class, builder-friendly support for these modes to
CacheConfigand 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:
the server-side bookkeeping (the invalidation table) becomes a real cost.
BCAST PREFIX user:flips the model: the server no longer tracks individualkeys per client; it just blasts an invalidation whenever anything matching
user:*is mutated. This trades a small amount of network noise forsignificantly 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.
NOLOOPis thestandard server-side fix for this, and other clients (Lettuce, redis-py, etc.)
already expose it.
Comparable client support
Jedis is currently the odd one out among first-class Redis clients.
3. Goals / Non-goals
Goals
BCASTmode with one or morePREFIXfilters viaCacheConfig.Builder.NOLOOPviaCacheConfig.Builder.CLIENT TRACKING ON …arguments emitted correctly byCacheConnection.CacheConfigusers see no behavioror API change.
Redis 7.4+ instance.
io.redis.examples.Non-goals
OPTIN/OPTOUTmodes (they belong in a follow-up; theinternal hooks added here make them straightforward later).
REDIRECT <client-id>(separate feature; orthogonal).4. Proposed public API
4.1
CacheConfig.Builderadditions4.2
CacheConfigaccessors4.3 No change to the
Cachepublic interface4.4 Example usage
5. Wire-level behavior
CacheConnection(which ownsHELLO 3+CLIENT TRACKING ON …) is extendedto assemble the arg vector from the
CacheConfig:Validation
prefixes(...)withoutbcast()→ throwsIllegalArgumentExceptionatbuild()time (Redis would rejectPREFIXwithoutBCASTanyway; failingfast in Java gives a friendlier error).
bcast()against a Redis < 7.4 instance still works (the featurehas 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
CacheConfigcallers compileand behave identically.
AbstractCacheandDefaultCacheget new constructor overloads, notmodified ones. The original constructors still exist and still work.
CacheFactoryuses reflective constructor lookup with fallback chains sothat user-supplied custom
Cacheclasses that haven't been recompiledagainst the new signatures continue to instantiate.
6.2 Where the tracking flags live
The prototype put the flags both on
CacheConfigand onCache(viadefault methods). In the proposed final design they live on
CacheConfigonly. Rationale:
CacheConnectionalready has theCacheConfigavailable.Cacheinterface stable avoids adding default methods to apublic extension point.
Cacheimplementations don't need to know the tracking mode — thatconcern belongs to the connection layer.
6.3 Error handling
If the server rejects the
CLIENT TRACKING …command (e.g. on a Redisversion that doesn't support
BCAST), the existing connection-handshakeerror 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 casescovering every valid combination plus the validation paths:
[ON][ON, BCAST][ON, BCAST, PREFIX, user:]PREFIX pper entry, in order[ON, BCAST, NOLOOP][ON, NOLOOP]IllegalArgumentException7.2 Integration tests:
Runs against the Docker-managed Redis 7.4 instance from
make start version=7.4:Jedismutatesuser:1; the cached client observes itsuser:1entryevicted within the timeout.
other:1; cached entry remains.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 hardcodedlocalhost:6379.I'm ready to open the PR once the API shape and validation policy are agreed.
Filed by: @vrubal