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.

Revisions

  1. mhewedy revised this gist Jan 13, 2024. 1 changed file with 6 additions and 4 deletions.
    10 changes: 6 additions & 4 deletions Lock.java
    Original file line number Diff line number Diff line change
    @@ -105,9 +105,11 @@ private static void initRedisTemplate() {
    private static void writeKey(String key, Duration duration, String errorMessage) {
    log.trace("write key: {}", key);

    Boolean wasAbsent = redisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + key, "1", duration);

    if (Boolean.FALSE.equals(wasAbsent)) {
    var ok = redisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + key, "1", duration);
    if (ok == null) {
    throw new RuntimeException("Lock cannot used inside Redis pipeline/transaction");
    }
    if (!ok) {
    throw new BusinessException(errorMessage, "key", key);
    }
    }
    @@ -123,4 +125,4 @@ private static Supplier<Void> runnableToSupplier(Runnable action) {
    return null;
    };
    }
    }
    }
  2. mhewedy renamed this gist Jan 13, 2024. 1 changed file with 14 additions and 3 deletions.
    17 changes: 14 additions & 3 deletions Idempotent.java → Lock.java
    Original file line number Diff line number Diff line change
    @@ -6,8 +6,19 @@
    import java.time.Duration;
    import java.util.function.Supplier;

    /**
    * A simple implementation for Distributed Lock based on Redis.
    * <p>
    * The implementation relies on Redis SETNX (SET if Not eXists).
    * if one process acquires the lock, the other processes won't wait, and simply an exception will be thrown.
    * <p>
    * Usually, you would use the Database as a lock mechanism (using unique keys for example).
    * However, sometimes, you cannot use the database (for any reason), or you need a quick solution, hence you can use this class.
    *
    * @see <a href="https://redis.com/glossary/redis-lock/">Redis Lock</a>
    */
    @Slf4j
    public class Idempotent {
    public class Lock {

    private static final String KEY_PREFIX = "idempotent-";
    private static final Options DEFAULT_OPTIONS = new Options();
    @@ -47,7 +58,7 @@ public static <T> T run(String key, Options options, Supplier<T> action) {
    }

    /**
    * Use {@link Idempotent#options()} to create default {@link Options} object.
    * Use {@link Lock#options()} to create default {@link Options} object.
    */
    @NoArgsConstructor(access = AccessLevel.PRIVATE)
    public static class Options {
    @@ -112,4 +123,4 @@ private static Supplier<Void> runnableToSupplier(Runnable action) {
    return null;
    };
    }
    }
    }
  3. mhewedy revised this gist Jan 12, 2024. 1 changed file with 11 additions and 27 deletions.
    38 changes: 11 additions & 27 deletions Idempotent.java
    Original file line number Diff line number Diff line change
    @@ -34,16 +34,11 @@ public static <T> T run(String key, Supplier<T> 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);
    }

    writeKey(key, options.duration, options.errorKey);

    try {
    T result = action.get();
    if (!options.lockBefore) {
    writeKey(key, options.duration);
    }
    return result;
    return action.get();
    } finally {
    if (!options.keepLock) {
    removeKey(key);
    @@ -57,7 +52,6 @@ public static <T> T run(String key, Options options, Supplier<T> action) {
    @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";

    @@ -69,14 +63,6 @@ public Options duration(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.
    */
    @@ -105,16 +91,14 @@ private static void initRedisTemplate() {
    }
    }

    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) {
    private static void writeKey(String key, Duration duration, String errorMessage) {
    log.trace("write key: {}", key);
    redisTemplate.opsForValue().set(KEY_PREFIX + key, "1", duration);

    Boolean wasAbsent = redisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + key, "1", duration);

    if (Boolean.FALSE.equals(wasAbsent)) {
    throw new BusinessException(errorMessage, "key", key);
    }
    }

    private static void removeKey(String key) {
  4. mhewedy revised this gist Jan 11, 2024. 1 changed file with 89 additions and 18 deletions.
    107 changes: 89 additions & 18 deletions Idempotent.java
    Original file line number Diff line number Diff line change
    @@ -1,3 +1,5 @@
    import lombok.AccessLevel;
    import lombok.NoArgsConstructor;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.data.redis.core.RedisTemplate;

    @@ -8,53 +10,122 @@
    public class Idempotent {

    private static final String KEY_PREFIX = "idempotent-";
    private static final Duration LOCK_TIMEOUT = Duration.ofMinutes(5);
    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) {
    initRedisTemplate();
    checkKeyExists(key);
    writeKey(key);
    try {
    action.run();
    } finally {
    removeKey(key);
    }
    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);
    writeKey(key);
    checkKeyExists(key, options.errorKey);
    if (options.lockBefore) {
    writeKey(key, options.duration);
    }
    try {
    return action.get();
    T result = action.get();
    if (!options.lockBefore) {
    writeKey(key, options.duration);
    }
    return result;
    } finally {
    removeKey(key);
    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) {
    private static void checkKeyExists(String key, String errorMessage) {
    boolean exists = redisTemplate.opsForValue().get(KEY_PREFIX + key) != null;
    if (exists) {
    throw new RuntimeException("operation_in_progress, key: "+ key);
    throw new RuntimeException(errorMessage + ", key: " + key);
    }
    }

    private static void writeKey(String key) {
    private static void writeKey(String key, Duration duration) {
    log.trace("write key: {}", key);
    redisTemplate.opsForValue().set(KEY_PREFIX + key, "1", LOCK_TIMEOUT);
    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;
    };
    }
    }
  5. mhewedy created this gist Mar 11, 2023.
    60 changes: 60 additions & 0 deletions Idempotent.java
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,60 @@
    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 Duration LOCK_TIMEOUT = Duration.ofMinutes(5);

    private static RedisTemplate<String, Object> redisTemplate;

    public static void run(String key, Runnable action) {
    initRedisTemplate();
    checkKeyExists(key);
    writeKey(key);
    try {
    action.run();
    } finally {
    removeKey(key);
    }
    }

    public static <T> T run(String key, Supplier<T> action) {
    initRedisTemplate();
    checkKeyExists(key);
    writeKey(key);
    try {
    return action.get();
    } finally {
    removeKey(key);
    }
    }

    @SuppressWarnings({"unchecked"})
    private static void initRedisTemplate() {
    if (redisTemplate == null) {
    redisTemplate = AppContextUtil.getBean(RedisTemplate.class, String.class, Object.class);
    }
    }

    private static void checkKeyExists(String key) {
    boolean exists = redisTemplate.opsForValue().get(KEY_PREFIX + key) != null;
    if (exists) {
    throw new RuntimeException("operation_in_progress, key: "+ key);
    }
    }

    private static void writeKey(String key) {
    log.trace("write key: {}", key);
    redisTemplate.opsForValue().set(KEY_PREFIX + key, "1", LOCK_TIMEOUT);
    }

    private static void removeKey(String key) {
    log.trace("remove key: {}", key);
    redisTemplate.opsForValue().getAndDelete(KEY_PREFIX + key);
    }
    }