
com.hazelcast.cp.lock.FencedLock Maven / Gradle / Ivy
/*
* Copyright (c) 2008-2024, Hazelcast, Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hazelcast.cp.lock;
import com.hazelcast.core.DistributedObject;
import com.hazelcast.cp.CPGroupId;
import com.hazelcast.cp.CPSubsystem;
import com.hazelcast.cp.lock.exception.LockAcquireLimitReachedException;
import com.hazelcast.cp.lock.exception.LockOwnershipLostException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
/**
* A linearizable & distributed & reentrant implementation of {@link Lock}.
*
* {@link FencedLock} is accessed via {@link CPSubsystem#getLock(String)}.
*
* {@link FencedLock} is CP with respect to the CAP principle. It works on top
* of the Raft consensus algorithm. It offers linearizability during crash-stop
* failures and network partitions. If a network partition occurs, it remains
* available on at most one side of the partition.
*
* {@link FencedLock} works on top of CP sessions.
*
* By default, {@link FencedLock} is reentrant. Once a caller acquires
* the lock, it can acquire the lock reentrantly as many times as it wants
* in a linearizable manner. You can configure the reentrancy behaviour via
* server side fenced lock configuration. For instance, reentrancy can be disabled and
* {@link FencedLock} can work as a non-reentrant mutex. One can also set
* a custom reentrancy limit. When the reentrancy limit is reached,
* {@link FencedLock} does not block a lock call. Instead, it fails with
* {@link LockAcquireLimitReachedException} or a specified return value.
* Please check the locking methods to see details about the behaviour.
*
* Distributed locks are unfortunately NOT EQUIVALENT to single-node mutexes
* because of the complexities in distributed systems, such as uncertain
* communication patterns, and independent and partial failures.
* In an asynchronous network, no lock service can guarantee mutual exclusion,
* because there is no way to distinguish between a slow and a crashed process.
* Consider the following scenario, where a Hazelcast client acquires
* a {@link FencedLock}, then hits a long GC pause. Since it will not be able
* to commit session heartbeats while paused, its CP session will be eventually
* closed. After this moment, another Hazelcast client can acquire this lock.
* If the first client wakes up again, it may not immediately notice that it
* has lost ownership of the lock. In this case, multiple clients think they
* hold the lock. If they attempt to perform an operation on a shared resource,
* they can break the system. To prevent such situations, you can choose to use
* an infinite session timeout, but this time probably you are going to deal
* with liveliness issues. For the scenario above, even if the first client
* actually crashes, requests sent by 2 clients can be re-ordered in the network
* and hit the external resource in reverse order.
*
* There is a simple solution for this problem. Lock holders are ordered by a
* monotonic fencing token, which increments each time the lock is assigned to
* a new owner. This fencing token can be passed to external services or
* resources to ensure sequential execution of side effects performed by lock
* holders.
*
* The following figure illustrates the idea. Client-1 acquires the lock first
* and receives 1 as its fencing token. Then, it passes this token to the
* external service, which is our shared resource in this scenario.
* Just after that, Client-1 hits a long GC pause and eventually loses
* ownership of the lock because it misses to commit CP session heartbeats.
* Then, Client-2 chimes in and acquires the lock. Similar to Client-1,
* Client-2 passes its fencing token to the external service. After that,
* once Client-1 comes back alive, its write request will be rejected
* by the external service, and only Client-2 will be able to safely talk it.
*
* CLIENT-1's session is expired.
* |
* |------------------| LOCK is acquired by CLIENT-1. | LOCK is acquired by CLIENT-2.
* | LOCK | . . . . . . . - - - - - - - - - - - - - - - - | . . + + + + + + + + + + + + + + + + + + + + + + + + + + +
* |------------------| /\ \ fence = 1 | /| \ fence = 2
* / \ / \
* |------------------| / \ | / \ | CLIENT-1 wakes up.
* | CLIENT-1 | . . . . ./. . . . \/. . .|_ _ _ _ _ _ _ _ _ _ /_ _ _ _ \ _ _ _ _|. . . . . . . . . . . . . . . . . . . .
* |------------------| lock() \ CLIENT-1 is paused. / \ write(A) \
* set_fence(1) \ / \ \
* |------------------| \ / \ \
* | CLIENT-2 | . . . . . . . . . . . . \ . . . . . . . . ./. . . . . . . . \/. . . . . . \ . . . . . . . . . . . . . . .
* |------------------| \ lock() \ \ write(B) \
* \ set_fence(2) \ \ \
* |------------------| \ | \ | \ \
* | EXTERNAL SERVICE | . . . . . . . . . . . . . . \/ |- - - - - - - - - - - - - - - - - \/ |+ + + + \/ + + + + + + \/ + + + +
* |------------------| | | write(A) fails. write(B) ok.
* | SERVICE belongs to CLIENT-1. | SERVICE belongs to CLIENT-2.
*
* You can read more about the fencing token idea in Martin Kleppmann's
* "How to do distributed locking" blog post and Google's Chubby paper.
* {@link FencedLock} integrates this idea with the {@link Lock}
* abstraction.
*
* All of the API methods in the new {@link FencedLock} abstraction offer
* exactly-once execution semantics. For instance, even if a {@link #lock()}
* call is internally retried because of a crashed CP member, the lock is
* acquired only once. The same rule also applies to the other methods
* in the API.
*
* @see LockOwnershipLostException
* @see LockAcquireLimitReachedException
*/
public interface FencedLock extends Lock, DistributedObject {
/**
* Representation of a failed lock attempt where
* the caller thread has not acquired the lock
*/
long INVALID_FENCE = 0L;
/**
* Acquires the lock.
*
* When the caller already holds the lock and the current lock() call is
* reentrant, the call can fail with
* {@link LockAcquireLimitReachedException} if the lock acquire limit is
* already reached.
*
* If the lock is not available then the current thread becomes disabled
* for thread scheduling purposes and lies dormant until the lock has been
* acquired.
*
* Consider the following scenario:
*
* FencedLock lock = ...;
* lock.lock();
* // JVM of the caller thread hits a long pause
* // and its CP session is closed on the CP group.
* lock.lock();
*
* In this scenario, a thread acquires the lock, then its JVM instance
* encounters a long pause, which is longer than
* session time to live. In this case,
* its CP session will be closed on the corresponding CP group because
* it could not commit session heartbeats in the meantime. After the JVM
* instance wakes up again, the same thread attempts to acquire the lock
* reentrantly. In this case, the second lock() call fails by throwing
* {@link LockOwnershipLostException} which extends
* {@link IllegalMonitorStateException}. If the caller wants to deal with
* its session loss by taking some custom actions, it can handle the thrown
* {@link LockOwnershipLostException} instance. Otherwise, it can treat it
* as a regular {@link IllegalMonitorStateException}.
*
* @throws LockOwnershipLostException if the underlying CP session is
* closed while locking reentrantly
* @throws LockAcquireLimitReachedException if the lock call is reentrant
* and the configured lock acquire limit is already reached.
*/
void lock();
/**
* Acquires the lock unless the current thread is
* {@linkplain Thread#interrupt interrupted}.
*
* When the caller already holds the lock and the current lock() call is
* reentrant, the call can fail with
* {@link LockAcquireLimitReachedException} if the lock acquire limit is
* already reached.
*
* If the lock is not available then the current thread becomes disabled
* for thread scheduling purposes and lies dormant until the lock has been
* acquired. Interruption may not be possible after the lock request
* arrives to the CP group, if the proxy does not attempt to retry its
* lock request because of a failure in the system.
*
* Please note that even if {@link InterruptedException} is thrown,
* the lock may be acquired on the CP group.
*
* When {@link InterruptedException} is thrown, the current thread's
* interrupted status is cleared.
*
* Consider the following scenario:
*
* FencedLock lock = ...;
* lock.lockInterruptibly();
* // JVM of the caller thread hits a long pause
* // and its CP session is closed on the CP group.
* lock.lockInterruptibly();
*
* In this scenario, a thread acquires the lock, then its JVM instance
* encounters a long pause, which is longer than
* session time to live. In this case,
* its CP session will be closed on the corresponding CP group because
* it could not commit session heartbeats in the meantime. After the JVM
* instance wakes up again, the same thread attempts to acquire the lock
* reentrantly. In this case, the second lock() call fails by throwing
* {@link LockOwnershipLostException} which extends
* {@link IllegalMonitorStateException}. If the caller wants to deal with
* its session loss by taking some custom actions, it can handle the thrown
* {@link LockOwnershipLostException} instance. Otherwise, it can treat it
* as a regular {@link IllegalMonitorStateException}.
*
* @throws InterruptedException if the current thread is interrupted while
* acquiring the lock.
*
* @throws LockOwnershipLostException if the underlying CP session is
* closed while locking reentrantly
* @throws LockAcquireLimitReachedException if the lock call is reentrant
* and the configured lock acquire limit is already reached.
*/
void lockInterruptibly() throws InterruptedException;
/**
* Acquires the lock and returns the fencing token assigned to the current
* thread for this lock acquire. If the lock is acquired reentrantly,
* the same fencing token is returned, or the lock() call can fail with
* {@link LockAcquireLimitReachedException} if the lock acquire limit is
* already reached.
*
* If the lock is not available then the current thread becomes disabled
* for thread scheduling purposes and lies dormant until the lock has been
* acquired.
*
* This is a convenience method for the following pattern:
*
* FencedLock lock = ...;
* lock.lock();
* return lock.getFence();
*
*
* Consider the following scenario where the lock is free initially:
*
* FencedLock lock = ...; // the lock is free
* lock.lockAndGetFence();
* // JVM of the caller thread hits a long pause
* // and its CP session is closed on the CP group.
* lock.lockAndGetFence();
*
* In this scenario, a thread acquires the lock, then its JVM instance
* encounters a long pause, which is longer than
* session time to live. In this case,
* its CP session will be closed on the corresponding CP group because
* it could not commit session heartbeats in the meantime. After the JVM
* instance wakes up again, the same thread attempts to acquire the lock
* reentrantly. In this case, the second lock() call fails by throwing
* {@link LockOwnershipLostException} which extends
* {@link IllegalMonitorStateException}. If the caller wants to deal with
* its session loss by taking some custom actions, it can handle the thrown
* {@link LockOwnershipLostException} instance. Otherwise, it can treat it
* as a regular {@link IllegalMonitorStateException}.
*
* Fencing tokens are monotonic numbers that are incremented each time
* the lock switches from the free state to the acquired state. They are
* simply used for ordering lock holders. A lock holder can pass
* its fencing to the shared resource to fence off previous lock holders.
* When this resource receives an operation, it can validate the fencing
* token in the operation.
*
* Consider the following scenario where the lock is free initially:
*
* FencedLock lock = ...; // the lock is free
* long fence1 = lock.lockAndGetFence(); // (1)
* long fence2 = lock.lockAndGetFence(); // (2)
* assert fence1 == fence2;
* lock.unlock();
* lock.unlock();
* long fence3 = lock.lockAndGetFence(); // (3)
* assert fence3 > fence1;
*
* In this scenario, the lock is acquired by a thread in the cluster. Then,
* the same thread reentrantly acquires the lock again. The fencing token
* returned from the second acquire is equal to the one returned from the
* first acquire, because of reentrancy. After the second acquire, the lock
* is released 2 times, hence becomes free. There is a third lock acquire
* here, which returns a new fencing token. Because this last lock acquire
* is not reentrant, its fencing token is guaranteed to be larger than the
* previous tokens, independent of the thread that has acquired the lock.
*
* @throws LockOwnershipLostException if the underlying CP session is
* closed while locking reentrantly
* @throws LockAcquireLimitReachedException if the lock call is reentrant
* and the configured lock acquire limit is already reached.
*/
long lockAndGetFence();
/**
* Acquires the lock if it is available or already held by the current
* thread at the time of invocation & the acquire limit is not exceeded,
* and immediately returns with the value {@code true}. If the lock is not
* available, then this method immediately returns with the value
* {@code false}. When the call is reentrant, it can return {@code false}
* if the lock acquire limit is exceeded.
*
* A typical usage idiom for this method would be:
*
* FencedLock lock = ...;
* if (lock.tryLock()) {
* try {
* // manipulate protected state
* } finally {
* lock.unlock();
* }
* } else {
* // perform alternative actions
* }
*
* This usage ensures that the lock is unlocked if it was acquired,
* and doesn't try to unlock if the lock was not acquired.
*
* @return {@code true} if the lock was acquired and
* {@code false} otherwise
*
* @throws LockOwnershipLostException if the underlying CP session is
* closed while locking reentrantly
*/
boolean tryLock();
/**
* Acquires the lock only if it is free or already held by the current
* thread at the time of invocation & the acquire limit is not exceeded,
* and returns the fencing token assigned to the current thread for this
* lock acquire. If the lock is acquired reentrantly, the same fencing
* token is returned. If the lock is already held by another caller or
* the lock acquire limit is exceeded, then this method immediately returns
* {@link #INVALID_FENCE} that represents a failed lock attempt.
*
* This is a convenience method for the following pattern:
*
* FencedLock lock = ...;
* if (lock.tryLock()) {
* return lock.getFence();
* } else {
* return FencedLock.INVALID_FENCE;
* }
*
*
* Consider the following scenario where the lock is free initially:
*
* FencedLock lock = ...; // the lock is free
* lock.tryLockAndGetFence();
* // JVM of the caller thread hits a long pause
* // and its CP session is closed on the CP group.
* lock.tryLockAndGetFence();
*
* In this scenario, a thread acquires the lock, then its JVM instance
* encounters a long pause, which is longer than
* session time to live. In this case,
* its CP session will be closed on the corresponding CP group because
* it could not commit session heartbeats in the meantime. After the JVM
* instance wakes up again, the same thread attempts to acquire the lock
* reentrantly. In this case, the second lock() call fails by throwing
* {@link LockOwnershipLostException} which extends
* {@link IllegalMonitorStateException}. If the caller wants to deal with
* its session loss by taking some custom actions, it can handle the thrown
* {@link LockOwnershipLostException} instance. Otherwise, it can treat it
* as a regular {@link IllegalMonitorStateException}.
*
* Fencing tokens are monotonic numbers that are incremented each time
* the lock switches from the free state to the acquired state. They are
* simply used for ordering lock holders. A lock holder can pass
* its fencing to the shared resource to fence off previous lock holders.
* When this resource receives an operation, it can validate the fencing
* token in the operation.
*
* Consider the following scenario where the lock is free initially:
*
* FencedLock lock = ...; // the lock is free
* long fence1 = lock.tryLockAndGetFence(); // (1)
* long fence2 = lock.tryLockAndGetFence(); // (2)
* assert fence1 == fence2;
* lock.unlock();
* lock.unlock();
* long fence3 = lock.tryLockAndGetFence(); // (3)
* assert fence3 > fence1;
*
* In this scenario, the lock is acquired by a thread in the cluster. Then,
* the same thread reentrantly acquires the lock again. The fencing token
* returned from the second acquire is equal to the one returned from the
* first acquire, because of reentrancy. After the second acquire, the lock
* is released 2 times, hence becomes free. There is a third lock acquire
* here, which returns a new fencing token. Because this last lock acquire
* is not reentrant, its fencing token is guaranteed to be larger than the
* previous tokens, independent of the thread that has acquired the lock.
*
* @return the fencing token if the lock was acquired and
* {@link #INVALID_FENCE} otherwise
*
* @throws LockOwnershipLostException if the underlying CP session is
* closed while locking reentrantly
*/
long tryLockAndGetFence();
/**
* Acquires the lock if it is free within the given waiting time,
* or already held by the current thread.
*
* If the lock is available, this method returns immediately with the value
* {@code true}. When the call is reentrant, it immediately returns
* {@code true} if the lock acquire limit is not exceeded. Otherwise,
* it returns {@code false} on the reentrant lock attempt if the acquire
* limit is exceeded.
*
* If the lock is not available then the current thread becomes disabled
* for thread scheduling purposes and lies dormant until the lock is
* acquired by the current thread or the specified waiting time elapses.
*
* If the lock is acquired, then the value {@code true} is returned.
*
* If the specified waiting time elapses, then the value {@code false}
* is returned. If the time is less than or equal to zero, the method does
* not wait at all.
*
* @param time the maximum time to wait for the lock
* @param unit the time unit of the {@code time} argument
* @return {@code true} if the lock was acquired and {@code false}
* if the waiting time elapsed before the lock was acquired
*
* @throws LockOwnershipLostException if the underlying CP session is
* closed while locking reentrantly
*/
boolean tryLock(long time, TimeUnit unit);
/**
* Acquires the lock if it is free within the given waiting time,
* or already held by the current thread at the time of invocation &
* the acquire limit is not exceeded, and returns the fencing token
* assigned to the current thread for this lock acquire. If the lock is
* acquired reentrantly, the same fencing token is returned. If the lock
* acquire limit is exceeded, then this method immediately returns
* {@link #INVALID_FENCE} that represents a failed lock attempt.
*
* If the lock is not available then the current thread becomes disabled
* for thread scheduling purposes and lies dormant until the lock is
* acquired by the current thread or the specified waiting time elapses.
*
* If the specified waiting time elapses, then {@link #INVALID_FENCE}
* is returned. If the time is less than or equal to zero, the method does
* not wait at all.
*
* This is a convenience method for the following pattern:
*
* FencedLock lock = ...;
* if (lock.tryLock(time, unit)) {
* return lock.getFence();
* } else {
* return FencedLock.INVALID_FENCE;
* }
*
*
* Consider the following scenario where the lock is free initially:
*
* FencedLock lock = ...; // the lock is free
* lock.tryLockAndGetFence(time, unit);
* // JVM of the caller thread hits a long pause and its CP session
* is closed on the CP group.
* lock.tryLockAndGetFence(time, unit);
*
* In this scenario, a thread acquires the lock, then its JVM instance
* encounters a long pause, which is longer than
* session time to live. In this case,
* its CP session will be closed on the corresponding CP group because
* it could not commit session heartbeats in the meantime. After the JVM
* instance wakes up again, the same thread attempts to acquire the lock
* reentrantly. In this case, the second lock() call fails by throwing
* {@link LockOwnershipLostException} which extends
* {@link IllegalMonitorStateException}. If the caller wants to deal with
* its session loss by taking some custom actions, it can handle the thrown
* {@link LockOwnershipLostException} instance. Otherwise, it can treat it
* as a regular {@link IllegalMonitorStateException}.
*
* Fencing tokens are monotonic numbers that are incremented each time
* the lock switches from the free state to the acquired state. They are
* simply used for ordering lock holders. A lock holder can pass
* its fencing to the shared resource to fence off previous lock holders.
* When this resource receives an operation, it can validate the fencing
* token in the operation.
*
* Consider the following scenario where the lock is free initially:
*
* FencedLock lock = ...; // the lock is free
* long fence1 = lock.tryLockAndGetFence(time, unit); // (1)
* long fence2 = lock.tryLockAndGetFence(time, unit); // (2)
* assert fence1 == fence2;
* lock.unlock();
* lock.unlock();
* long fence3 = lock.tryLockAndGetFence(time, unit); // (3)
* assert fence3 > fence1;
*
* In this scenario, the lock is acquired by a thread in the cluster. Then,
* the same thread reentrantly acquires the lock again. The fencing token
* returned from the second acquire is equal to the one returned from the
* first acquire, because of reentrancy. After the second acquire, the lock
* is released 2 times, hence becomes free. There is a third lock acquire
* here, which returns a new fencing token. Because this last lock acquire
* is not reentrant, its fencing token is guaranteed to be larger than the
* previous tokens, independent of the thread that has acquired the lock.
*
* @param time the maximum time to wait for the lock
* @param unit the time unit of the {@code time} argument
* @return the fencing token if the lock was acquired and
* {@link #INVALID_FENCE} otherwise
*
* @throws LockOwnershipLostException if the underlying CP session is
* closed while locking reentrantly
*/
long tryLockAndGetFence(long time, TimeUnit unit);
/**
* Releases the lock if the lock is currently held by the current thread.
*
* @throws IllegalMonitorStateException if the lock is not held by
* the current thread
* @throws LockOwnershipLostException if the underlying CP session is
* closed before the current thread releases the lock
*/
void unlock();
/**
* Returns the fencing token if the lock is held by the current thread.
*
* Fencing tokens are monotonic numbers that are incremented each time
* the lock switches from the free state to the acquired state. They are
* simply used for ordering lock holders. A lock holder can pass
* its fencing to the shared resource to fence off previous lock holders.
* When this resource receives an operation, it can validate the fencing
* token in the operation.
*
* @return the fencing token if the lock is held by the current thread
*
* @throws IllegalMonitorStateException if the lock is not held by
* the current thread
* @throws LockOwnershipLostException if the underlying CP session is
* closed while the current thread is holding the lock
*/
long getFence();
/**
* Returns whether this lock is locked or not.
*
* @return {@code true} if this lock is locked by any thread
* in the cluster, {@code false} otherwise.
*
* @throws LockOwnershipLostException if the underlying CP session is
* closed while the current thread is holding the lock
*/
boolean isLocked();
/**
* Returns whether the lock is held by the current thread or not.
*
* @return {@code true} if the lock is held by the current thread or not,
* {@code false} otherwise.
*
* @throws LockOwnershipLostException if the underlying CP session is
* closed while the current thread is holding the lock
*/
boolean isLockedByCurrentThread();
/**
* Returns the reentrant lock count if the lock is held by any thread
* in the cluster.
*
* @return the reentrant lock count if the lock is held by any thread
* in the cluster
*
* @throws LockOwnershipLostException if the underlying CP session is
* closed while the current thread is holding the lock
*/
int getLockCount();
/**
* Returns id of the CP group that runs this {@link FencedLock} instance
*
* @return id of the CP group that runs this {@link FencedLock} instance
*/
CPGroupId getGroupId();
/**
* NOT IMPLEMENTED. Fails by throwing {@link UnsupportedOperationException}.
*
* May the force be the one who dares to implement
* a linearizable distributed {@link Condition} :)
*
* @throws UnsupportedOperationException for now
*/
Condition newCondition();
}