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

io.github.bucket4j.distributed.proxy.optimization.predictive.PredictiveCommandExecutor 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.predictive;

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.*;
import io.github.bucket4j.distributed.remote.*;
import io.github.bucket4j.distributed.remote.commands.*;

import java.util.*;
import java.util.concurrent.CompletableFuture;

class PredictiveCommandExecutor 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 DelayParameters delayParameters;
    private final OptimizationListener listener;
    private final TimeMeter timeMeter;
    private final Sampling sampling;

    private RemoteBucketState state;
    private long postponedToConsumeTokens;
    private long speculativelyConsumedByPredictionTokens;

    PredictiveCommandExecutor(CommandExecutor originalExecutor, DelayParameters delayParameters,
                              PredictionParameters predictionParameters, OptimizationListener listener,
                              TimeMeter timeMeter) {
        this.originalExecutor = originalExecutor;
        this.originalAsyncExecutor = null;
        this.sampling = new Sampling(predictionParameters);
        this.delayParameters = delayParameters;
        this.listener = listener;
        this.timeMeter = timeMeter;
    }

    PredictiveCommandExecutor(AsyncCommandExecutor originalAsyncExecutor, DelayParameters delayParameters,
                              PredictionParameters predictionParameters, OptimizationListener listener,
                              TimeMeter timeMeter) {
        this.originalExecutor = null;
        this.originalAsyncExecutor = originalAsyncExecutor;
        this.sampling = new Sampling(predictionParameters);
        this.delayParameters = delayParameters;
        this.listener = listener;
        this.timeMeter = timeMeter;
    }

    @Override
    public  CommandResult execute(RemoteCommand command) {
        CommandResult result = tryConsumeLocally(command);
        if (result != null) {
            // remote call is not needed
            listener.incrementSkipCount(1);
            return result;
        }

        MultiCommand remoteCommand = prepareRemoteCommand(command);
        CommandResult remoteResult = originalExecutor.execute(remoteCommand);

        rememberRemoteCommandResult(remoteCommand, remoteResult);
        return remoteResult.isError() ?
            (CommandResult)remoteResult :
            (CommandResult) remoteResult.getData().getResults().get(ORIGINAL_COMMAND_INDEX);
    }

    @Override
    public  CompletableFuture> executeAsync(RemoteCommand command) {
        CommandResult result = tryConsumeLocally(command);
        if (result != null) {
            // remote call is not needed
            listener.incrementSkipCount(1);
            return CompletableFuture.completedFuture(result);
        }

        MultiCommand remoteCommand = prepareRemoteCommand(command);
        CompletableFuture> resultFuture = originalAsyncExecutor.executeAsync(remoteCommand);
        return resultFuture.thenApply((CommandResult remoteResult) -> {
            rememberRemoteCommandResult(remoteCommand, 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;
        }


        long predictedConsumptionSinceLastSync = sampling.predictedConsumptionByOthersSinceLastSync(currentTimeNanos);
        if (predictedConsumptionSinceLastSync == Long.MAX_VALUE) {
            return null;
        }

        long predictedConsumptionByOthersSinceLastLocalCall = predictedConsumptionSinceLastSync - speculativelyConsumedByPredictionTokens;
        if (predictedConsumptionByOthersSinceLastLocalCall < 0) {
            predictedConsumptionByOthersSinceLastLocalCall = 0;
        }

        // prepare local command
        ConsumeAsMuchAsPossibleCommand consumeByPredictionCommand = new ConsumeAsMuchAsPossibleCommand(predictedConsumptionByOthersSinceLastLocalCall);
        List> commands = Arrays.asList(consumeByPredictionCommand, command);
        MultiCommand multiCommand = new MultiCommand(commands);

        // execute local command
        MutableBucketEntry entry = new MutableBucketEntry(state.copy());
        MultiResult multiResult = multiCommand.execute(entry, currentTimeNanos).getData();
        if (multiResult.getResults().get(1).isConfigurationNeedToBeReplaced()) {
            return null;
        }

        if (multiCommand.getConsumedTokens(multiResult) == Long.MAX_VALUE) {
            return null;
        }

        long locallyConsumedTokens = command.getConsumedTokens((T) multiResult.getResults().get(ORIGINAL_COMMAND_INDEX).getData());
        if (!isLocalExecutionResultSatisfiesThreshold(locallyConsumedTokens)) {
            return null;
        }

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

        return (CommandResult) multiResult.getResults().get(ORIGINAL_COMMAND_INDEX);
    }

    private boolean isLocalExecutionResultSatisfiesThreshold(long locallyConsumedTokens) {
        if (locallyConsumedTokens == Long.MAX_VALUE || postponedToConsumeTokens + locallyConsumedTokens < 0) {
            // math overflow
            return false;
        }
        return postponedToConsumeTokens + locallyConsumedTokens <= delayParameters.maxUnsynchronizedTokens;
    }

    private  boolean isNeedToExecuteRemoteImmediately(RemoteCommand command, long currentTimeNanos) {
        if (sampling.isNeedToExecuteRemoteImmediately(currentTimeNanos)) {
            return true;
        }

        long lastSyncTimeNanos = sampling.getLastSyncTimeNanos();
        if (currentTimeNanos - lastSyncTimeNanos > delayParameters.maxUnsynchronizedTimeoutNanos) {
            // too long period passed since last sync
            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 commandTokens + postponedToConsumeTokens > delayParameters.maxUnsynchronizedTokens;
    }

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

    private void rememberRemoteCommandResult(MultiCommand multiCommand, CommandResult commandResult) {
        postponedToConsumeTokens = 0;
        speculativelyConsumedByPredictionTokens = 0;
        CommandResult snapshotResult = commandResult.isError() ? commandResult : commandResult.getData().getResults().get(GET_SNAPSHOT_COMMAND_INDEX);
        if (snapshotResult.isError()) {
            state = null;
            sampling.clear();
            return;
        }
        state = (RemoteBucketState) snapshotResult.getData();
        long now = timeMeter.currentTimeNanos();
        long consumedTokens = multiCommand.getConsumedTokens(commandResult.getData());
        sampling.rememberRemoteCommandResult(consumedTokens, state.getRemoteStat().getConsumedTokens(), now);
    }


}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy