All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.hazelcast.cp.lock.FencedLock Maven / Gradle / Ivy

The newest version!
/*
 * 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(); }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy