Skip to content

Bound SharedLock lockWritersExclusive spin with Thread.onSpinWait and timeout (#3880) - #3956

Open
ronitgupta138 wants to merge 8 commits into
redis:mainfrom
ronitgupta138:fix/bound-exclusive-lock-spin
Open

ronitgupta138 wants to merge 8 commits into
redis:mainfrom
ronitgupta138:fix/bound-exclusive-lock-spin

Conversation

@ronitgupta138

@ronitgupta138 ronitgupta138 commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Make sure that:

  • You have read the contribution guidelines.
  • You have created a feature request first to discuss your contribution intent. Please reference the feature request ticket number in the pull request.
  • You applied code formatting rules using the mvn formatter:format target. Don’t submit any formatting related changes.
  • You submit test cases (unit or integration tests) that back your changes.

Fixes #3880

Description

In SharedLock.lockWritersExclusive(), when another thread holds or leaks a shared writer without draining, the calling thread spins in an unbounded for (;;) busy-loop at 100% CPU. Because the spinner holds the inner ReentrantLock during this loop, other threads (including IO event loops attempting channelInactive -> doExclusive) become permanently blocked or wedged.

As discussed in #3880 with @a-TODO-rov:

  • Adds Thread.onSpinWait() in the spin loop to hint to the processor and mitigate CPU saturation.
  • Bounds the exclusive lock acquire with a timeout (default 30 seconds), throwing RedisException if shared writers fail to drain within the deadline.
  • Adds package-private constructor SharedLock(Duration) allowing configuration/testing of timeouts.
  • Adds unit tests in SharedLockUnitTests verifying the fail-fast timeout and validation.

Note

Medium Risk
Changes connection teardown and exclusive locking on Netty event loops; bounded timeouts can surface new RedisExceptions during close/disconnect, but they replace prior deadlock and CPU spin scenarios.

Overview
Fixes unbounded CPU spin and indefinite blocking when SharedLock cannot acquire an exclusive lock because shared writers never drain (e.g. during channelInactive → doExclusive).

SharedLock: The exclusive-acquire spin loop now uses Thread.onSpinWait() when available (fallback Thread.yield()), and waits are bounded by a 30s default timeout via tryLock plus a deadline while draining writers. Failures surface as RedisException; a package-private SharedLock(Duration) supports tests and custom timeouts.

Lifecycle hardening: CommandHandler.channelInactive, DefaultEndpoint.notifyChannelInactive, and DefaultEndpoint.closeAsync wrap deactivation/cleanup in try/finally so channel clearing, DEACTIVATED state, protected-mode handling, and channel close still run if earlier steps throw (including the new lock timeout).

Cluster: ClusterNodeEndpoint.closeAsync catches drain failures, logs a warning, and completes the close future exceptionally instead of silently continuing.

Unit tests cover timeout fail-fast, default timeout, and invalid timeout validation.

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

… timeout (redis#3880)

- Add Thread.onSpinWait() to the lockWritersExclusive() spin loop to avoid pegging CPU cores at 100%
- Bound exclusive lock acquisition by a timeout (default 30 seconds) and fail fast with RedisException if shared writers fail to drain
- Add unit tests verifying fail-fast behavior and validation in SharedLockUnitTests
Copilot AI balanced review requested due to automatic review settings October 16, 2026 08:09

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-20T10:34:50.411108Z 3dd351b New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a7a06ef048

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

return;
}

Thread.onSpinWait();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve Java 8 compatibility in the spin loop

When Lettuce is built on its supported Java 8 baseline, this call cannot be resolved because Thread.onSpinWait() was introduced in Java 9; pom.xml still compiles to 1.8, so the JDK 8 build fails, while an artifact compiled with a newer JDK can instead throw NoSuchMethodError on Java 8 as soon as contention reaches this branch. Use a Java 8-compatible pause/backoff mechanism here.

AGENTS.md reference: AGENTS.md:L61-L64

Useful? React with 👍 / 👎.

Comment on lines +174 to +175
throw new RedisException(
"Failed to acquire exclusive lock: shared writers failed to drain within " + exclusiveLockTimeout);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Keep channel-inactive cleanup running after lock timeout

When a shared writer is still active at the 30-second deadline during disconnect, this exception escapes DefaultEndpoint.notifyChannelInactive() through CommandHandler.channelInactive(). That skips notifyDrainQueuedCommands, the DEACTIVATED transition, and super.channelInactive(ctx) in that method; because ConnectionWatchdog is added downstream by ConnectionBuilder.buildHandlers(), it never receives the inactive event and auto-reconnect is not scheduled, leaving the connection permanently disconnected. Convert the timeout to a result or catch it in the lifecycle path so cleanup and event propagation still run.

AGENTS.md reference: AGENTS.md:L89-L93

Useful? React with 👍 / 👎.

@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.

Stale Bugbot comment from a previous run.

@a-TODO-rov

Copy link
Copy Markdown
Collaborator

Hey @ronitgupta138
Please check CI and bot reviews first.

…ycle on lock timeout (redis#3880)

- Use dynamic MethodHandle for Thread.onSpinWait() to preserve Java 8 compiler and runtime compatibility
- Guard endpoint lifecycle methods (channelInactive, notifyDrainQueuedCommands, closeAsync) against lock acquisition timeouts to ensure connection cleanup and auto-reconnect are not bypassed
@ronitgupta138

Copy link
Copy Markdown
Contributor Author

Addressed the review findings in commit f25b3a1:

  1. Java 8 Compatibility: Replaced static Thread.onSpinWait() reference with dynamic MethodHandle lookup via Java 7 MethodHandles.lookup(), with fallback to Thread.yield() on Java 8 runtimes. Tested and verified cleanly with Maven compiling against Java 8 (-source 1.8 -target 1.8).
  2. Channel Lifecycle Protection: Wrapped exclusive lock execution in DefaultEndpoint.notifyChannelInactive(), notifyDrainQueuedCommands(), and ClusterNodeEndpoint.closeAsync() to handle RedisException on timeout gracefully. Ensured lifecycle state deactivation, channel teardown, and Netty super.channelInactive() propagation are never bypassed, preserving downstream ConnectionWatchdog reconnection scheduling.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f25b3a14b8

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +188 to +190
if (System.nanoTime() >= deadline) {
throw new RedisException(
"Failed to acquire exclusive lock: shared writers failed to drain within " + exclusiveLockTimeout);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Guard remaining lifecycle calls from the lock timeout

When a shared writer is still held at this deadline, the exception also escapes the unguarded DefaultEndpoint.notifyChannelActive() and the cancelBufferedCommands() call reached from closeAsync(). In the reconnect path, CommandHandler.channelActive() never reaches super.channelActive(), so the downstream watchdog/event trigger never sees the new channel and buffered commands remain disconnected; in the close path, ST_CLOSED has already been set but the channel and closeFuture have not been completed, causing every later close to return the same permanently incomplete future. Handle this timeout in both paths as is now done for inactive/drain handling.

Useful? React with 👍 / 👎.

@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.

Stale Bugbot comment from a previous run.

…ufferedCommands against lock timeout (redis#3880)

- Catch RedisException in DefaultEndpoint.notifyChannelActive() to activate directly and ensure super.channelActive() always runs
- Guard cancelBufferedCommands() and closeAsync() so channel closure and closeFuture completion are never blocked by lock timeouts
- Fall back to direct command draining in flushCommands() when exclusive lock acquisition times out
@ronitgupta138

Copy link
Copy Markdown
Contributor Author

@a-TODO-rov Thanks for the review!

Addressed the feedback and bot findings across commits f25b3a1 and 3ac00d8:

  1. Java 8 Compatibility: Replaced the direct Thread.onSpinWait() reference with a Java 7-compatible dynamic MethodHandle lookup (MethodHandles.lookup().findStatic(...)) and graceful fallback to Thread.yield() on Java 8 runtimes. Validated cleanly under -source 1.8 -target 1.8.
  2. Channel Reconnection Path: Guarded DefaultEndpoint.notifyChannelActive() and CommandHandler.channelActive() so that if exclusive lock acquisition times out during reconnect, activation completes directly and Netty's super.channelActive(ctx) always runs, ensuring downstream watchdog and event triggers receive the channel active notification.
  3. Channel Close & Completion Path: Guarded cancelBufferedCommands() and closeAsync() so that lock acquisition timeouts during disconnect never skip channel closure or prevent closeFuture from completing.
  4. Command Flushing: Added fallback to direct command draining in flushCommands() if an exclusive acquire times out.

All unit tests (SharedLockUnitTests, DefaultEndpointUnitTests, CommandHandlerUnitTests) and Eclipse formatter checks pass cleanly.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3ac00d863b

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

sharedLock.doExclusive(activate);
} catch (RedisException e) {
logger.warn("{} Failed to acquire exclusive lock during notifyChannelActive, activating directly", logPrefix(), e);
activate.run();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Avoid a second lock wait in the activation fallback

When the shared writer is still held after the first 30-second deadline, the new activate.run() fallback is not actually lock-free: it reaches flushCommands(...), which calls sharedLock.doExclusive again before even checking whether the buffer is empty. The channel-active event loop therefore spins for a second full timeout (about 60 seconds total) before super.channelActive, so reconnect processing remains stalled; reuse the original timeout result or otherwise avoid starting a fresh lock-acquisition period here.

Useful? React with 👍 / 👎.

} catch (RedisException e) {
logger.warn("{} Failed to acquire exclusive lock during cancelBufferedCommands, draining commands directly",
logPrefix(), e);
cancelCommands(message, drainCommands(), RedisCommand::cancel);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Prevent writes from enqueueing after timed-out close

When an in-flight disconnected write is the writer that misses the deadline (for example, it is blocked in a configured replayFilter and resumes just after the timeout), this direct drain can finish before that command has been added. closeAsync then completes, but the writer resumes and writeToDisconnectedBuffer enqueues without rechecking ST_CLOSED; later closes only return the completed future, leaving the command queued indefinitely when command expiry is disabled. Ensure timed-out writers cannot enqueue after close or cancel them when they release.

Useful? React with 👍 / 👎.

Comment on lines +506 to +508
} catch (RedisException e) {
logger.warn("{} Failed to acquire exclusive lock during notifyChannelActive, activating directly", logPrefix(), e);
activate.run();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Distinguish lock timeout from activation RedisExceptions

When connectionFacade.activated() throws a RedisException after the exclusive lock was successfully acquired, this catch treats it as an acquisition timeout and executes the entire activation a second time. A streaming credentials provider or Pub/Sub resubscription that fails after partial side effects can therefore be subscribed or dispatched twice while the original failure is misreported; use a timeout-specific exception or result instead of catching the base client exception.

Useful? React with 👍 / 👎.

Comment on lines +333 to +335
} catch (Exception e) {
logger.warn("{} notifyChannelActive failed", logPrefix(), e);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Do not report a failed endpoint activation as active

If notifyChannelActive throws for a non-timeout activation error (for example, a streaming credentials provider throws from ConnectionFacade.activated()), this catch suppresses it and still invokes super.channelActive. In the inspected ConnectionBuilder pipeline, that publishes ConnectionActivatedEvent and resets the watchdog even though DefaultEndpoint set the channel but never reached flushCommands, so buffered commands can remain stuck while consumers are told the connection succeeded; propagate or close the channel for non-recoverable activation failures.

Useful? React with 👍 / 👎.

@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.

Stale Bugbot comment from a previous run.

… socket closure (redis#3880)

- Use try-finally in CommandHandler.channelInactive to guarantee setState(DEACTIVATED) and super.channelInactive(ctx) always run
- Use try-finally in DefaultEndpoint.closeAsync to guarantee socket closure and closeFuture completion without unlocked state fallbacks
- Revert unlocked mutation fallbacks in DefaultEndpoint and ClusterNodeEndpoint to prevent data races with concurrent writers
@ronitgupta138

Copy link
Copy Markdown
Contributor Author

Addressed the review findings in commit d6c23eb:

  1. Eliminated Unlocked Mutation Fallbacks: Reverted the direct un-synchronized fallbacks in DefaultEndpoint (notifyChannelActive, flushCommands, etc.). Bypassing SharedLock to mutate buffers or channel state while shared writers are active introduces data races with concurrent write() operations.
  2. Deterministic Lifecycle Teardown with try-finally:
    • In CommandHandler.channelInactive(), wrapped endpoint notification in a try-finally block so that setState(LifecycleState.DEACTIVATED) and super.channelInactive(ctx) are guaranteed to execute even if lock acquisition times out. This ensures ConnectionWatchdog always receives the event and schedules auto-reconnection without swallowing errors during channel activation.
    • In DefaultEndpoint.closeAsync(), wrapped cancelBufferedCommands("Close") in a try-finally block so that socket closure via channel.close() and closeFuture completion are guaranteed to proceed, preventing half-closed connections or permanently hung futures.
  3. No Double Reconnect Latency: Kept notifyChannelActive() atomic and single-pass without re-trying secondary 30s lock timeouts on reconnection.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d6c23eb216

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

return;
}

long deadline = System.nanoTime() + exclusiveLockTimeout.toNanos();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Include the outer lock wait in the timeout

When multiple threads call doExclusive while one shared writer remains stuck, every caller except the first blocks at the outer lock.lock() in doExclusive, and this deadline is created only after that wait. The first caller times out after 30 seconds, then the second receives a fresh 30-second window, so its configured 30-second acquisition can take about 60 seconds (and N queued lifecycle calls can take N times 30 seconds), continuing to stall Netty event loops; apply one deadline to both the initial lock acquisition and the writer-drain loop.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in commit f3e2af8: bounded outer lock acquisition via lock.tryLock(timeout), applying a single unified timeout budget across both the outer lock wait and the writer-drain spin loop.

@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.

Stale Bugbot comment from a previous run.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: aba86d243d

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +188 to +190
if (System.nanoTime() >= deadline) {
throw new RedisException(
"Failed to acquire exclusive lock: shared writers failed to drain within " + exclusiveLockTimeout);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Ensure cluster endpoints close after lock timeouts

When a cluster-node connection hits the stuck-writer case, this exception escapes ClusterNodeEndpoint.closeAsync(): its doExclusive(this::drainCommands) runs before super.closeAsync() (lines 50–58), so the endpoint's watchdog and channel are never closed. Through the normal RedisChannelHandler.closeAsync() path the handler has already set CLOSED before invoking the writer, leaving its closeFuture incomplete and making later close calls unable to retry; ensure the base endpoint close runs even when the cluster drain times out.

AGENTS.md reference: AGENTS.md:L86-L87

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in commit f3e2af8: ClusterNodeEndpoint.closeAsync() now guarantees that super.closeAsync() is always executed so the channel, watchdog, and closeFuture complete properly even if the cluster drain times out.

@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.

Stale Bugbot comment from a previous run.

…leanup (redis#3880)

- Bound outer lock acquisition with tryLock(timeout) so queued exclusive lock callers share the single configured timeout budget
- Guarantee this.channel = null in DefaultEndpoint.notifyChannelInactive via finally block even if deactivation times out
- Guarantee super.closeAsync() in ClusterNodeEndpoint.closeAsync() so watchdog and channels always close even if cluster drain times out

Signed-off-by: Ronit Gupta <[email protected]>
@ronitgupta138

Copy link
Copy Markdown
Contributor Author

Addressed the review findings in commit f3e2af8:

  1. Unified Lock Timeout: Updated SharedLock.lockWritersExclusive to use lock.tryLock(timeout, TimeUnit.NANOSECONDS), enforcing a single shared timeout budget across both outer lock contention and inner writer-drain spin loops.
  2. Guaranteed Channel Cleanup: Enclosed deactivation inside a try-finally block in DefaultEndpoint.notifyChannelInactive so this.channel = null is always cleared even if lock acquisition times out.
  3. Guaranteed Cluster Endpoint Closure: Wrapped command draining in ClusterNodeEndpoint.closeAsync() with cleanup error handling so super.closeAsync() always executes, ensuring channels, watchdogs, and completion futures close properly.
  4. Validation: Formatted and verified with mvn formatter:validate and mvn test -Dtest=SharedLockUnitTests (10/10 tests pass).

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f3e2af8553

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

setState(LifecycleState.DEACTIVATED);
PristineFallbackCommand command = this.fallbackCommand;
if (isProtectedMode(command)) {
onProtectedMode(command.getOutput().getError());

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Put inactive propagation in an inner finally

When a protected-mode fallback is present and the shared writer is still held, onProtectedMode() calls DefaultEndpoint.notifyException(), whose exclusive acquisition reaches the new timeout and throws. Because this call precedes super.channelInactive(ctx) inside the outer finally, this exception aborts the remainder of the block, so the downstream ConnectionWatchdog in ConnectionBuilder never sees the inactive event and reconnect is not scheduled. Fresh evidence beyond the earlier inactive-path report is this unguarded protected-mode acquisition in the new finally; put propagation in an inner finally.

Useful? React with 👍 / 👎.

Comment on lines +65 to +66
if (drainError != null) {
closeFuture.completeExceptionally(drainError);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Apply drain errors after the base close settles

When cluster draining fails while an open node channel is still closing, this completes the endpoint future immediately, before the ChannelFuture adapted by super.closeAsync() settles; the wrapping RedisChannelHandler can therefore fire its closed event and finish closeAsync() while the socket remains open. For an already-disconnected node whose base cleanup succeeds synchronously, the same future is already normally completed and completeExceptionally is a no-op, so the drain failure is reported as success instead. Compose a separate result that waits for the base close before applying drainError.

Useful? React with 👍 / 👎.

@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 high effort and found 1 potential issue.

Fix All in Cursor

Reviewed by Cursor Bugbot for commit f3e2af8. Configure here.

CompletableFuture<Void> closeFuture = super.closeAsync();
if (drainError != null) {
closeFuture.completeExceptionally(drainError);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cluster close loses drain failure

High Severity

closeFuture.completeExceptionally(drainError) runs only after super.closeAsync(). A drain timeout makes super.closeAsync() wait on the same exclusive lock again and throw, so completeExceptionally never runs. If the parent future is already completed (typical when the node is disconnected and the channel is null), completeExceptionally is a no-op and queued commands are not retriggered.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit f3e2af8. Configure here.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: df4a2ce1dd

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

}

return super.closeAsync();
CompletableFuture<Void> closeFuture = super.closeAsync();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Reuse the first timeout during cluster close

When a cluster-node writer remains held across both deadlines, the initial doExclusive(this::drainCommands) has already spun for 30 seconds before throwing, and this unconditional base close enters DefaultEndpoint.closeAsync(), where cancelBufferedCommands() starts a second doExclusive against the same writer. Fresh evidence beyond the earlier activation-path report is this sequential cluster-drain/base-close pair in the final tree; cluster shutdown or stale-node cleanup can therefore block and burn CPU for about 60 seconds per affected endpoint despite the 30-second bound, so carry the timeout result into a lock-free base cleanup instead of reacquiring the lock.

Useful? React with 👍 / 👎.

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.

SharedLock.lockWritersExclusive still spins RUNNABLE at 100% CPU on 6.8.2 when another thread's shared writers never drain (residual of #2905 / #2961)

3 participants