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

io.hekate.lock.internal.LockControllerClient Maven / Gradle / Ivy

/*
 * Copyright 2022 The Hekate Project
 *
 * The Hekate Project licenses this file to you 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.hekate.lock.internal;

import io.hekate.cluster.ClusterHash;
import io.hekate.cluster.ClusterNode;
import io.hekate.cluster.ClusterNodeId;
import io.hekate.cluster.ClusterTopology;
import io.hekate.lock.LockOwnerInfo;
import io.hekate.lock.internal.LockProtocol.LockOwnerRequest;
import io.hekate.lock.internal.LockProtocol.LockOwnerResponse;
import io.hekate.lock.internal.LockProtocol.LockRequest;
import io.hekate.lock.internal.LockProtocol.LockResponse;
import io.hekate.lock.internal.LockProtocol.UnlockRequest;
import io.hekate.lock.internal.LockProtocol.UnlockResponse;
import io.hekate.messaging.MessagingChannel;
import io.hekate.messaging.operation.RequestRetryConfigurer;
import io.hekate.messaging.operation.Response;
import io.hekate.partition.PartitionMapper;
import io.hekate.util.format.ToString;
import io.hekate.util.format.ToStringIgnore;
import java.util.concurrent.CancellationException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

class LockControllerClient {
    enum Status {
        LOCKING,

        LOCKED,

        UNLOCKING,

        UNLOCKED,

        TERMINATED
    }

    private static final Logger log = LoggerFactory.getLogger(LockControllerClient.class);

    private static final boolean DEBUG = log.isDebugEnabled();

    private static final boolean TRACE = log.isTraceEnabled();

    private final LockKey key;

    private final long lockId;

    private final long threadId;

    private final ClusterNodeId localNode;

    private final long lockTimeout;

    @ToStringIgnore
    private final MessagingChannel channel;

    @ToStringIgnore
    private final Object mux = new Object();

    @ToStringIgnore
    private final LockFuture lockFuture;

    @ToStringIgnore
    private final LockFuture unlockFuture;

    @ToStringIgnore
    private final LockRegionMetrics metrics;

    @ToStringIgnore
    private final AsyncLockCallbackAdaptor asyncCallback;

    @ToStringIgnore
    private ClusterTopology topology;

    @ToStringIgnore
    private LockOwnerInfo lockOwner;

    @ToStringIgnore
    private ClusterNodeId manager;

    private Status status = Status.UNLOCKED;

    public LockControllerClient(
        long lockId,
        String region,
        String name,
        ClusterNodeId localNode,
        long threadId,
        MessagingChannel channel,
        long lockTimeout,
        LockRegionMetrics metrics,
        AsyncLockCallbackAdaptor asyncCallback
    ) {
        this.key = new LockKey(region, name);
        this.lockId = lockId;
        this.localNode = localNode;
        this.threadId = threadId;
        this.channel = channel;
        this.lockTimeout = lockTimeout;
        this.metrics = metrics;
        this.asyncCallback = asyncCallback;

        lockFuture = new LockFuture(this);
        unlockFuture = new LockFuture(this);
    }

    public LockKey key() {
        return key;
    }

    public long lockId() {
        return lockId;
    }

    public long threadId() {
        return threadId;
    }

    public ClusterNodeId manager() {
        synchronized (mux) {
            return manager;
        }
    }

    public LockFuture lockFuture() {
        return lockFuture;
    }

    public LockFuture unlockFuture() {
        return unlockFuture;
    }

    public ClusterNodeId localNode() {
        return localNode;
    }

    public void update(PartitionMapper mapping) {
        if (mapping != null) {
            synchronized (mux) {
                this.topology = mapping.topology();

                this.manager = mapping.map(key).primaryNode().id();

                if (TRACE) {
                    log.trace("Updated partition mapping [key={}, manager={}, topology={}]", key, manager, topology);
                }
            }
        }
    }

    public boolean updateAndCheckLocked(ClusterTopology topology) {
        synchronized (mux) {
            this.topology = topology;

            if (DEBUG) {
                log.trace("Updated topology [key={}, topology={}]", key, topology);
            }

            return status == Status.LOCKED;
        }
    }

    public void becomeLocking(PartitionMapper mapping) {
        synchronized (mux) {
            status = Status.LOCKING;

            if (DEBUG) {
                log.debug("Became {} [key={}]", status, key);
            }

            update(mapping);

            remoteLock();
        }
    }

    public LockFuture becomeUnlocking() {
        doBecomeUnlocking(false);

        return unlockFuture;
    }

    public void becomeUnlockingIfNotLocked() {
        doBecomeUnlocking(true);
    }

    public void becomeTerminated() {
        synchronized (mux) {
            status = Status.TERMINATED;

            if (DEBUG) {
                log.debug("Became {} [key={}]", status, key);
            }

            if (!lockFuture.isDone()) {
                lockFuture.completeExceptionally(new CancellationException("Lock service terminated."));
            }

            if (unlockFuture.complete(true)) {
                metrics.onUnlock();

                if (asyncCallback != null) {
                    asyncCallback.onLockRelease();
                }
            }
        }
    }

    private void doBecomeUnlocking(boolean ignoreIfLocked) {
        synchronized (mux) {
            switch (status) {
                case LOCKING: {
                    status = Status.UNLOCKING;

                    if (DEBUG) {
                        log.debug("Became {} [key={}]", status, key);
                    }

                    if (!lockFuture.isDone()) {
                        lockFuture.complete(false);
                    }

                    remoteUnlock();

                    break;
                }
                case LOCKED: {
                    if (!ignoreIfLocked) {
                        status = Status.UNLOCKING;

                        if (DEBUG) {
                            log.debug("Became {} [key={}]", status, key);
                        }

                        remoteUnlock();
                    }

                    break;
                }
                case UNLOCKING:
                case UNLOCKED:
                case TERMINATED: {
                    // No-op.
                    break;
                }
                default: {
                    throw new IllegalArgumentException("Unexpected lock status: " + status);
                }
            }
        }
    }

    private boolean becomeLocked(ClusterHash requestTopology) {
        synchronized (mux) {
            if (topology == null || !requestTopology.equals(topology.hash())) {
                if (TRACE) {
                    log.trace("Rejected to become {} [key={}, topology={}]", Status.LOCKED, key, topology);
                }

                return false;
            }

            switch (status) {
                case LOCKING: {
                    status = Status.LOCKED;

                    lockOwner = new DefaultLockOwnerInfo(threadId, topology.localNode());

                    if (DEBUG) {
                        log.debug("Became {} [key={}]", status, key);
                    }

                    metrics.onLock();

                    lockFuture.complete(true);

                    if (asyncCallback != null) {
                        asyncCallback.onLockAcquire(this);
                    }

                    break;
                }
                case UNLOCKED: {
                    remoteUnlock();

                    break;
                }
                case LOCKED:
                case UNLOCKING:
                case TERMINATED: {
                    // No-op.
                    break;
                }
                default: {
                    throw new IllegalArgumentException("Unexpected lock status: " + status);
                }
            }
        }

        return true;
    }

    private void becomeUnlocked() {
        doBecomeUnlocked();
    }

    private boolean tryBecomeUnlocked(ClusterHash requestTopology) {
        synchronized (mux) {
            if (topology == null || (requestTopology != null && !requestTopology.equals(topology.hash()))) {
                if (TRACE) {
                    log.trace("Rejected to become {} [key={}, topology={}]", Status.UNLOCKED, key, topology);
                }

                return false;
            } else {
                doBecomeUnlocked();

                return true;
            }
        }
    }

    private void doBecomeUnlocked() {
        synchronized (mux) {
            try {
                switch (status) {
                    case LOCKING: {
                        status = Status.UNLOCKED;

                        if (DEBUG) {
                            log.debug("Became {} [key={}]", status, key);
                        }

                        lockFuture.complete(false);

                        break;
                    }
                    case LOCKED: {
                        illegalStateTransition(Status.UNLOCKED);

                        break;
                    }
                    case UNLOCKING: {
                        status = Status.UNLOCKED;

                        if (DEBUG) {
                            log.debug("Became {} [key={}]", status, key);
                        }

                        metrics.onUnlock();

                        unlockFuture.complete(true);

                        if (asyncCallback != null) {
                            asyncCallback.onLockRelease();
                        }

                        break;
                    }
                    case UNLOCKED:
                    case TERMINATED: {
                        // No-op.
                        break;
                    }
                    default: {
                        throw new IllegalArgumentException("Unexpected lock status: " + status);
                    }
                }
            } finally {
                lockOwner = null;
            }
        }
    }

    private void remoteLock() {
        LockRequest lockReq = new LockRequest(lockId, key.region(), key.name(), localNode, lockTimeout, threadId);

        // Retry policy.
        RequestRetryConfigurer retryPolicy = retry -> retry
            .unlimitedAttempts()
            .alwaysReRoute()
            .whileTrue(() -> is(Status.LOCKING))
            .whileResponse(rsp -> {
                LockResponse lockRsp = rsp.payload(LockResponse.class);

                switch (lockRsp.status()) {
                    case OK: {
                        ClusterHash topology = rsp.topology().hash();

                        return !becomeLocked(topology);
                    }
                    case RETRY: {
                        return true;
                    }
                    case LOCK_TIMEOUT:
                    case LOCK_BUSY: {
                        becomeUnlocked();

                        return false;
                    }
                    case LOCK_OWNER_CHANGE: {
                        throw new IllegalArgumentException("Got an unexpected lock owner update message: " + rsp);
                    }
                    default: {
                        throw new IllegalArgumentException("Unexpected status: " + lockRsp.status());
                    }
                }
            });

        if (asyncCallback == null) {
            if (DEBUG) {
                log.debug("Submitting lock request [request={}]", lockReq);
            }

            // Send single request if we don't need to subscribe for updates.
            channel.newRequest(lockReq)
                .withAffinity(key)
                .withRetry(retryPolicy)
                .submit((err, rsp) -> {
                    if (err != null && is(Status.LOCKING)) {
                        log.error("Failed to submit lock request [request={}]", lockReq, err);
                    }
                });
        } else {
            if (DEBUG) {
                log.debug("Submitting lock subscription [request={}]", lockReq);
            }

            // Send subscription request if we need to receive lock owner updates.
            channel.newSubscribe(lockReq)
                .withAffinity(key)
                .withRetry(retryPolicy)
                .submit((err, rsp) -> {
                    if (err == null) {
                        LockResponse lockRsp = rsp.payload(LockResponse.class);

                        if (lockRsp.status() == LockResponse.Status.LOCK_OWNER_CHANGE) {
                            processLockOwnerChange(lockRsp, rsp);
                        }
                    } else if (is(Status.LOCKING)) {
                        log.error("Failed to submit lock request [request={}]", lockReq, err);
                    }
                });
        }
    }

    private void remoteUnlock() {
        UnlockRequest unlockReq = new UnlockRequest(lockId, key.region(), key.name(), localNode);

        if (DEBUG) {
            log.debug("Submitting unlock request [request={}]", unlockReq);
        }

        channel.newRequest(unlockReq)
            .withAffinity(key)
            .withRetry(retry -> retry
                .unlimitedAttempts()
                .alwaysReRoute()
                .whileTrue(() -> is(Status.UNLOCKING))
                .whileResponse(rsp -> {
                    UnlockResponse unlockRsp = rsp.payload(UnlockResponse.class);

                    return unlockRsp.status() != UnlockResponse.Status.OK
                        || !tryBecomeUnlocked(rsp.topology().hash());
                })
            )
            .submit((err, rsp) -> {
                if (err != null && is(Status.UNLOCKING)) {
                    log.error("Failed to submit unlock request [request={}]", unlockReq, err);
                }
            });
    }

    private void processLockOwnerChange(LockResponse lockRsp, Response msg) {
        boolean notified = tryNotifyOnLockOwnerChange(lockRsp.owner(), lockRsp.ownerThreadId(), msg.topology().hash());

        if (!notified) {
            if (DEBUG) {
                log.debug("Sending explicit lock owner query [to={}, key={}]", msg.from(), key);
            }

            LockOwnerRequest req = new LockOwnerRequest(key.region(), key.name());

            channel.newRequest(req)
                .withAffinity(key)
                .withRetry(retry -> retry
                    .unlimitedAttempts()
                    .alwaysReRoute()
                    .whileTrue(() -> is(Status.LOCKING))
                    .whileResponse(rsp -> {
                        LockOwnerResponse ownerRsp = rsp.payload(LockOwnerResponse.class);

                        // Retry if update got rejected.
                        return ownerRsp.status() != LockOwnerResponse.Status.OK
                            || !tryNotifyOnLockOwnerChange(ownerRsp.owner(), ownerRsp.threadId(), rsp.topology().hash());
                    })
                )
                .submit((err, rsp) -> {
                    if (err != null && is(Status.LOCKING)) {
                        log.error("Failed to submit explicit lock owner query [request={}]", req, err);
                    }
                });
        }
    }

    private boolean tryNotifyOnLockOwnerChange(ClusterNodeId ownerId, long ownerThreadId, ClusterHash requestTopology) {
        synchronized (mux) {
            if (status != Status.LOCKING) {
                if (TRACE) {
                    log.trace("Ignored lock owner change because status is not {} [key={}, status={}]", Status.LOCKING, key, status);
                }

                // Not locking anymore (should not retry).
                return true;
            } else if (topology == null || !requestTopology.equals(topology.hash())) {
                if (TRACE) {
                    log.trace("Ignored lock owner change because of topology mismatch [key={}, topology={}]", key, topology);
                }

                // Should retry.
                return false;
            } else {
                ClusterNode ownerNode = topology.get(ownerId);

                LockOwnerInfo newOwner = new DefaultLockOwnerInfo(ownerThreadId, ownerNode);

                if (lockOwner == null) {
                    if (DEBUG) {
                        log.debug("Set initial lock owner [key={}, owner={}]", key, newOwner);
                    }

                    lockOwner = newOwner;

                    asyncCallback.onLockBusy(newOwner);
                } else if (!lockOwner.equals(newOwner)) {
                    if (DEBUG) {
                        log.debug("Updated lock owner [key={}, owner={}]", key, newOwner);
                    }

                    lockOwner = newOwner;

                    asyncCallback.onLockOwnerChange(newOwner);
                }

                // Successfully updated (should not retry).
                return true;
            }
        }
    }

    private boolean is(Status status) {
        synchronized (mux) {
            return this.status == status;
        }
    }

    private void illegalStateTransition(Status newStatus) {
        throw new IllegalStateException("Illegal lock state transition from " + status + " to " + newStatus);
    }

    @Override
    public String toString() {
        return ToString.format(this);
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy