package com.atlassian.bitbucket.mesh.coordination;

import com.atlassian.bitbucket.mesh.git.exception.AbortedException;
import com.atlassian.bitbucket.mesh.network.MeshNode;

import javax.annotation.Nonnull;
import java.io.Closeable;
import java.util.concurrent.CompletionStage;

/**
 * Implementation of <a href="https://en.wikipedia.org/wiki/Maekawa%27s_algorithm">Maekawa's algorithm</a> for
 * implementing exclusive (write) access in a distributed system. Each node has a {@link WriteTokenRegistry} which
 * holds a single {@link WriteToken} for each repository.
 * <p>
 * Conceptually, a process or thread that needs exclusive access to the repository requests the token from each of
 * the replica nodes. Once a process obtains a write token from each participant node, that process has obtained
 * exclusive access until it releases the tokens.
 * <p>
 * If multiple nodes try to acquire a lock at the same time, the algorithm can deadlock unless special care is taken.
 * To avoid this, Maekawa's algorithm allows a node to request a granted token to be (temporarily) returned so it
 * can be granted to a higher priority thread or process. As long as all participants see the same token requests
 * and apply the same priority, the algorithm will not deadlock.
 * <p>
 * We use the token request's logical timestamp to establish priority order. If two requests have the same timestamp,
 * we use the requesting transaction ID as the secondary sort key.
 * <p>
 * Note that {@link WriteLock} does <em>NOT</em> actively request write tokens from other nodes, notify other
 * nodes when the token is released, or listen for requests to temporarily yield a previously granted token.
 * It is more efficient to implement requesting, granting and releasing tokens in existing coordination flows
 * such as {@code TransactionService/Transact} without introducing separate RPCs for requesting, granting and
 * releasing tokens. For this reason, the user of {@link WriteLock} is expected to handle exchanging token related
 * messages and notifying the instance:
 * <ul>
 *     <li>After a {@link WriteLock} is {@link WriteTokenRegistry#startLock started}, the caller is expected
 *     to request tokens from the <em>other</em> replica nodes, using the {@link #getLogicalTimestamp() logical
 *     timestamp}. The token for the local node is automatically requested when a lock is started.</li>
 *     <li>When another node grants its token to this lock, {@link #onTokenGranted(MeshNode)} should be called.</li>
 *     <li>When another node requests this lock to yield a previously granted token, {@link #yieldToken(MeshNode)}
 *     should be called. If the lock has not yet been acquired, that will yield the token and return {@code true}.
 *     </li>
 *     <li>When caller is done with the lock (completed the critical section or aborted), the lock should
 *     be {@link #close() closed}. This will release this node's token. The caller should also take care that
 *     any tokens granted by other nodes are released (by calling {@link WriteToken.Promise#close()} for that
 *     token on the remote node).</li>
 * </ul>
 *
 * @see WriteToken
 * @see WriteTokenRegistry
 */
public interface WriteLock extends Closeable {

    /**
     * @return a {@link CompletionStage} that is completed when the lock is acquired.
     */
    @Nonnull
    CompletionStage<Void> asCompletionStage();

    /**
     * Waits until the lock is acquired.
     *
     * @throws AbortedException if the lock is {@link #close() closed} before the lock could be acquired
     * @throws InterruptedException if the thread was interrupted before the lock was acquired
     */
    void await() throws InterruptedException;

    /**
     * Discards the lock, removes the request for the write token from the local {@link WriteTokenRegistry} and
     * completes the {@link #asCompletionStage()} {@link CompletionStage} exceptionally with an {@link AbortedException}
     * if the lock has not yet been acquired. This also releases any threads blocked in {@link #await()} by throwing
     * the same {@link AbortedException}.
     * <p>
     * Callers are expected to release the token from other nodes over RPC.
     */
    @Override
    void close();

    /**
     * @return the logical timestamp at which this {@link WriteLock} instance was created. Callers should use this
     *         timestamp when requesting the corresponding write tokens from other nodes.
     */
    long getLogicalTimestamp();

    /**
     * @param node the node that granted its write token
     * @return {@code true} if the lock accepts the token and now holds it
     */
    boolean onTokenGranted(@Nonnull MeshNode node);

    /**
     * Removes the provided node as a participant in the write operation. If the token for the provided repository
     * is held, it is immediately released. If the token was not held, removing the participant may trigger the lock
     * to be acquired (if the tokens for all remaining participants are held).
     *
     * @param node the node to remove as a participant
     * @return {@code true} if the write token for the node was released
     */
    boolean remove(@Nonnull MeshNode node);

    /**
     * @param node the node that wishes to revoke its token from the lock
     * @return {@code true} if the token was yielded by the lock
     */
    boolean yieldToken(@Nonnull MeshNode node);
}
