Skip to content

DOC-7226 Move rate-limiter, leaderboard, session-store and cache-aside Jedis use cases to RedisClient - #4388

Merged
andy-stark-redis merged 1 commit into
mainfrom
DOC-7226-jedis-pool-a
Oct 30, 2026
Merged

andy-stark-redis merged 1 commit into
mainfrom
DOC-7226-jedis-pool-a

Conversation

@andy-stark-redis

@andy-stark-redis andy-stark-redis commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

Moves four java-jedis use cases from JedisPool to RedisClient (Jedis 8.0.2): code, snippets, versions and run commands.

  • Every multi()/pipelined() is now in try-with-resources, because each borrows its own pooled connection on a RedisClient.
  • cache-aside updateField needed a redesign: UnifiedJedis has no watch(), so it uses transaction(false) and reads existence through the same transaction. Reading through the client deadlocked the default pool of 8 under load.
  • Page run commands list the six jars Jedis 8.0.2 needs at runtime. The old jar-only lines could not run.

Verified: 0 deprecation warnings; demos run against Redis 8.10; 300 concurrent increments exact; updateField contention test shows no orphan hashes at pool sizes 64, 8 and 2 (a no-WATCH control does); page commands run in a clean directory. Not run: Hugo build.

Part of DOC-7226.

🤖 Generated with Claude Code


Note

Medium Risk
Connection lifecycle and WATCH/MULTI usage changed across demos; cache-aside field updates were redesigned to avoid pool deadlocks under contention.

Overview
Updates the cache-aside, leaderboard, rate-limiter, and session-store Java Jedis examples to Jedis 8.0.2 and RedisClient instead of JedisPool / per-request Jedis borrows. Demo servers connect with RedisClient.create() and ping(), pass a shared client into helpers, and close() on shutdown.

Redis access is simplified to calls on the pooled client; multi(), pipelined(), and WATCH flows use AbstractTransaction / Pipeline in try-with-resources so borrowed connections are released. Docs and compile/run snippets now list the full Jedis 8 runtime classpath (not a single jedis.jar).

The notable behavior change is cache-aside updateField: it uses jedis.transaction(false) with watch and exists on the same transaction connection, avoiding pool deadlocks when a held transaction would otherwise trigger a second connection from the client.

Medium risk: connection-pooling semantics changed; cache-aside optimistic updates were reworked for RedisClient APIs.

Reviewed by Cursor Bugbot for commit 129008f. Bugbot is set up for automated code reviews on this repo. Configure here.

Rate limiter, leaderboard, session store and cache-aside (java-jedis) now take a
RedisClient instead of a JedisPool and call commands on it directly. The
getResource() try-with-resources blocks are gone, the demo servers build the
client with RedisClient.create(host, port) and check it with ping(), and the
pages' snippets, prose and Maven/Gradle versions move to Jedis 8.0.2. Each
page's javac/java commands now list the six jars Jedis 8.0.2 needs at runtime.

With a pool, jedis.multi() and jedis.pipelined() ran on the connection the
try block already held. On a RedisClient each one borrows its own pooled
connection and only close() returns it, so every transaction and pipeline is
now in try-with-resources.

cache-aside updateField needed a real redesign, not a rename. WATCH is
per-connection, and UnifiedJedis has no watch(). It now opens
jedis.transaction(false), calls tx.watch(), then reads existence through the
same transaction (tx.exists(key).get()) before tx.multi(). The first port read
existence with jedis.exists() while the transaction held a connection; that
passed at maxTotal=64 and deadlocked at the default maxTotal=8 (jstack: 40
threads parked in GenericObjectPool.borrowObject at the exists line).

Verification, each demo compiled with -Xlint:deprecation (0 warnings) against
Jedis 8.0.2 and run against redis:8.10 on a scratch port: rate limiter 10
allowed then denied and 200 concurrent requests; leaderboard 300 concurrent
increments land exactly (400), trim and pipeline hydration work; session store
full login/refresh/increment/ttl/logout cycle, 300 concurrent increments exact;
cache-aside stampede x50 does one primary read. A driver calling
RedisCache.updateField under contention (12 updaters, 6 invalidators, 2 HLEN
samplers) gave 0 orphan single-field hashes at maxTotal 64, 8 and 2, while a
no-WATCH control produced thousands, so the sampler can see the failure. The
pool is back to 0 active afterward in every run. The page commands themselves
were extracted from each _index.md and run literally in a clean directory
holding only the six jars; all four compile and serve.

Constraint: never call the client while a transaction or pipeline is open; each holds a pooled connection and a second borrow can deadlock a small pool
Constraint: every multi()/pipelined() result on a RedisClient must be closed (try-with-resources) or its connection leaks
Rejected: Lua script for updateField (atomic, no retry loop) because the Go, redis-py and Lettuce pages all teach WATCH/MULTI/EXEC for this path and this page lists those commands
Learned: RedisClient.transaction(false) accepts reads before multi(); they run immediately on that connection and Response.get() returns at once
Learned: the cache-aside demo's /update endpoint writes the primary and invalidates, so it never calls RedisCache.updateField; test that method with a driver, not HTTP
Learned: Jedis 8.0.2 runtime jars are jedis, commons-pool2, slf4j-api, gson, json and redis-authx-core (its pom's compile scope). A smaller set appears to work until a class that needs the rest loads (JedisPubSub needs redis-authx-core), so test the classpath with the feature that loads it
Learned: before this change the pages' jar-only or partial classpaths could not run either (commons-pool2 and slf4j-api were missing), on Jedis 5.x too
Ticket: DOC-7226
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

🧠 Redis Memory

Found 5 related items from repository history (5 new this commit):

Memory updated at 129008f

@github-actions

github-actions Bot commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

DOC-7226

@dwdougherty dwdougherty left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM.

@andy-stark-redis
andy-stark-redis merged commit 713a9ff into main Oct 30, 2026
106 checks passed
@andy-stark-redis
andy-stark-redis deleted the DOC-7226-jedis-pool-a branch October 30, 2026 13:41
andy-stark-redis added a commit that referenced this pull request Oct 30, 2026
…streaming Jedis use cases to RedisClient

The prefetch-cache, time-series-dashboard, pub-sub and streaming java-jedis use cases now take a RedisClient instead of a JedisPool. As in #4388, the getResource() blocks are gone, pipelines and MULTI transactions are in try-with-resources, the demo servers build the client with RedisClient.create or RedisClient.builder() plus a ConnectionPoolConfig where they size the pool, and each page's Maven/Gradle version, prose and javac/java commands move to Jedis 8.0.2 with the six runtime jars.

Three of the directories used APIs that RedisClient lacks, so they needed more than a rename. Time-series-dashboard reached for jedis.getConnection() and wrote raw commands to it; it now calls jedis.executeCommand(new CommandArguments(cmd).addObjects(args)), because the obvious swap, UnifiedJedis.sendCommand(ProtocolCommand, String...), is deprecated in Jedis 8. Pub-sub used Jedis.pubsubChannels, pubsubNumSub and pubsubNumPat, which exist only on the legacy Jedis class, so they are now executeCommand(new CommandObject<>(new CommandArguments(Protocol.Command.PUBSUB).add(Protocol.Keyword.X), BuilderFactory.Y)). Streaming's raw XAUTOCLAIM reply moves from sendCommand to executeCommand, and the deprecated StreamEntryID.UNRECEIVED_ENTRY becomes XREADGROUP_UNDELIVERED_ENTRY; the page's claim that the typed xautoclaim drops the deleted-ids slot still holds in 8.0.2, so the hand parsing stays.

Each pub-sub Subscription no longer owns a Jedis. It borrows its own Connection from the shared client with jedis.getPool().getResource() and runs listener.proceed(connection, ...) (or proceedWithPatterns) on a worker thread, which is what UnifiedJedis.subscribe does internally; the thread returns the connection in a finally block. It does not call jedis.subscribe(), because that borrows its connection internally and leaves close() no way to break a stuck subscription. close() sends UNSUBSCRIBE and, if the thread has not exited a second later, disconnects the connection, as the pre-migration code did with its own Jedis. The sharded pub/sub sentence now names RedisClusterClient as the class that exposes spublish and ssubscribe.

Each demo compiled with -Xlint:deprecation with 0 warnings against Jedis 8.0.2 and ran against Redis 8.10. Prefetch: bulk load, the sync-worker transaction, add, delete and 200 concurrent mixed calls. Time series: snapshot parsing matches the original on Jedis 5.2.0 (types, and min <= avg <= max in every bucket), plus 200 concurrent snapshots. Pub/sub: exact and pattern delivery counts were exact (203 = 3 + 200), PUBSUB introspection was correct, and 150 subscribe/unsubscribe cycles ran against a pool of 64. With a TCP proxy frozen to make a half-open link, close() left 1 connection checked out before the force-disconnect and 0 after. Streaming: the original (5.2.0) and migrated builds gave identical output for produce, consume, crash, pending, XAUTOCLAIM, add and remove worker, trim and replay; only the final concurrent-load tail differs, which is timing. Each page's javac and java commands were extracted and run in a clean directory holding only the six jars.

Constraint: RedisClient has no pubsubChannels, pubsubNumSub, pubsubNumPat or getConnection(), and its sendCommand is deprecated; use executeCommand with CommandArguments or CommandObject
Constraint: each pub-sub Subscription holds one pooled connection for its whole life, so maxTotal must cover subscribers plus command traffic
Constraint: UnifiedJedis.subscribe() borrows its connection internally; code that must break a stuck subscription has to borrow the Connection itself and call JedisPubSub.proceed(connection, ...)
Learned: JedisPubSub needs redis.clients.authentication.core.Token (redis-authx-core) at class-load time, so a three-jar classpath serves every other demo and fails only pub-sub
Learned: the streaming demo's crash button drops the next N deliveries, so a test must crash before producing or nothing is ever pending
Learned: to reproduce a stuck unsubscribe, freeze a TCP proxy between the client and Redis; killing either end makes subscribe() throw and hides the bug
Ticket: DOC-7226
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

clients Client library docs

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants