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

io.github.bucket4j.distributed.proxy.optimization.manual.ManuallySyncingCommandExecutor Maven / Gradle / Ivy

The newest version!
/*-
 * ========================LICENSE_START=================================
 * Bucket4j
 * %%
 * Copyright (C) 2015 - 2020 Vladimir Bukhtoyarov
 * %%
 * 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.
 * =========================LICENSE_END==================================
 */
package io.github.bucket4j.distributed.proxy.optimization.manual;

import io.github.bucket4j.TimeMeter;
import io.github.bucket4j.distributed.proxy.AsyncCommandExecutor;
import io.github.bucket4j.distributed.proxy.CommandExecutor;
import io.github.bucket4j.distributed.proxy.optimization.OptimizationListener;
import io.github.bucket4j.distributed.remote.*;
import io.github.bucket4j.distributed.remote.commands.ConsumeIgnoringRateLimitsCommand;
import io.github.bucket4j.distributed.remote.commands.CreateSnapshotCommand;
import io.github.bucket4j.distributed.remote.commands.MultiCommand;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.locks.ReentrantLock;

class ManuallySyncingCommandExecutor implements CommandExecutor, AsyncCommandExecutor {

    private static final int ORIGINAL_COMMAND_INDEX = 1;
    private static final int GET_SNAPSHOT_COMMAND_INDEX = 2;

    private final CommandExecutor originalExecutor;
    private final AsyncCommandExecutor originalAsyncExecutor;
    private final OptimizationListener listener;
    private final TimeMeter timeMeter;

    private RemoteBucketState state;

    private long lastSyncTimeNanos;
    private long postponedToConsumeTokens;

    private final ReentrantLock localStateMutationLock = new ReentrantLock();
    private final ReentrantLock remoteExecutionLock = new ReentrantLock();
    private CompletableFuture inProgressSynchronizationFuture = CompletableFuture.completedFuture(null);

    ManuallySyncingCommandExecutor(CommandExecutor originalExecutor, OptimizationListener listener, TimeMeter timeMeter) {
        this.originalExecutor = originalExecutor;
        this.originalAsyncExecutor = null;
        this.listener = listener;
        this.timeMeter = timeMeter;
    }

    ManuallySyncingCommandExecutor(AsyncCommandExecutor originalAsyncExecutor, OptimizationListener listener, TimeMeter timeMeter) {
        this.originalExecutor = null;
        this.originalAsyncExecutor = originalAsyncExecutor;
        this.listener = listener;
        this.timeMeter = timeMeter;
    }

    @Override
    public  CommandResult execute(RemoteCommand command) {
        MultiCommand remoteCommand;
        localStateMutationLock.lock();
        try {
            CommandResult localResult = tryConsumeLocally(command);
            if (localResult != null) {
                // remote call is not needed
                listener.incrementSkipCount(1);
                return localResult;
            } else {
                remoteCommand = prepareRemoteCommand(command);
            }
        } finally {
            localStateMutationLock.unlock();
        }

        remoteExecutionLock.lock();
        try {
            CommandResult remoteResult = originalExecutor.execute(remoteCommand);
            rememberRemoteCommandResult(remoteResult);
            return remoteResult.isError() ?
                (CommandResult) remoteResult :
                (CommandResult) remoteResult.getData().getResults().get(ORIGINAL_COMMAND_INDEX);
        } finally {
            remoteExecutionLock.unlock();
        }
    }

    @Override
    public  CompletableFuture> executeAsync(RemoteCommand command) {
        MultiCommand remoteCommand;
        localStateMutationLock.lock();
        try {
            CommandResult result = tryConsumeLocally(command);
            if (result != null) {
                // remote call is not needed
                listener.incrementSkipCount(1);
                return CompletableFuture.completedFuture(result);
            } else {
                remoteCommand = prepareRemoteCommand(command);
            }
        } finally {
            localStateMutationLock.unlock();
        }

        remoteExecutionLock.lock();
        CompletableFuture> resultFuture;
        try {
            resultFuture = inProgressSynchronizationFuture.thenCompose(f -> originalAsyncExecutor.executeAsync(remoteCommand));
            inProgressSynchronizationFuture = resultFuture.exceptionally((Throwable ex) -> null);
        } finally {
            remoteExecutionLock.unlock();
        }

        return resultFuture.thenApply((CommandResult remoteResult) -> {
            rememberRemoteCommandResult(remoteResult);
            return remoteResult.isError() ?
                (CommandResult) remoteResult :
                (CommandResult) remoteResult.getData().getResults().get(ORIGINAL_COMMAND_INDEX);
        });
    }

    private  CommandResult tryConsumeLocally(RemoteCommand command) {
        long currentTimeNanos = timeMeter.currentTimeNanos();
        if (isNeedToExecuteRemoteImmediately(command, currentTimeNanos)) {
            return null;
        }

        // execute local command
        MutableBucketEntry entry = new MutableBucketEntry(state.copy());
        CommandResult result = command.execute(entry, currentTimeNanos);
        if (result.isConfigurationNeedToBeReplaced()) {
            return null;
        }
        long locallyConsumedTokens = command.getConsumedTokens(result.getData());
        if (locallyConsumedTokens == Long.MAX_VALUE) {
            return null;
        }

        if (!isLocalExecutionResultSatisfiesThreshold(locallyConsumedTokens)) {
            return null;
        }

        postponedToConsumeTokens += locallyConsumedTokens;
        if (entry.isStateModified()) {
            state = entry.get();
        }

        return result;
    }

    private boolean isLocalExecutionResultSatisfiesThreshold(long locallyConsumedTokens) {
        // check math overflow
        return locallyConsumedTokens != Long.MAX_VALUE && postponedToConsumeTokens + locallyConsumedTokens >= 0;
    }

    private  boolean isNeedToExecuteRemoteImmediately(RemoteCommand command, long currentTimeNanos) {
        if (state == null) {
            // was never synchronized before
            return true;
        }

        if (command.isImmediateSyncRequired(postponedToConsumeTokens, currentTimeNanos - lastSyncTimeNanos)) {
            // need to execute immediately because of special command
            return true;
        }

        long commandTokens = command.estimateTokensToConsume();
        if (commandTokens == Long.MAX_VALUE || commandTokens + postponedToConsumeTokens < 0) {
            // math overflow
            return true;
        }

        return false;
    }

    private  MultiCommand prepareRemoteCommand(RemoteCommand command) {
        List> commands = new ArrayList<>(3);
        commands.add(new ConsumeIgnoringRateLimitsCommand(this.postponedToConsumeTokens));
        this.postponedToConsumeTokens = 0;
        commands.add(command);
        commands.add(new CreateSnapshotCommand());
        return new MultiCommand(commands);
    }

    private void rememberRemoteCommandResult(CommandResult remoteResult) {
        localStateMutationLock.lock();
        try {
            lastSyncTimeNanos = timeMeter.currentTimeNanos();
            CommandResult snapshotResult = remoteResult.isError() ? remoteResult : remoteResult.getData().getResults().get(GET_SNAPSHOT_COMMAND_INDEX);
            if (snapshotResult.isError()) {
                state = null;
                return;
            }
            this.state = (RemoteBucketState) snapshotResult.getData();

            // decrease available tokens by amount that consumed while remote request was in progress
            if (postponedToConsumeTokens > 0) {
                this.state.consume(postponedToConsumeTokens);
            }
        } finally {
            localStateMutationLock.unlock();
        }
    }


}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy