io.github.bucket4j.redis.lettuce.cas.LettuceBasedProxyManager Maven / Gradle / Ivy
/*-
* ========================LICENSE_START=================================
* Bucket4j
* %%
* Copyright (C) 2015 - 2022 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.redis.lettuce.cas;
import io.github.bucket4j.TimeMeter;
import io.github.bucket4j.distributed.ExpirationAfterWriteStrategy;
import io.github.bucket4j.distributed.proxy.ClientSideConfig;
import io.github.bucket4j.distributed.proxy.generic.compare_and_swap.AbstractCompareAndSwapBasedProxyManager;
import io.github.bucket4j.distributed.proxy.generic.compare_and_swap.AsyncCompareAndSwapOperation;
import io.github.bucket4j.distributed.proxy.generic.compare_and_swap.CompareAndSwapOperation;
import io.github.bucket4j.distributed.remote.RemoteBucketState;
import io.github.bucket4j.redis.AbstractRedisProxyManagerBuilder;
import io.lettuce.core.RedisClient;
import io.lettuce.core.RedisException;
import io.lettuce.core.RedisFuture;
import io.lettuce.core.ScriptOutputType;
import io.lettuce.core.api.StatefulRedisConnection;
import io.lettuce.core.api.async.RedisAsyncCommands;
import io.lettuce.core.codec.ByteArrayCodec;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
public class LettuceBasedProxyManager extends AbstractCompareAndSwapBasedProxyManager {
private final RedisAsyncCommands commands;
private final ExpirationAfterWriteStrategy expirationStrategy;
public static LettuceBasedProxyManagerBuilder builderFor(RedisAsyncCommands redisAsyncCommands) {
return new LettuceBasedProxyManagerBuilder(redisAsyncCommands);
}
public static LettuceBasedProxyManagerBuilder builderFor(StatefulRedisConnection statefulRedisConnection) {
return new LettuceBasedProxyManagerBuilder(statefulRedisConnection.async());
}
public static LettuceBasedProxyManagerBuilder builderFor(RedisClient redisClient) {
return builderFor(redisClient.connect(ByteArrayCodec.INSTANCE));
}
public static class LettuceBasedProxyManagerBuilder extends AbstractRedisProxyManagerBuilder {
private final RedisAsyncCommands commands;
private LettuceBasedProxyManagerBuilder(RedisAsyncCommands commands) {
this.commands = Objects.requireNonNull(commands);
}
public LettuceBasedProxyManager build() {
return new LettuceBasedProxyManager(this);
}
}
private LettuceBasedProxyManager(LettuceBasedProxyManagerBuilder builder) {
super(builder.getClientSideConfig());
this.expirationStrategy = builder.getNotNullExpirationStrategy();
this.commands = builder.commands;
}
@Override
protected CompareAndSwapOperation beginCompareAndSwapOperation(byte[] key) {
byte[][] keys = {key};
return new CompareAndSwapOperation() {
@Override
public Optional getStateData() {
RedisFuture stateFuture = commands.get(key);
return Optional.ofNullable(getFutureValue(stateFuture));
}
@Override
public boolean compareAndSwap(byte[] originalData, byte[] newData, RemoteBucketState newState) {
return getFutureValue(compareAndSwapFuture(key, keys, originalData, newData, newState));
}
};
}
@Override
protected AsyncCompareAndSwapOperation beginAsyncCompareAndSwapOperation(byte[] key) {
byte[][] keys = {key};
return new AsyncCompareAndSwapOperation() {
@Override
public CompletableFuture> getStateData() {
RedisFuture stateFuture = commands.get(key);
return convertToCompletableFuture(stateFuture)
.thenApply((byte[] resultBytes) -> Optional.ofNullable(resultBytes));
}
@Override
public CompletableFuture compareAndSwap(byte[] originalData, byte[] newData, RemoteBucketState newState) {
return convertToCompletableFuture(compareAndSwapFuture(key, keys, originalData, newData, newState));
}
};
}
@Override
public void removeProxy(byte[] key) {
RedisFuture> future = commands.del(key);
getFutureValue(future);
}
@Override
protected CompletableFuture removeAsync(byte[] key) {
RedisFuture> future = commands.del(key);
return convertToCompletableFuture(future).thenApply(bytes -> null);
}
@Override
public boolean isAsyncModeSupported() {
return true;
}
private RedisFuture compareAndSwapFuture(byte[] key, byte[][] keys, byte[] originalData, byte[] newData, RemoteBucketState newState) {
long ttlMillis = calculateTtlMillis(newState);
if (ttlMillis > 0) {
if (originalData == null) {
// nulls are prohibited as values, so "replace" must not be used in such cases
String script = "return redis.call('set', KEYS[1], ARGV[1], 'nx', 'px', ARGV[2])";
byte[][] params = {newData, encodeLong(ttlMillis)};
return commands.eval(script, ScriptOutputType.BOOLEAN, keys, params);
} else {
String script =
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
"redis.call('psetex', KEYS[1], ARGV[3], ARGV[2]); " +
"return 1; " +
"else " +
"return 0; " +
"end";
byte[][] params = {originalData, newData, encodeLong(ttlMillis)};
return commands.eval(script, ScriptOutputType.BOOLEAN, keys, params);
}
} else {
if (originalData == null) {
// nulls are prohibited as values, so "replace" must not be used in such cases
String script = "return redis.call('set', KEYS[1], ARGV[1], 'nx')";
byte[][] params = {newData};
return commands.eval(script, ScriptOutputType.BOOLEAN, keys, params);
} else {
String script =
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
"redis.call('set', KEYS[1], ARGV[2]); " +
"return 1; " +
"else " +
"return 0; " +
"end";
byte[][] params = {originalData, newData};
return commands.eval(script, ScriptOutputType.BOOLEAN, keys, params);
}
}
}
private CompletableFuture convertToCompletableFuture(RedisFuture redissonFuture) {
CompletableFuture jdkFuture = new CompletableFuture<>();
redissonFuture.whenComplete((result, error) -> {
if (error != null) {
jdkFuture.completeExceptionally(error);
} else {
jdkFuture.complete(result);
}
});
return jdkFuture;
}
private V getFutureValue(RedisFuture value) {
try {
return value.get();
} catch (InterruptedException e) {
value.cancel(true);
Thread.currentThread().interrupt();
throw new RedisException(e);
} catch (ExecutionException e) {
throw e.getCause() instanceof RedisException ? (RedisException) e.getCause() :
new RedisException("Unexpected exception while processing command", e.getCause());
}
}
private byte[] encodeLong(Long value) {
return ("" + value).getBytes(StandardCharsets.UTF_8);
}
private long calculateTtlMillis(RemoteBucketState state) {
Optional clock = getClientSideConfig().getClientSideClock();
long currentTimeNanos = clock.isPresent() ? clock.get().currentTimeNanos() : System.currentTimeMillis() * 1_000_000;
return expirationStrategy.calculateTimeToLiveMillis(state, currentTimeNanos);
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy