Skip to content

[maintenance events] Smart Client Handoff (umbrella, feature/sch-1) - #4668

Draft
ggivo wants to merge 11 commits into
masterfrom
feature/sch-1
Draft

[maintenance events] Smart Client Handoff (umbrella, feature/sch-1)#4668
ggivo wants to merge 11 commits into
masterfrom
feature/sch-1

Conversation

@ggivo

@ggivo ggivo commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator

Umbrella PR for the Smart Client Handoff (SCH) maintenance-events feature. Kept as a draft until the feature is complete; meanwhile it serves two purposes:

  • Syncing: "Update branch" (merge commit — not rebase) pulls master into feature/sch-1, with PR CI validating each sync.
  • Visibility: standing view of the feature's full diff and its drift against master.

Feature summary

With CLIENT MAINT_NOTIFICATIONS enabled, the client reacts to server maintenance push notifications (MOVING/MIGRATING/FAILING_OVER): relaxed command timeouts during maintenance windows, proactive reconnection of affected pooled connections to the new endpoint (or to the re-pointed configured endpoint for the none moving-endpoint-type), with per-connection handshake-time resolution of the endpoint type.

Work lands here through focused PRs (#4623, #4625, #4636, ...); do not merge this PR until the feature set is complete.

ggivo and others added 7 commits July 9, 2026 16:39
* Introduce push handler

   - Preparation step for processing custom push notifications
   - Push notification can appear out-of band in-between executed commands
   - Current Connection implementation does not support out of band Push notifications
   - Meaning it will crash if "CLIENT TRACKING ON is enabled" on regular Jedis Connection and "invalidation" push event is triggered

 This commit provides a way to register push handler for the connection which process incoming push messages, before actual command is executed.  To preserve backward compatibility unprocessed push messages are forward to application logic as before.

   - By default Connection will start with NOOP push handler which marks any incoming push event as processed and skips it
   - On subcsribe/psubscribe a dedicated push handler is registered which propagates to the app only supported push  vents such as (message, subscribe, unsubscribe ...)
   - CacheConection is refactored to use a push handler handling "invalidate" push events only, and skipping any other

# Conflicts:
#	src/main/java/redis/clients/jedis/Connection.java
#	src/test/java/redis/clients/jedis/commands/jedis/PublishSubscribeCommandsTest.java

* Introduce PushHandlerChain for composable push event handling

This commit adds a new PushHandlerChain class that implements the Chain of
Responsibility pattern for Redis RESP3 push message handling. Key features:

- Allows composing multiple PushHandlers in a processing chain
- Push events propagate through the complete chain in sequence
- Events marked as not processed are propagated to the client application
- Provides both constructor-based and fluent builder API for chain creation
- Includes predefined handlers for common use cases (CONSUME_ALL, PROPAGATE_ALL)
- Supports immutable chain transformations via methods like then(),

The chain approach provides a flexible way to handle different types of push
messages (invalidations, pub/sub, etc.) with specialized handlers while
maintaining a clean separation of concerns.

Example usage:
  PushHandlerChain chain = PushHandlerChain.of(loggingHandler)
      .then(invalidationHandler)
      .then(PushHandlerChain.PROPAGATE_PUB_SUB_PUSH_HANDLER);

* Handle relax timeout for maintenance events
  - code clean up
  - added relaxed timeout configuration
  - fix unit tests

* Support custom Push listeners for Jedis client

* Add proactiveRebindEnabled configuration option

* PushHandler is now provided through JedisClientConfig instead through constructors.

* Fix NPE in CacheConnection

Register PushInvalidateConsumer after cache is initialised

* [cleanup] Use weak reference in AdaptiveTimeoutHandler to avoid memory leak

* [cleanup] Fix javadoc errors

* [cleanup] Fix TransactionCommandsTest mocked test

* Moving/Rebind initial support

* Mocked relaxed timeout test

* Mocked rebind test

* Fix : wrong order connection.rebind pool.clear

ConenctionFactory should be rebound before triggering the disposal of Idle connection, so that any newly creaetd are using the proper hostname

* [clean up] Address review comments from a-TODO-rov

* add more rebind tests

* clean up

* clean up remove unused test method

* fix relaxed timeout on blocking command

Issue : If Maintenace notifications are received during blocking command, relaxTimeout is enforced instead of infinit timeout.

Fix: Introduce dedicated relax timeout setting for blocking commands. It will fall back to infinit timeout if not set

* format

* enforce code formating for new classes

* reformat to fix java docs

* force formating of TimeoutOptions.java

* Address review comments

* Address review comments

* Address review comments
   - Mark all pushes by default as processed
   - Remove CONSUME_ALL_HANDLER

* format ConnectionTestHelper

* fix  merge errors after rebase
 - Use existing ReflectionTestUtil instead of ReflectionTestUtils.java
 - address connection pool now uses builder - socketFactory not accessible
 - test should now use Endpoints
 - ListenerNotificationConsumer from Jedis
        moved to Connection
 - fix PushMessageNotificationTest
 - fix pom.xml missing includes tag
 - ConnectionTestHelper is obsolete after rebase

* remove support for generic listeners for Push events

* per connection maintenance event handler

 - fix A MIGRATING event on connection A triggers AdaptiveTimeoutHandler for all connections

* use Connection memberOf reference to its owning ConnectionPool to notify it for move event

* PushConsumerContext rename and clean up

* drop readProtocolWithCheckingBroken(pushConsumer)

- propagate connection configured pushConsumers

* fix tests
 - ClientSideCacheFunctionalityTest.testConcurrentAccessWithStats:420 expected: <100000> but was: <60006>
- ClientSideCacheFunctionalityTest.testEvictionPolicyMultithreaded:562 expected: <0> but was: <29>
- ClientSideCacheFunctionalityTest.testMaxSize:463 expected: <110000> but was: <17>
-  RedisClientSideCacheTest>UnifiedJedisClientSideCacheTestBase.invalidationOnCacheHitTest:212 » NullPointer

* tcp mock clean up & improvements

* fix : Single-argument Protocol.read returns wrong type for pushes

* fix : Builder constructor skips MaintenanceEventConsumer registration for relaxed timeouts

* address review comments

* fix: PushMessageNotificationTest

* format and fix moving target parsing

* Address review comments from @atakavci

* Add out-of-band push notification handling

Handle unsupported push notifications gracefully instead of failing the connection.

Fixes errors occurring when push messages are received on connections that are not configured to process them. For example, enabling CLIENT TRACKING on a regular Connection may result in errors when invalidate push messages are delivered.

This change introduces initial support for out-of-band push notifications by:
- detecting push messages on the connection
- silently skipping unsupported push types

* clean up tests

* fix: Wrong @experimental annotation

* add unit test for PushConsumerChainImpl

* address review comments

* add test PushInvalidateConsumer triggers cache invalidations

* format

* [csc] protocolReadPushes now uses  pushConsumers instead of custom invalidate processing

* fix: NumberUtils.safeToInt does not consider negative

* format

* update maint event format

MIGRATING <seq_number> <time> <shard_id-s>:
MIGRATED <seq_number> <shard_id-s>
FAILING_OVER <seq_number> <time> <shard_id-s>
FAILED_OVER <seq_number> <shard_id-s>
MOVING <seq_number> <time> <endpoint>

* Unify maintenance notifications configuration and add MAINT_NOTIFICATIONS ON handshake command

 - add maint notification handshake (CLIENT MAINT_NOTIFICATIONS ON)  - Move TimeoutOptions from JedisClientConfig to MaintenanceNotificationsConfig
 - Remove proactiveRebindEnabled flag from JedisClientConfig

* extract known push message types as consts

* remove commented code

* push consumer chain is now required

* document push handling

* enforce push message as required when initialising PushMessageContext

* add benchmark for Protocol.read(RedisInputStream,PushConsumerChain)

* perf: eliminate string decoding in PushMessage type checking

Replace string-based type checking with byte array comparisons using
Arrays.equals(). PushMessage.getType() now returns byte[] directly
from content.get(0) without decoding or caching.

Adds *_BYTES constants to PushMessageTypes for efficient comparison.

* address review comments
 - npe guard
 - check invalidation message format
 - fix tests

* format

* format

* format

* fix UnifiedJedisProactiveRebindTest
    - apache commons pool creates new connection upon invalidating existing one

* protocol is now required for CommandObjects

* remove a noisy test message on disconnect

* remove a noisy test message on disconnect

* fix auto-merge error error - TRACKING ON missing for CacheConnection

* rename DISABLED_TIMEOUT -> UNSET_TIMEOUT

* fix: parse MOVING endpoint at index 3 to match ["MOVING", seq, time_s, host:port] format

* Make MOVING pool rebind bounded, lock-free and seq-guarded

The rebind to a MOVING target was permanent and reprocessed every duplicate
event. Now it expires back to the original endpoint after time_s, and a
monotonic seq guard ignores duplicate/out-of-order MOVINGs.

Implemented as a lock-free overlay in DefaultJedisSocketFactory (one
AtomicReference holding {seq, target, deadline}); new connections pick the
target while in the grace window, else the original. Deduplication is keyed
solely on the seq number: the first event for a given seq wins, any seq <=
the last applied is ignored as STALE, and any strictly-newer seq applies as
APPLIED_NEW_TARGET. ConnectionPool clears idle connections on each applied
event.

* fix: guard MOVING seq/time_s casts; overflow-safe deadline check

* chore: add DefaultJedisSocketFactoryRebindTest to formatter includes

* Move MOVING rebind overlay ownership to ConnectionFactory

ConnectionFactory now owns the {seq, target, deadline} rebind overlay and
the seq-guarded apply; DefaultJedisSocketFactory becomes a plain resolver
that reads the target through an injected supplier (override during the
grace window, else the configured host). Target selection is a pure read of
the CAS-updated state, so it can never observe a stale target, and revert is
implicit once the window expires.

Replace DefaultJedisSocketFactoryRebindTest with ConnectionFactoryRebindTest.

* Revert relaxed timeouts after the maintenance window

Relaxed timeouts were activated on MIGRATING/FAILING_OVER/MOVING but never
reverted for MOVING (it has no closing event), leaving connections stuck on
the relaxed read timeout. A connection now holds a time-bounded relaxed-timeout
overlay that reverts lazily once its window passes: MIGRATED/FAILED_OVER revert
early, MIGRATING/FAILING_OVER fall back to a configurable max duration if the
closing event is lost, and MOVING relaxes for time_s.

Relaxation is applied on pool borrow, so every connection handed out during a
rebind window is relaxed - not only the one that received MOVING.

Add TimeoutOptions.relaxedTimeoutMaxDuration (default 60s).

* Parse maintenance events into a typed hierarchy

* Centralize maintenance handling in a pool-owned controller

Maintenance is a pool-only feature handled by a single MaintenanceEventController
that owns the MOVING rebind overlay, the relax-window policy, and the handoff
hooks fired when a MOVING is applied.

- ConnectionPool creates and owns the controller and hands it to the factory.
- ConnectionFactory wires the socket-factory target resolver and relaxes on
  borrow.
- Connection forwards events to the controller and keeps the per-socket
  relaxed-timeout overlay.
- Handoff hooks (public add/removeHandoffHook): registered synchronous hooks,
  fired once per applied MOVING with a MaintenanceHandoff payload (seq, target,
  ttl); CopyOnWriteArrayList; hook exceptions propagate. ConnectionPool wires
  the pool's clear() through this hook.

* set moving-target-type during handshake

* add example with enabled maint-events

* Rebalance pool on MOVING via post-DNS address mapping

Replace the pre-DNS global target override + pool.clear() with a post-DNS,
per-affected-peer redirect and selective idle eviction:

- New SocketAddressMapper interface (post-DNS)
- RebindAwareEvictionPolicy wraps the user's EvictionPolicy, destroying idle
  connections whose peer is affected; handoff hook triggers evict() (not clear()).
- Connection gains getRemoteSocketAddress() to provide the affected key.
- Tests use 127.0.0.1 (deterministic post-DNS match) and assert connected-client
  counts.

* format

* fix SentineledConnectionProviderReconnectionTest

* drop EndpointType.NONE till complete support is implmented

* Merge same-seq MOVING peers into the rebind's affected set

   same node could resolve to multiple ip's

* Default MaintenanceNotificationsConfig.DEFAULT mode to AUTO

* Apply and revert relaxed socket timeouts on each read

Relaxed SO_TIMEOUT was pushed from maintenance event handlers and the
pool's borrow hook, requiring every affecting site to keep the socket
in sync. The protocol read now pulls the desired value from the
connection's per-receiver window and the pool controller's rebind
state, applying it through a cached last-applied value so
setSoTimeout fires only on actual transitions.
The server-supplied MOVING ttl is capped at the configured
relaxedTimeoutMaxDuration, and expired relaxation state is cleared
lazily on first observe.

* Fix stale relaxed-timeout Javadoc

* Require RESP3 for maintenance notifications

Maintenance push frames need RESP3. Mode.ENABLED now throws on a
RESP2 connection; Mode.AUTO logs and falls back without maintenance.
TcpMockServer gains per-connection HELLO negotiation so the new
MaintenanceHandshake tests can exercise both paths.

* clean up

* maintenance and relaxed-timeout mock tests for CacheConnection

* Fix TcpMockServer dropping HELLO from custom CommandHandler

* Require maintenance controller for MAINT_NOTIFICATIONS; unregister consumer on server reject

* Add sch/ to formatter includes; format Abstract* tests

* Rename UnifiedJedisProactiveRebindTest -> sch/RebindMockTest

* Inject maintenance controller into socket factory at construction time

The controller used to be wired post-construction via
ConnectionFactory.attachMaintenanceController + DefaultJedisSocketFactory
.setSocketAddressMapper. That created a window where a ConnectionFactory
existed without a fully-wired socket factory, and the instanceof
DefaultJedisSocketFactory gate inside attachMaintenanceController silently
dropped the mapper for any custom factory.

Move the wiring into the builder. ConnectionFactory.Builder now carries the
controller; withDefaults() constructs a DefaultJedisSocketFactory with the
mapper baked in via a new package-private ctor (h, c, mapper) and feeds the
same controller into the default Connection.Builder. The field becomes
final; setSocketAddressMapper and attachMaintenanceController are deleted.

ConnectionPool.wireMaintenance shrinks to installMaintenanceHooks: it only
adds the rebind-aware eviction policy + handoff hook, reading the (already
attached) controller back from the factory. Controller construction moves
to a new buildFactoryWithMaintenance helper used by the four host+config
convenience constructors -- a temporary location; C2 will lift this up to
StandaloneClientBuilder where the maintenance decision actually belongs.

* Move MaintenanceNotificationsConfig to StandaloneClientBuilder; split TimeoutOptions

The maintenance config no longer rides on JedisClientConfig -- only the
RedisClient builder accepts it, which makes the feature's scope explicit
and prevents silent no-ops on JedisPool/JedisCluster/Jedis.

MaintenanceEventController becomes public final so RedisClient-style
builders outside redis.clients.jedis can wire it without indirection;
implemented MaintenanceEvent.Handler and SocketAddressMapper interfaces
stay package-private, so dispatch and remap entry points remain internal.

StandaloneClientBuilder creates the controller directly and injects it
via a new 5-arg clientConfig-aware constructor on ConnectionPool /
PooledConnectionProvider that also attaches the AuthX listener, preserving
the contract that AuthX wiring lives in clientConfig-aware constructors.

ENABLED + custom ConnectionProvider throws IllegalArgumentException at
build time; AUTO + custom logs at debug and disables.

TimeoutOptions is dropped entirely: relaxedSocketTimeoutMillis and
relaxedBlockingSocketTimeoutMillis move to JedisClientConfig as peers of
socketTimeoutMillis/blockingSocketTimeoutMillis; relaxedWindowMaxDuration
(the per-window backstop) stays on MaintenanceNotificationsConfig with a
clearer name. Connection reads relaxed timeouts directly from clientConfig.

* Wire pool maintenance hooks via explicit controller

Pool-side hook installation (rebind-aware eviction +handoff-driven evict)
now happens only when the caller explicitly hands a
MaintenanceEventController to the clientConfig-aware 5-arg constructor:

  ConnectionPool(host, cfg, cache, poolConfig, controller)
  PooledConnectionProvider(host, cfg, cache, poolConfig, controller)

* Decouple Connection timeout decision from MaintenanceEventController

Connection's SO_TIMEOUT no longer queries the controller's rebind state
directly. A pluggable SoTimeoutSupplier (package-private, int getSoTimeout
(boolean blocking)) is consulted before each read; it returns the timeout
to apply or UNSET_TIMEOUT_MS to defer to the connection's own calculation.

The pool wires a controller-backed supplier in ConnectionFactory that
relaxes the timeout while a MOVING rebind window is active and defers
otherwise. Relaxed values are captured from the immutable client config at
wiring time. Connection keeps no reference to the controller on the timeout
path; the controller is still used for event dispatch and the handshake.

* Dispatch maintenance events to listeners; drop controller from Connection

Connection no longer references MaintenanceEventController. Maintenance push
frames are dispatched to registered MaintenanceEventListener(s)
, invoked synchronously on the read thread.

* Make MaintenanceEventController package-private

Thread the public MaintenanceNotificationsConfig from the builder through the
provider to ConnectionPool, which creates the controller in-package and wires
it into the factory + eviction.

* Trim unused maintenance accessors; pool owns the controller

Shrinks the maintenance surface and moves the controller reference
from ConnectionFactory to ConnectionPool, which owns it.

- Drop dead MaintenanceEventController.getMode/getEndpointType
  (duplicated MaintenanceNotificationsConfig accessors).
- Drop test-only isTimeoutRelaxed and
  DefaultJedisSocketFactory.getSocketAddressMapper.
- ConnectionPool holds the controller; ConnectionTestHelper reads it
  from the pool instead of the factory.

borrowRelaxesConnection_duringRebindWindow asserts via isRebindActive;
obsolete factory-wiring tests removed.

* [maintenance events] Wire maintenance config/controller into multidb components  (#4569)

* wire maintenance config/controller to multidb compoenents

* fix multiDb mocking issues in test

* fix flaky rebind test

* Apply MOVING rebind mapper to multidb pool connections

* Preserve maintenance config across multidb pool rebuild

* attachAuthenticationListener() now invoked in supper

* address @atakavci review
  - maintNotificationsConfig set to DEFAULT in StandaloneClientBuilder
  - add MaintenanceNotificationsConfig shorthand config methods
  - code clean up

* address @atakavci review comments
  - maintenance event parsing optimised
  - extract push notification parsing from domain MaintenanceEvent in dedicated MaintenancePushCodec and optimise parsing to one pass message type processing and parsing

* extract MaintenanceEventConsumer as top level class and add test

* Move relaxed-timeout config to the maintenance feature

Relaxed socket/blocking timeouts were public knobs on JedisClientConfig
but only take effect during SCH maintenance windows. Move them to
MaintenanceNotificationsConfig (sourced by Connection/ConnectionFactory)
and make the connection-level relax API package-private, so the public
surface only exposes settings wired end-to-end.

Addresses @atakavci: don't add interface config that isn't supported
end-to-end via core components — it only creates user-side confusion.

* Set per-client maintenance-notifications defaults: standalone AUTO, MCF DISABLED

RedisClient defaults to AUTO; MultiDbClient databases default to DISABLED.
Each builder tracks the unset state via a dedicated sentinel instance, so an
explicit value is distinguishable from the default and the default can change
in a future release without overriding clients that set one.

Pending: "auto" endpoint-type discovery for MOVING (from IP format + TLS
config), to become the resolved default under AUTO mode.

* Signal malformed maintenance pushes via exception
  - add todo for missing `none` support for moving event

* [maintenance events] Alternative approach for handling timeout changes via TimeoutSupplier (#4572)

* fix duble authx registration

* draft timeoutsupplier

* hot path perf optimization

* micro optimization by AI =) ridiculously micro

* - clean up connection from rebindstate
- preserver the old semantics with timout getters on conneciton
- use last reference for relaxingtimeouts instead of list
- drop simpletimeoutsupplier
- improve controller to hold rebinding connections.
- introduce returnHook in ConnectionPool

* clean up

* - feedback from @ggivo

* - clean duplicate fields
- fix compile issue

* - introduce TimeoutSupplierChain
- replace AdvancedTimeoutSupplier

* fix duration-timestamp issue with relaxation

* - drop weakhashmap
- remove applysotimeout at getStatusCodeReplyInner

* - fix possible refencing corruption with unified timeoutsupplierchain instance from controller

* - add expiration to conneciton in case future renewals needed
- improve connection with init visitors and disect the logic  out to relevant components for optional features.

* - rollback to using isBlocking at connection level
-clean up with configuration classes

* - feedback from @copilot

* - polish names

* - move applyCurrentTimeout into readProtocol

* - refactor type names

* - introduce upluggable source

* - move customtimeoutsupplier setup into maintenanceawarevisitor

* - fix test-compiler issues

* - inject maintenance initiation responsibililty into connection.builder

* - fix controller unit tests

* - fix connection build for maintenancevisitor
- fix AbstractRelaxedTimeoutBehaviorTest

* - probe and add controller as listener in maintenancevisitor

* fix java doc refs

* polish

* - address @cursor feedback

* clean up connection builder

* - fix tests

* feeback from @curosr

* - format

* - format

* Align TimeoutSource chain naming with its chain-of-responsibility role

TimeoutSourceNode leaked the linked-list implementation into the type
name and forced every source to be chainable through the
TimeoutSource/UnplugableSource split.

- TimeoutSourceNode -> ChainedTimeoutSource; overrideWith/unplug ->
  addOverride/removeOverride, typed to the chain class
- TimeoutSource slimmed to the read contract; the null-means-no-opinion
  protocol is now documented on get()
- UnplugableSource, its unused generic and the unchecked cast removed
- anonymous controller node named RebindTimeoutSource
- get() snapshots the volatile override to avoid an NPE against a
  concurrent removeOverride

---------

Co-authored-by: ggivo <ivo.gaydazhiev@redis.com>

* - format

* Restore connect-on-demand in setTimeoutInfinite and revert to SocketException when applying socket.setSoTimeout

* drop public listener from maintenanceconfig

* remove maintennace init in conn builder

* remove comments

* - fix failing MaintenanceHandshake tests

* Scope shared maintenance handshake tests to connection level

The shared handshake tests built a full RedisClient per test, coupling
connection-level behavior to client wiring. The base test now wires the
handshake onto a Connection.Builder directly, mirroring ConnectionFactory
production wiring. Client-level wiring coverage moves to a dedicated
RedisClient test.

* CacheConnectionMockTest use test cache

* mark connection expire immediately upon receiving MOVING

---------

Co-authored-by: atakavci <a_takavci@yahoo.com>
* fix accessors

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* - fix tests

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
…tighten the deadline (#4611)

-to not reduce the timeout by changes on timeout config
…n characteristics (#4613)

* Auto-resolve moving-endpoint-type from connection characteristics

CAE-1560. The handshake previously always requested the configured
fixed endpoint type (default external-ip), which is wrong for private
networks and TLS. The default is now resolved per connection, matching
Lettuce: private peer IP selects internal-*, public external-*; TLS
selects *-fqdn, plaintext *-ip. TLS is taken from the declared client
config (ssl flag or sslOptions set), mirroring
DefaultJedisSocketFactory; custom socket factories deviating from the
declared configuration are not supported.

endpointType(...) still forces a fixed type.

* Rename EndpointTypeSource to EndpointTypeResolver

Resolver better conveys the per-connection resolution strategy.
Addresses review feedback on #4613.
…ing a rebind window (#4623)

* - plug timeoutSources before handshake, unplug after handshake if disabled.

* format

* fix merge issue

* javadoc fix

* format

* - feedback from copilot

* apply timeouts right after relaxation

* change log level for MaintenanceAwareVisitor

* fix flaky one in TBA tests
@github-actions

github-actions Bot commented Jul 31, 2026

Copy link
Copy Markdown

Test Results

  206 files  ±0    206 suites  ±0   7m 46s ⏱️ -3s
8 101 tests ±0  7 796 ✅  - 237  305 💤 +237  0 ❌ ±0 
8 121 runs  ±0  7 816 ✅  - 237  305 💤 +237  0 ❌ ±0 

Results for commit 58e23ca. ± Comparison against base commit ccfc080.

This pull request skips 237 tests.
redis.clients.jedis.UnboundRedisClusterClientTest ‑ testAskResponseWithHimportSet
redis.clients.jedis.commands.commandobjects.CommandObjectsListCommandsTest[1] ‑ testLmovemAndBlmovem()[1]
redis.clients.jedis.commands.commandobjects.CommandObjectsListCommandsTest[1] ‑ testLmovemAndBlmovemBinary()[1]
redis.clients.jedis.commands.commandobjects.CommandObjectsListCommandsTest[2] ‑ testLmovemAndBlmovem()[2]
redis.clients.jedis.commands.commandobjects.CommandObjectsListCommandsTest[2] ‑ testLmovemAndBlmovemBinary()[2]
redis.clients.jedis.commands.commandobjects.CommandObjectsSetCommandsTest[1] ‑ testSdiffcard()[1]
redis.clients.jedis.commands.commandobjects.CommandObjectsSetCommandsTest[1] ‑ testSunioncard()[1]
redis.clients.jedis.commands.commandobjects.CommandObjectsSetCommandsTest[2] ‑ testSdiffcard()[2]
redis.clients.jedis.commands.commandobjects.CommandObjectsSetCommandsTest[2] ‑ testSunioncard()[2]
redis.clients.jedis.commands.commandobjects.CommandObjectsTimeSeriesCommandsTest[1] ‑ testTsNRange()[1]
…

♻️ This comment has been updated with latest results.

ggivo and others added 3 commits August 3, 2026 10:45
…on registry (CAE-1559) (#4625)

* Support 'none' moving-endpoint-type: wire decoding and handshake

Groundwork shared with the deadline-based implementation (PR #4620):
EndpointType.NONE with the 'none' handshake token, RESP3 null MOVING
target decoded as a null-target MovingEvent, and test-server support
for RESP3 null frames and server-side client drops.

* Mark MOVING-affected connections via registry walk and scheduled pass

The controller tracks every pool-managed connection in a weak-reference
registry and runs one marking pass per applied MOVING transition (new
seq or merged source) - inline for a real target, at half the grace
period for 'none' - flipping an advisory volatile flag and running the
handoff hooks so the pool evicts marked idles. Marked in-use connections
retire on return; validation reports them invalid under
testOnBorrow/testWhileIdle.

Connections register before socket init, so a connect racing a MOVING
commit is either visible to its marking pass or remapped via the
committed rebind. Connections created after the pass are never visited,
so a reconnect that re-lands on a not-yet-repointed endpoint (and its
same-seq re-notification) is immune by construction.

* Include ConnectionRegistry in formatter validation

New file was missing from the formatter includes, failing
formatter:validate in CI; add it and apply the formatter.

* Create the maintenance scheduler lazily on first null-target rebind

The endpoint type can be auto-resolved per connection, so the config
alone cannot decide whether a scheduler is needed. Creating it on the
first null-target rebind.

* Use the raw MOVING ttl as the relax window

MOVING's time_s is the server's completion bound (move done, old
connections dropped by then), so it is a trustworthy horizon and the
relaxedWindowMaxDuration cap does not apply. The backstop remains for
MIGRATING/FAILING_OVER, whose time_s only means "starts within".

* Fix error after merge

* Remove unused MaintenanceEventController.isAffected

Connection-level affected checks now go through getSocketAddress; the
per-connection variant had no remaining callers.

* Rename connection reconnect mark to 'retire' and route returns in one place

'markedForReconnect' suggested the connection re-establishes its own
socket; it never does — the pool disposes of it and creates a
replacement. 'retire()' / 'isRetired()' name the actual contract:
generic, advisory, one-way removal from pool service.

Connection.close() no longer special-cases the flag; the pool's return
hook is the single routing point for retired connections. Addresses
review feedback on #4625.

* Bind the handoff hook to a single owner

A multi-hook list suggested the controller outlives its pool and can be
shared; it cannot — one owner creates it, registers its reaction, and
closes it. Replace the list with a single-slot setHandoffHook and state
the ownership contract on the class. Addresses review feedback on #4625.

* Explain the registration-before-init contract in comments

The register-before-connect ordering carries the marking-pass coverage
guarantee; state the why on the factory helper and align comments with
the code's 'applied' vocabulary. Addresses review feedback on #4625.

* Move maintenance registration into the visitor

visitBeforeHandshake runs before connect(), so the visitor gives the
same register-before-connect guarantee as the factory did — and all
maintenance wiring now lives in one component. ConnectionFactory loses
its maintenance code entirely. Addresses review feedback on #4625.
…(CAE-3395) (#4636)

* Support 'none' moving-endpoint-type: wire decoding and handshake

Groundwork shared with the deadline-based implementation (PR #4620):
EndpointType.NONE with the 'none' handshake token, RESP3 null MOVING
target decoded as a null-target MovingEvent, and test-server support
for RESP3 null frames and server-side client drops.

* Mark MOVING-affected connections via registry walk and scheduled pass

The controller tracks every pool-managed connection in a weak-reference
registry and runs one marking pass per applied MOVING transition (new
seq or merged source) - inline for a real target, at half the grace
period for 'none' - flipping an advisory volatile flag and running the
handoff hooks so the pool evicts marked idles. Marked in-use connections
retire on return; validation reports them invalid under
testOnBorrow/testWhileIdle.

Connections register before socket init, so a connect racing a MOVING
commit is either visible to its marking pass or remapped via the
committed rebind. Connections created after the pass are never visited,
so a reconnect that re-lands on a not-yet-repointed endpoint (and its
same-seq re-notification) is immune by construction.

* Include ConnectionRegistry in formatter validation

New file was missing from the formatter includes, failing
formatter:validate in CI; add it and apply the formatter.

* Create the maintenance scheduler lazily on first null-target rebind

The endpoint type can be auto-resolved per connection, so the config
alone cannot decide whether a scheduler is needed. Creating it on the
first null-target rebind.

* Use the raw MOVING ttl as the relax window

MOVING's time_s is the server's completion bound (move done, old
connections dropped by then), so it is a trustworthy horizon and the
relaxedWindowMaxDuration cap does not apply. The backstop remains for
MIGRATING/FAILING_OVER, whose time_s only means "starts within".

* Support overlapping MOVING events at pool level

Distinct MOVING events (different seq or endpoint) can overlap on one
pool. The single rebind slot let a newer event orphan an earlier
unexpired one: its remap stopped, its pending 'none' pass no-oped, and
the relax window could be silently truncated.

Events are now keyed by (seq, original endpoint) in an immutable
snapshot map and only expire — never supersede. Remap resolves the
matched event's endpoint at connect time; relaxation holds until the
last event expires; a marking pass is a no-op only if its event was
pruned.

* Cover overlapping MOVING events in rebind and relaxation suites

Real-world scenario: DNS round-robin (simulated with a host-port
mapper) spreads the pool over two backends; each announces its own
MOVING to a different target, the second within the first's window.
Asserts pool-wide relaxation holds until the last event expires and
that connections created during each window land on that window's
target only while it is open.

* Fix error after merge

* Remove unused MaintenanceEventController.isAffected

Connection-level affected checks now go through getSocketAddress; the
per-connection variant had no remaining callers.

* Rename connection reconnect mark to 'retire' and route returns in one place

'markedForReconnect' suggested the connection re-establishes its own
socket; it never does — the pool disposes of it and creates a
replacement. 'retire()' / 'isRetired()' name the actual contract:
generic, advisory, one-way removal from pool service.

Connection.close() no longer special-cases the flag; the pool's return
hook is the single routing point for retired connections. Addresses
review feedback on #4625.

* Bind the handoff hook to a single owner

A multi-hook list suggested the controller outlives its pool and can be
shared; it cannot — one owner creates it, registers its reaction, and
closes it. Replace the list with a single-slot setHandoffHook and state
the ownership contract on the class. Addresses review feedback on #4625.

* Explain the registration-before-init contract in comments

The register-before-connect ordering carries the marking-pass coverage
guarantee; state the why on the factory helper and align comments with
the code's 'applied' vocabulary. Addresses review feedback on #4625.

* Move maintenance registration into the visitor

visitBeforeHandshake runs before connect(), so the visitor gives the
same register-before-connect guarantee as the factory did — and all
maintenance wiring now lives in one component. ConnectionFactory loses
its maintenance code entirely. Addresses review feedback on #4625.

* Simplify event identity to seq plus target for MOVING

Only MOVING operations are deduplicated pool-wide, so the id classes
and their type component were dead generality: seq is sufficient for
every other notification. identity() now returns an opaque value —
boxed seq, or (seq, target) for MOVING so concurrent MOVINGs to
different endpoints stay distinct. Addresses review feedback on #4636.

* [maintenance  events]Proposal for early retirement of connections (#4670)

* tag with expireAt early, schedule a delayed evict

* fix delayed schedule and retirement

* Re-walk the registry in the scheduled pass; always run the hook

The pass stamps connections registered after the apply-time walk; the
hook runs however late, when stamped idles are dead sockets.

* Fix tests for deadline-based retirement

Tests drive a deterministic clock and await the off-thread hook.

---------

Co-authored-by: ggivo <ivo.gaydazhiev@redis.com>

* Format MovingOperations and add it to the formatter includes

CI's check-format validates every file a PR adds; the file was missing
from the formatter includes, so local runs never formatted it.

* test : Add test against evict() livelock on evictor-triggered rebind

The idle-validation ping consumes the MOVING on the evictor thread;
retirement is stamped inline and the eviction hook runs on the
maintenance scheduler.

---------

Co-authored-by: atakavci <a_takavci@yahoo.com>
ggivo added a commit that referenced this pull request Aug 5, 2026
A long-lived template kept strong references to every connection it
was prepared on, retaining connections the pool had already destroyed.

Record them in ConnectionRegistry instead: a weak-reference registry
(same class as the maintenance-events branch, PR #4668) that prunes
collected connections, so close() visits only live ones. close() is
now idempotent, and registerConnection re-checks discarded afterwards
so a close() racing first use cannot leave the fieldset undiscarded.
ggivo added a commit that referenced this pull request Aug 7, 2026
* Initial

* Add pipeline support

* Add hooks

* Move state in the template

* Track prepared connections weakly in HashImport

A long-lived template kept strong references to every connection it
was prepared on, retaining connections the pool had already destroyed.

Record them in ConnectionRegistry instead: a weak-reference registry
(same class as the maintenance-events branch, PR #4668) that prunes
collected connections, so close() visits only live ones. close() is
now idempotent, and registerConnection re-checks discarded afterwards
so a close() racing first use cannot leave the fieldset undiscarded.

* Run cluster ASKING as a command pre-process hook

On an ASK redirect the executor sent ASKING out-of-band before the
command; a command-intrinsic hook such as HIMPORT's lazy PREPARE then
consumed the one-shot ASKING state, redirecting the command again
until attempts were exhausted.

CommandObject is now immutable and carries an ordered list of
pre-process hooks; withPreProcessHook() returns a modified copy.
ClusterCommandExecutor appends the ASKING hook to a copy of the
redirected command, so the wire order is PREPARE, ASKING, SET and
ASKING applies to the command itself.

Covered by CommandObjectTest and testAskResponseWithHimportSet
(himportSet on a migrating slot), verified against a live cluster.

* Send pending HIMPORT discards before command execution

Discard reconciliation ran only in ConnectionFactory.activateObject, so
legacy JedisPool/JedisSentinelPool and direct unpooled connections never
issued queued HIMPORT DISCARDs, accumulating server-side fieldsets. A
discard failure was also swallowed there after marking the connection
broken, handing a dead connection to the borrower.

Move reconciliation to the Connection.executeCommand entry points: one
choke point covers every path, gated by a single volatile read. Pending
discards go out as one packed write with replies drained in one pass;
error replies are ignored while connection failures propagate to the
retry machinery. activateObject is a no-op again.

* Keep internal pipeline commands out of syncAndReturnAll results

The HIMPORT PREPARE injected before a first-use himportSet was buffered
like a user command, so syncAndReturnAll() returned an extra OK and
shifted the positions callers rely on.

Pipeline responses are now QueuedResponse entries flagged internal or
user at creation; internal commands are buffered via
appendInternalCommand and their replies, while still read in wire
order, are filtered from syncAndReturnAll(). sync() is unchanged.

* Reset HIMPORT state only on reconnect, on the owner thread

setBroken() cleared the connection's HIMPORT state, but it may be
called by threads that do not own the connection (forceDisconnect,
maintenance paths), racing the owner's unsynchronized prepared set.
The clear was also unnecessary: broken connections are dropped on
every pooled and executor path, never reused.

Remove the reset from setBroken(); connect() alone resets the state
(renamed HimportConnectionState.reset()), which only the owner thread
runs. Drop the legacy integration tag from HashImportReconcileIT -
the IT suffix alone routes it to failsafe.

* Reject empty field names eagerly; cover HIMPORT lifecycle at wire level

Empty field names slipped past validation despite the documented
contract, deferring the failure to HIMPORT PREPARE on the server.
Reject "" and zero-length byte[] in the shared build step.

Replace HashImportReconcileIT (legacy-client, introspection-based)
with HimportLifecycleMockTest: a TcpMockServer command log pins the
wire contract - PREPARE once per connection ahead of the first SET,
queued DISCARDs of closed templates drained right before the next
command, batched. Reconciliation runs at the shared
Connection.executeCommand choke point, so provisioning-path variants
added no logic coverage.

* Clear HIMPORT state on RESET

RESET drops connection-scoped fieldsets server-side, but the client
kept its prepared note, so later himportSet calls skipped PREPARE and
failed until reconnect. Jedis.reset() now resets the connection's
HIMPORT state; clearing in finally is harmless on failure (at worst
one extra re-PREPARE). Raw sendCommand(RESET) is intentionally out of
scope.

* Add @SInCE to CommandObject.withPreProcessHook

New public API needs release provenance in the generated docs; also
complete the truncated javadoc sentence.

* Build HIMPORT PREPARE only when a connection needs it

Every himportSet call eagerly built the PREPARE command object -
fieldset name plus all field tokens - just to capture it in the
pre-process hook, then discarded it whenever the connection was
already prepared, the common case in bulk-import loops.

Assemble the PREPARE arguments inside prepareBeforeUse instead, on
the not-prepared branch only; the steady-state path allocates
nothing PREPARE-related. Wire order covered by
HimportLifecycleMockTest.

* Document himportSet API and add Hash Import user guide

Expand the himportSet javadoc on all four command interfaces with the
verified contract: existing hashes are replaced, wrong value count and
closed templates fail eagerly, transactions and cluster pipelines are
unsupported, PREPARE is injected per pooled connection.

Add docs/hash-import.md under User Guide covering usage, pipelining,
cluster behavior and limitations, with a compiled example
(HashImportUsage) mirroring the doc snippets, verified against a live
Redis 8.10.

---------

Co-authored-by: ggivo <ivo.gaydazhiev@redis.com>
# Conflicts:
#	pom.xml
#	src/main/java/redis/clients/jedis/Connection.java
#	src/main/java/redis/clients/jedis/ConnectionRegistry.java
@uglide

uglide commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

⚠️ Jedis Scenario Tests run on RE 8.0.22 produced no results — it failed before or while running the tests — details for maintainers

@uglide

uglide commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

⚠️ Jedis Scenario Tests run on RE 8.0.16 produced no results — it failed before or while running the tests — details for maintainers

@uglide

uglide commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

⚠️ Jedis Scenario Tests run on RE 100.0.20 produced no results — it failed before or while running the tests — details for maintainers

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants