Skip to content

[maintenance events] Support overlapping MOVING events at pool level (CAE-3395) - #4636

Merged
ggivo merged 26 commits into
feature/sch-1from
topic/ggivo/CAE-3395-overlapping-moving
Aug 4, 2026
Merged

[maintenance events] Support overlapping MOVING events at pool level (CAE-3395)#4636
ggivo merged 26 commits into
feature/sch-1from
topic/ggivo/CAE-3395-overlapping-moving

Conversation

@ggivo

@ggivo ggivo commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator

Summary

Builds on #4625. Replaces the controller's single active MOVING slot (volatile RebindState + CAS) with a per-operation store so distinct MOVING events that overlap in time on one pool no longer fight over one slot.

A pool talks to multiple nodes (or one node across a re-plan), so distinct MOVING events — different seq or different target — can be unexpired simultaneously. The single-slot model wholesale-replaced on a higher seq, dropped a lower seq as a stale replay, and rejected same-seq/different-target — in each case orphaning the earlier event's remap, marking pass, and relaxation window.

Change

  • MovingOperations — a ConcurrentHashMap<MovingEventId, MovingOperation> keyed by (seq, original endpoint), deduplicating the per-connection deliveries of one server-side operation into a single pool-wide entry. Admission (dedup + peer merge) is atomic per key via compute; expired entries are removed by readers on sight, so the post-maintenance hot path returns to isEmpty().
  • Per-peer remapgetSocketAddress resolves the active operation for the connecting peer across all operations, at connect time (a mid-window DNS repoint is honored).
  • Per-operation marking — each applied transition marks its own affected set; a newer event never orphans an earlier unexpired one.
  • Pool-wide relaxation — relaxed timeouts stay in effect while any operation is unexpired and lift once all have expired.

Behavior deltas

  1. Same-seq/different-target is a distinct concurrent event, no longer a rejected conflict.
  2. Lower-seq events are admitted (identity, not ordering); an earlier still-active event keeps remapping/relaxing after a newer one arrives and even after the newer one expires first.
  3. A pending none marking pass runs after a newer event arrives instead of no-oping.
  4. Relaxation ends at the last active event's deadline, not the latest-arrived event's.
  5. The remap target is resolved at connect time, not at event admission.

Tests

  • Unit: MaintenanceEventControllerTest, MaintenanceMarkingTest, MaintenanceEventIdentityTest — overlapping scenarios on distinct peers (per-peer remap, relaxation outlives the shorter event, pending none pass survives a newer event, identity dedup).
  • Integration: AbstractRebindBehaviorTest, AbstractRelaxedTimeoutBehaviorTest — chained/overlapping MOVING via mock servers, for both RedisClient and MultiDbClient.

Draft — opened for review; not ready to merge.


Note

Medium Risk
Touches core connection-pool maintenance, remap, and retirement during live server handoffs; behavior changes are broad but heavily covered by unit and SCH integration tests.

Overview
Replaces the maintenance controller’s single active MOVING slot (RebindState + CAS) with MovingOperations, a concurrent store keyed by event identity()(seq, target) for MOVING — so multiple unexpired MOVING announcements can coexist instead of superseding or rejecting each other.

Per-operation behavior: each admitted operation gets its own affected-peer set, remap window, and marking schedule; getSocketAddress picks the active operation for a peer and resolves the target at connect time. Pool-wide relaxed timeouts stay on while any operation is unexpired. Connections use retireAt(deadline) instead of a one-way boolean so 'none' MOVINGs can retire at half the grace while targeted moves retire immediately; duplicate deliveries can still stamp retirement without re-running a full pass.

Semantics changes: same seq/different target and lower seq are distinct concurrent events; newer events no longer cancel earlier pending 'none' passes; handoff/eviction runs on the maintenance scheduler (fixes async eviction / evictor livelock scenarios covered in tests).

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

ggivo added 8 commits July 15, 2026 14:30
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.
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.
New file was missing from the formatter includes, failing
formatter:validate in CI; add it and apply the formatter.
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.
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".
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.
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.
@github-actions

github-actions Bot commented Jul 22, 2026

Copy link
Copy Markdown

Test Results

  206 files  ±0    206 suites  ±0   7m 57s ⏱️ -7s
8 068 tests ±0  8 000 ✅ ±0  68 💤 ±0  0 ❌ ±0 
8 088 runs  ±0  8 020 ✅ ±0  68 💤 ±0  0 ❌ ±0 

Results for commit 5500c4a. ± Comparison against base commit 13a1edc.

♻️ This comment has been updated with latest results.

@ggivo
ggivo marked this pull request as ready for review July 23, 2026 06:01
Comment thread src/main/java/redis/clients/jedis/MovingOperations.java
ggivo added 5 commits July 24, 2026 10:15
Connection-level affected checks now go through getSocketAddress; the
per-connection variant had no remaining callers.
… 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.
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.
ggivo added 3 commits July 30, 2026 15:25
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.
…vo/CAE-3395-overlapping-moving

# Conflicts:
#	src/main/java/redis/clients/jedis/MaintenanceEventController.java
#	src/test/java/redis/clients/jedis/MaintenanceEventControllerTest.java
#	src/test/java/redis/clients/jedis/MaintenanceMarkingTest.java
@ggivo
ggivo requested a review from atakavci July 31, 2026 05:07
ggivo added 2 commits July 31, 2026 08:44
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.
Comment thread src/main/java/redis/clients/jedis/MaintenanceEvent.java Outdated
Comment thread src/main/java/redis/clients/jedis/MaintenanceEventController.java
Comment thread src/main/java/redis/clients/jedis/MaintenanceEventController.java
Comment thread src/main/java/redis/clients/jedis/MaintenanceEventController.java Outdated
ggivo and others added 2 commits August 3, 2026 15:10
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.
* 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>

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 57697e5. Configure here.

Comment thread src/main/java/redis/clients/jedis/Connection.java
CI's check-format validates every file a PR adds; the file was missing
from the formatter includes, so local runs never formatted it.
@ggivo
ggivo deleted the branch feature/sch-1 August 3, 2026 15:38
@ggivo ggivo closed this Aug 3, 2026
The idle-validation ping consumes the MOVING on the evictor thread;
retirement is stamped inline and the eviction hook runs on the
maintenance scheduler.
@ggivo ggivo reopened this Aug 3, 2026
@ggivo
ggivo changed the base branch from topic/ggivo/CAE-1559-none-registry-walk to feature/sch-1 August 3, 2026 15:58
ggivo added 2 commits August 3, 2026 19:00
…CAE-3395-overlapping-moving

# Conflicts:
#	pom.xml
#	src/main/java/redis/clients/jedis/Connection.java
#	src/main/java/redis/clients/jedis/MaintenanceEventController.java
#	src/test/java/redis/clients/jedis/ConnectionTestHelper.java
#	src/test/java/redis/clients/jedis/MaintenanceEventControllerTest.java
#	src/test/java/redis/clients/jedis/MaintenanceMarkingTest.java
#	src/test/java/redis/clients/jedis/sch/AbstractRebindBehaviorTest.java
@ggivo
ggivo merged commit 5e37978 into feature/sch-1 Aug 4, 2026
13 checks passed
@ggivo
ggivo deleted the topic/ggivo/CAE-3395-overlapping-moving branch August 4, 2026 05:36
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.

2 participants