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

io.atomix.concurrent.DistributedLock Maven / Gradle / Ivy

/*
 * Copyright 2015 the original author or authors.
 *
 * 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 io.atomix.concurrent;

import io.atomix.catalyst.concurrent.BlockingFuture;
import io.atomix.concurrent.internal.LockCommands;
import io.atomix.concurrent.util.DistributedLockFactory;
import io.atomix.copycat.client.CopycatClient;
import io.atomix.resource.AbstractResource;
import io.atomix.resource.Resource;
import io.atomix.resource.ResourceTypeInfo;

import java.time.Duration;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * Facilitates synchronizing access to cluster-wide shared resources.
 * 

* The distributed lock resource provides a mechanism for nodes to synchronize access to cluster-wide shared resources. * This interface is an asynchronous version of Java's {@link java.util.concurrent.locks.Lock}. *

 *   {@code
 *   atomix.getLock("my-lock").thenAccept(lock -> {
 *     lock.lock().thenRun(() -> {
 *       ...
 *       lock.unlock();
 *     });
 *   });
 *   }
 * 
* Locks are implemented as a simple replicated queue that tracks which client currently holds a lock. When a lock * is {@link #lock() locked}, if the lock is available then the requesting resource instance will immediately receive * the lock, otherwise the lock request will be queued. When a lock is {@link #unlock() unlocked}, the next lock requester * will be granted the lock. *

* Distributed locks require no polling from the client. Locks are granted via session events published by the Atomix * cluster to the lock instance. In the event that a lock's client becomes disconnected from the cluster, its session * will expire after the configured cluster session timeout and the lock will be automatically released. *

Detecting failures

* Once a lock is acquired by a client, the cluster will monitor the lock holder's availability and release the lock * automatically if the client becomes disconnected from the cluster. However, in the event that a lock holder becomes * disconnected without crashing, it's possible for two processes to believe themselves to hold the lock simultaneously. * If the lock holder becomes disconnected the cluster may grant the lock to another process. For this reason it's essential * that clients monitor the {@link io.atomix.resource.Resource.State State} of the lock. If the resource transitions to the * {@link Resource.State#SUSPENDED} state, that indicates that the underlying client is unable to communicate with the * cluster and another process may have been granted the lock. Lock holders should monitor the resource for state changes * and release the lock if the resource becomes suspended. *

*

 *   {@code
 *   DistributedLock lock = atomix.getLock("my-lock").get();
 *
 *   lock.lock().thenRun(() -> {
 *     lock.onStateChange(state -> {
 *       if (state == DistributedLock.State.SUSPENDED) {
 *         lock.unlock();
 *         System.out.println("lost the lock");
 *       }
 *     });
 *     // Do stuff
 *   });
 *   }
 * 
*

Fencing

* Detecting state changes in the lock client's session may not always be sufficient for determining whether the * local resource instance is the unique lock holder in the cluster. For instance, a lengthy garbage collection * pause can result in the client's session expiring without any change to the client's session state, resulting * in another process being granted the lock. To account for arbitrary lapses in time, the {@code DistributedLock} * provides a monotonically increasing, globally unique identifier to each process granted the lock. The token can * be used for optimistic concurrency control when accessing external resources. *
 *   {@code
 *   lock.lock().thenAccept(token -> {
 *     // Write to external data store, checking that the last written token is not greater than the current token
 *   });
 *   }
 * 
* This is known as a fencing token. When using the lock to control concurrent writes to e.g. an external data * store, the monotonically increasing identifier provided when the lock is granted allows lock holders to * optimistically determine whether the lock has been granted to a more recent lock requester that has written * to the data store. If the last write to the external data store is greater than the local lock's token, that * indicates that another process has been granted the lock. *

Implementation

* Lock state management is implemented in a Copycat replicated {@link io.atomix.copycat.server.StateMachine}. * When a lock is created, an instance of the lock state machine is created on each replica in the cluster. * The state machine instance manages state for the specific lock. When a client makes a {@link #lock()} * request, it submits the request with a monotonically increasing identifier unique to the resource instance. * If the lock is not currently held by another process, the state machine grants the lock to the * requester and {@link io.atomix.copycat.server.session.ServerSession#publish(String, Object) publishes} * a {@code lock} event with the requested lock ID and a globally unique, monotonically increasing token to * the client. If the lock is held by another process, the lock request is enqueued to await availability of * the lock. *

* When a client-side {@code DistributedLock} receives a published {@code lock} event from the cluster, the * client associates the lock ID with a lock requested by the client and grants the lock to the requester. * The globally unique, monotonically increasing token provided by the cluster is provided to the user as * a fencing token. Because of garbage collection and other types of process pauses, we cannot guarantee that * two clients cannot perceive themselves to hold the same lock at the same time. The fencing token provides * a mechanism for optimistic locking when interacting with external resources. *

* When a lock holder {@link #unlock() unlocks} a lock, a second request is sent to the cluster. The {@code unlock} * request is logged and replicated to a majority of the cluster and applied to the replicated state machine. * If another lock requester is awaiting the lock, the state machine will grant the lock to the next requester * in the queue. *

* Because a lock holder may become partitioned or crash before releasing a lock, the lock state machine is responsible * for tracking which session currently holds the lock. In the event that the lock holder is partitioned or crashes, * its session will eventually be expired by the state machine, and the lock will be granted to the next requester * in the queue. *

* The lock state machine manages compaction in the replicated log by tracking which {@code lock} and {@code unlock} * requests contribute to the state of the lock. As long as a client holds a lock, the commit requesting the lock * will be retained in the replicated log. And as long as a client's request to acquire a lock is held in the state * machine's lock queue, the commit requesting the lock will be retained in the replicated log. This ensures that if * a replica crashes and recovers it will recover the correct state of the lock. Once a lock is released by a client, * both the {@code lock} and {@code unlock} commit associated with the lock will be released from the state machine * and eventually removed from the log during compaction. * * @author Jordan Halterman */ @ResourceTypeInfo(id=-22, factory=DistributedLockFactory.class) public class DistributedLock extends AbstractResource { private final Map> futures = new ConcurrentHashMap<>(); private final AtomicInteger id = new AtomicInteger(); private int lock; public DistributedLock(CopycatClient client, Properties options) { super(client, options); } @Override public CompletableFuture open() { return super.open().thenApply(result -> { client.onEvent("lock", this::handleEvent); client.onEvent("fail", this::handleFail); return result; }); } /** * Handles a received lock event. */ private void handleEvent(LockCommands.LockEvent event) { CompletableFuture future = futures.get(event.id()); if (future != null) { this.lock = event.id(); future.complete(event.version()); } } /** * Handles a received failure event. */ private void handleFail(LockCommands.LockEvent event) { CompletableFuture future = futures.get(event.id()); if (future != null) { future.complete(null); } } /** * Acquires the lock. *

* When the lock is acquired, this lock instance will publish a lock request to the cluster and await * an event granting the lock to this instance. The returned {@link CompletableFuture} will not be completed * until the lock has been acquired. *

* Once the lock is granted, the returned future will be completed with a positive {@code Long} value. This value * is guaranteed to be unique across all clients and monotonically increasing. Thus, the value can be used as a * fencing token for further concurrency control. *

* This method returns a {@link CompletableFuture} which can be used to block until the operation completes * or to be notified in a separate thread once the operation completes. To block until the operation completes, * use the {@link CompletableFuture#join()} method to block the calling thread: *

   *   {@code
   *   lock.lock().join();
   *   }
   * 
* Alternatively, to execute the operation asynchronous and be notified once the lock is acquired in a different * thread, use one of the many completable future callbacks: *
   *   {@code
   *   lock.lock().thenRun(() -> System.out.println("Lock acquired!"));
   *   }
   * 
* * @return A completable future to be completed once the lock has been acquired. */ public CompletableFuture lock() { CompletableFuture future = new BlockingFuture<>(); int id = this.id.incrementAndGet(); futures.put(id, future); client.submit(new LockCommands.Lock(id, -1)).whenComplete((result, error) -> { if (error != null) { futures.remove(id); future.completeExceptionally(error); } }); return future; } /** * Attempts to acquire the lock if available. *

* When the lock is acquired, this lock instance will publish an immediate lock request to the cluster. If the * lock is available, the lock will be granted and the returned {@link CompletableFuture} will be completed * successfully. If the lock is not immediately available, the {@link CompletableFuture} will be completed * with a {@code null} value. *

* If the lock is granted, the returned future will be completed with a positive {@code Long} value. This value * is guaranteed to be unique across all clients and monotonically increasing. Thus, the value can be used as a * fencing token for further concurrency control. *

* This method returns a {@link CompletableFuture} which can be used to block until the operation completes * or to be notified in a separate thread once the operation completes. To block until the operation completes, * use the {@link CompletableFuture#get()} method to block the calling thread: *

   *   {@code
   *   if (lock.tryLock().get()) {
   *     System.out.println("Lock acquired!");
   *   } else {
   *     System.out.println("Lock failed!");
   *   }
   *   }
   * 
* Alternatively, to execute the operation asynchronous and be notified once the lock is acquired in a different * thread, use one of the many completable future callbacks: *
   *   {@code
   *   lock.tryLock().thenAccept(locked -> {
   *     if (locked) {
   *       System.out.println("Lock acquired!");
   *     } else {
   *       System.out.println("Lock failed!");
   *     }
   *   });
   *   }
   * 
* * * @return A completable future to be completed with a boolean indicating whether the lock was acquired. */ public CompletableFuture tryLock() { CompletableFuture future = new BlockingFuture<>(); int id = this.id.incrementAndGet(); futures.put(id, future); client.submit(new LockCommands.Lock(id, 0)).whenComplete((result, error) -> { if (error != null) { futures.remove(id); future.completeExceptionally(error); } }); return future; } /** * Attempts to acquire the lock if available within the given timeout. *

* When the lock is acquired, this lock instance will publish an immediate lock request to the cluster. If the * lock is available, the lock will be granted and the returned {@link CompletableFuture} will be completed * successfully. If the lock is not immediately available, the lock request will be queued until the lock comes * available. If the lock {@code timeout} expires, the lock request will be cancelled and the returned * {@link CompletableFuture} will be completed successfully with a {@code null} result. *

* If the lock is granted, the returned future will be completed with a positive {@code Long} value. This value * is guaranteed to be unique across all clients and monotonically increasing. Thus, the value can be used as a * fencing token for further concurrency control. *

* Timeouts and wall-clock time * The provided {@code timeout} may not ultimately be representative of the actual timeout in the cluster. Because * of clock skew and determinism requirements, the actual timeout may be arbitrarily greater, but not less, than * the provided {@code timeout}. However, time will always progress monotonically. That is, time will never go * in reverse. A timeout of {@code 10} seconds will always be greater than a timeout of {@code 9} seconds, but the * times simply may not match actual wall-clock time. *

* This method returns a {@link CompletableFuture} which can be used to block until the operation completes * or to be notified in a separate thread once the operation completes. To block until the operation completes, * use the {@link CompletableFuture#get()} method to block the calling thread: *

   *   {@code
   *   if (lock.tryLock(Duration.ofSeconds(10)).get()) {
   *     System.out.println("Lock acquired!");
   *   } else {
   *     System.out.println("Lock failed!");
   *   }
   *   }
   * 
* Alternatively, to execute the operation asynchronous and be notified once the lock is acquired in a different * thread, use one of the many completable future callbacks: *
   *   {@code
   *   lock.tryLock(Duration.ofSeconds(10)).thenAccept(locked -> {
   *     if (locked) {
   *       System.out.println("Lock acquired!");
   *     } else {
   *       System.out.println("Lock failed!");
   *     }
   *   });
   *   }
   * 
* * @param timeout The duration within which to acquire the lock. * @return A completable future to be completed with a value indicating whether the lock was acquired. */ public CompletableFuture tryLock(Duration timeout) { CompletableFuture future = new BlockingFuture<>(); int id = this.id.incrementAndGet(); futures.put(id, future); client.submit(new LockCommands.Lock(id, timeout.toMillis())).whenComplete((result, error) -> { if (error != null) { futures.remove(id); future.completeExceptionally(error); } }); return future; } /** * Releases the lock. *

* When the lock is released, the lock instance will publish an unlock request to the cluster. Once the lock has * been released, if any other instances of this resource are waiting for a lock on this or another node, the lock * will be acquired by the waiting instance before this unlock operation is completed. Once the lock has been released * and granted to any waiters, the returned {@link CompletableFuture} will be completed. *

* This method returns a {@link CompletableFuture} which can be used to block until the operation completes * or to be notified in a separate thread once the operation completes. To block until the operation completes, * use the {@link CompletableFuture#join()} method to block the calling thread: *

   *   {@code
   *   lock.unlock().join();
   *   }
   * 
* Alternatively, to execute the operation asynchronous and be notified once the lock is acquired in a different * thread, use one of the many completable future callbacks: *
   *   {@code
   *   lock.unlock().thenRun(() -> System.out.println("Lock released!"));
   *   }
   * 
* * @return A completable future to be completed once the lock has been released. */ public CompletableFuture unlock() { int lock = this.lock; this.lock = 0; if (lock != 0) { return client.submit(new LockCommands.Unlock(lock)); } return CompletableFuture.completedFuture(null); } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy