-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Bound exclusive lock acquisition in SharedLock to fail fast on a dead… #3882
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,11 +1,14 @@ | ||
| package io.lettuce.core.protocol; | ||
|
|
||
| import java.time.Duration; | ||
| import java.util.WeakHashMap; | ||
| import java.util.concurrent.TimeUnit; | ||
| import java.util.concurrent.atomic.AtomicLongFieldUpdater; | ||
| import java.util.concurrent.locks.Lock; | ||
| import java.util.concurrent.locks.ReentrantLock; | ||
| import java.util.function.Supplier; | ||
|
|
||
| import io.lettuce.core.RedisException; | ||
| import io.lettuce.core.internal.LettuceAssert; | ||
|
|
||
| /** | ||
|
|
@@ -43,6 +46,28 @@ class SharedLock { | |
|
|
||
| private volatile Thread exclusiveLockOwner; | ||
|
|
||
| private static final Duration DEFAULT_EXCLUSIVE_LOCK_TIMEOUT = Duration.ofSeconds(30); | ||
|
|
||
| private final long exclusiveLockTimeoutNanos; | ||
|
|
||
| /** | ||
| * Create a {@link SharedLock} with the {@link #DEFAULT_EXCLUSIVE_LOCK_TIMEOUT default} exclusive-lock timeout. | ||
| */ | ||
| SharedLock() { | ||
| this(DEFAULT_EXCLUSIVE_LOCK_TIMEOUT); | ||
| } | ||
|
|
||
| /** | ||
| * Create a {@link SharedLock} with a configurable exclusive-lock acquisition timeout. | ||
| * | ||
| * @param exclusiveLockTimeout the maximum time an exclusive acquisition waits before failing fast with a | ||
| * {@link RedisException}; must not be {@code null}. | ||
| */ | ||
| SharedLock(Duration exclusiveLockTimeout) { | ||
| LettuceAssert.notNull(exclusiveLockTimeout, "Exclusive lock timeout must not be null"); | ||
| this.exclusiveLockTimeoutNanos = exclusiveLockTimeout.toNanos(); | ||
| } | ||
|
|
||
| /** | ||
| * Wait for stateLock and increment writers. Will wait if stateLock is locked and if writer counter is negative. | ||
| */ | ||
|
|
@@ -108,12 +133,13 @@ <T> T doExclusive(Supplier<T> supplier) { | |
|
|
||
| LettuceAssert.notNull(supplier, "Supplier must not be null"); | ||
|
|
||
| lock.lock(); | ||
| long deadline = System.nanoTime() + exclusiveLockTimeoutNanos; | ||
| acquireExclusiveGuard(deadline); | ||
| try { | ||
|
|
||
| try { | ||
|
|
||
| lockWritersExclusive(); | ||
| lockWritersExclusive(deadline); | ||
|
cursor[bot] marked this conversation as resolved.
|
||
| return supplier.get(); | ||
| } finally { | ||
| unlockWritersExclusive(); | ||
|
|
@@ -124,10 +150,55 @@ <T> T doExclusive(Supplier<T> supplier) { | |
| } | ||
|
|
||
| /** | ||
|
cursor[bot] marked this conversation as resolved.
|
||
| * Wait for stateLock and no writers. Must be used in an outer {@code synchronized} block to prevent interleaving with other | ||
| * methods using writers. Sets writers to a negative value to create a lock for {@link #incrementWriters()}. | ||
| * Acquire the guarding {@link #lock} for an exclusive operation, bounded by {@code deadline}. If the lock was leaked by a | ||
| * thread that died inside the guarded region (issue #3804) a plain {@code lock.lock()} would park the caller - frequently a | ||
| * Netty event-loop thread - forever. Fail fast with a {@link RedisException} instead so the endpoint can be rebuilt. The | ||
| * acquisition remains reentrant, so a thread that already holds the lock re-acquires immediately. | ||
| * <p> | ||
| * Acquisition continues in a loop bounded by {@code deadline} while catching {@link InterruptedException} and restoring the | ||
| * interrupt flag afterwards, so callers with their interrupt flag already set (such as during | ||
| * {@code close()}/{@code closeAsync()}) do not fail an otherwise successful exclusive operation solely due to the interrupt | ||
| * under brief contention. | ||
| * | ||
| * @param deadline the {@link System#nanoTime()} timestamp by which the acquisition must complete. | ||
| */ | ||
| private void lockWritersExclusive() { | ||
| private void acquireExclusiveGuard(long deadline) { | ||
|
|
||
| boolean interrupted = false; | ||
| boolean acquired = lock.tryLock(); | ||
| try { | ||
| while (!acquired) { | ||
| long timeoutNanos = deadline - System.nanoTime(); | ||
| if (timeoutNanos <= 0) { | ||
| break; | ||
| } | ||
| try { | ||
| acquired = lock.tryLock(timeoutNanos, TimeUnit.NANOSECONDS); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When the caller enters Useful? React with 👍 / 👎. |
||
| } catch (InterruptedException e) { | ||
| interrupted = true; | ||
| } | ||
| } | ||
| } finally { | ||
| if (interrupted) { | ||
| Thread.currentThread().interrupt(); | ||
| } | ||
| } | ||
|
|
||
| if (!acquired) { | ||
| throw new RedisException("Timed out after " + TimeUnit.NANOSECONDS.toMillis(exclusiveLockTimeoutNanos) | ||
| + "ms acquiring the exclusive SharedLock; the lock holder likely died inside the guarded region. " | ||
| + "The endpoint must be rebuilt to recover."); | ||
|
Comment on lines
+188
to
+190
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When this timeout is reached from Useful? React with 👍 / 👎. |
||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Wait for stateLock and no writers, bounded by {@code deadline}. Must be used in an outer {@code synchronized} block to | ||
| * prevent interleaving with other methods using writers. Sets writers to a negative value to create a lock for | ||
| * {@link #incrementWriters()}. | ||
| * | ||
| * @param deadline the {@link System#nanoTime()} timestamp by which writer draining must complete. | ||
| */ | ||
| private void lockWritersExclusive(long deadline) { | ||
|
|
||
| if (exclusiveLockOwner == Thread.currentThread()) { | ||
| WRITERS.decrementAndGet(this); | ||
|
|
@@ -138,12 +209,36 @@ private void lockWritersExclusive() { | |
| try { | ||
| for (;;) { | ||
|
|
||
| // allow reentrant exclusive lock by comparing writers count and threadWriters count | ||
| // allow reentrant exclusive lock by comparing writers count and threadWriters | ||
| // count | ||
| int threadWriterCount = getThreadWriterCount(); | ||
| if (WRITERS.compareAndSet(this, threadWriterCount, -1)) { | ||
| exclusiveLockOwner = Thread.currentThread(); | ||
| return; | ||
| } | ||
|
|
||
| // A leaked shared writer (a thread that died before decrementWriters()) keeps | ||
| // the writer count above this thread's own count, so the CAS above can never | ||
| // succeed. | ||
| // Or if exclusive mode was abandoned (writers is negative, e.g. a previous | ||
| // exclusive holder died), | ||
| // the CAS also cannot succeed. Bound the spin and fail fast instead of burning | ||
| // a CPU core forever | ||
| // (issues #3804, #3880). | ||
| if (System.nanoTime() - deadline >= 0) { | ||
| long currentWriters = WRITERS.get(this); | ||
| if (currentWriters < 0) { | ||
| throw new RedisException("Timed out after " + TimeUnit.NANOSECONDS.toMillis(exclusiveLockTimeoutNanos) | ||
| + "ms acquiring the exclusive SharedLock; the exclusive lock was likely abandoned by a previous holder. " | ||
| + "The endpoint must be rebuilt to recover."); | ||
| } | ||
|
|
||
| throw new RedisException("Timed out after " + TimeUnit.NANOSECONDS.toMillis(exclusiveLockTimeoutNanos) | ||
| + "ms waiting for " + currentWriters + " shared writer(s) to drain while acquiring the " | ||
| + "exclusive SharedLock; a shared writer was likely leaked. The endpoint must be rebuilt to recover."); | ||
|
cursor[bot] marked this conversation as resolved.
Comment on lines
+231
to
+238
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What happens to the connection after exception is thrown ?
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When a
|
||
| } | ||
|
|
||
| Thread.yield(); | ||
| } | ||
| } finally { | ||
| lock.unlock(); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When
closeAsync()is called while disconnected (channel == null) and there are commands indisconnectedBuffer, the new SharedLock timeout path makescancelBufferedCommands()throw beforedrainCommands()runs; this branch only completes the close future exceptionally. Because no channel close/inactive callback follows in the null-channel case, those queued command futures remain in the buffer and never complete after close returns, so the timeout fallback should also drain/fail the buffered commands or otherwise complete them.Useful? React with 👍 / 👎.