Skip to content

Instantly share code, notes, and snippets.

@mhewedy
Last active January 13, 2024 16:56
Show Gist options
  • Select an option

  • Save mhewedy/d09cef74a614613be9709e38a2b5c5ae to your computer and use it in GitHub Desktop.

Select an option

Save mhewedy/d09cef74a614613be9709e38a2b5c5ae to your computer and use it in GitHub Desktop.
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import java.time.Duration;
import java.util.function.Supplier;
@Slf4j
public class Idempotent {
private static final String KEY_PREFIX = "idempotent-";
private static final Options DEFAULT_OPTIONS = new Options();
private static RedisTemplate<String, Object> redisTemplate;
/**
* Run with default options
*/
public static void run(String key, Runnable action) {
run(key, DEFAULT_OPTIONS, action);
}
public static void run(String key, Options options, Runnable action) {
run(key, options, runnableToSupplier(action));
}
/**
* Run with default options
*/
public static <T> T run(String key, Supplier<T> action) {
return run(key, DEFAULT_OPTIONS, action);
}
public static <T> T run(String key, Options options, Supplier<T> action) {
initRedisTemplate();
checkKeyExists(key, options.errorKey);
if (options.lockBefore) {
writeKey(key, options.duration);
}
try {
T result = action.get();
if (!options.lockBefore) {
writeKey(key, options.duration);
}
return result;
} finally {
if (!options.keepLock) {
removeKey(key);
}
}
}
/**
* Use {@link Idempotent#options()} to create default {@link Options} object.
*/
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public static class Options {
private Duration duration = Duration.ofMinutes(5);
private boolean lockBefore = true;
private boolean keepLock = false;
private String errorKey = "operation_in_progress";
/**
* the maximum duration to lock, default is 5 minutes
*/
public Options duration(Duration d) {
this.duration = d;
return this;
}
/**
* whether to lock before the execution or not, default is true.
*/
public Options lockBefore(boolean b) {
this.lockBefore = b;
return this;
}
/**
* keep the lock for the whole duration regardless of whether the execution is done or not, default is false.
*/
public Options keepLock(boolean b) {
this.keepLock = b;
return this;
}
/**
* error key to thrown in case of operation is in progress, default is "operation_in_progress".
*/
public Options errorKey(String key) {
this.errorKey = key;
return this;
}
}
public static Options options() {
return new Options();
}
@SuppressWarnings({"unchecked"})
private static void initRedisTemplate() {
if (redisTemplate == null) {
redisTemplate = AppContextUtil.getBean(RedisTemplate.class, String.class, Object.class);
}
}
private static void checkKeyExists(String key, String errorMessage) {
boolean exists = redisTemplate.opsForValue().get(KEY_PREFIX + key) != null;
if (exists) {
throw new RuntimeException(errorMessage + ", key: " + key);
}
}
private static void writeKey(String key, Duration duration) {
log.trace("write key: {}", key);
redisTemplate.opsForValue().set(KEY_PREFIX + key, "1", duration);
}
private static void removeKey(String key) {
log.trace("remove key: {}", key);
redisTemplate.opsForValue().getAndDelete(KEY_PREFIX + key);
}
private static Supplier<Void> runnableToSupplier(Runnable action) {
return () -> {
action.run();
return null;
};
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment