• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Hyshmily / hotkey / 28637103998

03 Jul 2026 03:48AM UTC coverage: 90.399% (-0.2%) from 90.63%
28637103998

push

github

Hyshmily
fix: add packaging to gitignore, fix flaky jitter test, update docs references

Signed-off-by: Hyshmily <cxm8607@outlook.com>

1264 of 1461 branches covered (86.52%)

Branch coverage included in aggregate %.

3604 of 3924 relevant lines covered (91.85%)

4.19 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

88.89
/common/src/main/java/io/github/hyshmily/hotkey/sync/distributedlock/impl/RedisLockProvider.java
1
/*
2
 * Copyright 2026 Hyshmily. All Rights Reserved.
3
 *
4
 * Licensed under the Apache License, Version 2.0 (the "License");
5
 * you may not use this file except in compliance with the License.
6
 * You may obtain a copy of the License at
7
 *
8
 *     http://www.apache.org/licenses/LICENSE-2.0
9
 *
10
 * Unless required by applicable law or agreed to in writing, software
11
 * distributed under the License is distributed on an "AS IS" BASIS,
12
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
 * See the License for the specific language governing permissions and
14
 * limitations under the License.
15
 */
16
package io.github.hyshmily.hotkey.sync.distributedlock.impl;
17

18
import static io.github.hyshmily.hotkey.cache.cachesupport.CacheKeysPolicy.invalidCacheKey;
19

20
import io.github.hyshmily.hotkey.Internal;
21
import io.github.hyshmily.hotkey.constants.HotKeyConstants;
22
import io.github.hyshmily.hotkey.sync.distributedlock.AutoReleaseLock;
23
import io.github.hyshmily.hotkey.sync.distributedlock.LockProvider;
24
import java.time.Duration;
25
import java.util.List;
26
import java.util.Objects;
27
import java.util.concurrent.ThreadLocalRandom;
28
import java.util.concurrent.TimeUnit;
29
import lombok.RequiredArgsConstructor;
30
import lombok.extern.slf4j.Slf4j;
31
import org.springframework.data.redis.RedisSystemException;
32
import org.springframework.data.redis.core.StringRedisTemplate;
33
import org.springframework.data.redis.core.script.DefaultRedisScript;
34

35
/**
36
 * Redis-backed {@link LockProvider} using {@code SET NX PX} with UUID-based
37
 * safe release and configurable retry.
38
 *
39
 * <p>Validation and fallback are concentrated in the implementation:
40
 * <ul>
41
 *   <li>{@link #tryLock(String, long, TimeUnit)} — uses configured defaults</li>
42
 *   <li>{@link #tryLock(String, long, TimeUnit, int, int, int)} — validates
43
 *       each count; negative values fall back to defaults with a warning</li>
44
 * </ul>
45
 *
46
 * <p>Algorithm (JetCache-inspired):
47
 * <ol>
48
 *   <li>{@code SET key uuid NX PX ttl} — atomically acquire with expiry</li>
49
 *   <li>On transient failure ({@code null}) — {@code GET} to inquire;
50
 *       if the value is our UUID the lock is considered acquired
51
 *       (optimistic recovery)</li>
52
 *   <li>{@code close()} — Lua {@code GET + DEL} with UUID comparison
53
 *       for safe release, with retry on transient Redis errors</li>
54
 * </ol>
55
 */
56
@Slf4j
3✔
57
@RequiredArgsConstructor
58
@Internal
59
public class RedisLockProvider implements LockProvider {
60

61
  private static final DefaultRedisScript<Long> UNLOCK_SCRIPT = new DefaultRedisScript<>(
7✔
62
    "if redis.call('GET', KEYS[1]) == ARGV[1] then return redis.call('DEL', KEYS[1]) else return 0 end",
63
    Long.class
64
  );
65

66
  private static String lockId() {
67
    return (
1✔
68
      Long.toHexString(ThreadLocalRandom.current().nextLong()) +
3✔
69
      Long.toHexString(ThreadLocalRandom.current().nextLong())
4✔
70
    );
71
  }
72

73
  private final StringRedisTemplate redisTemplate;
74
  private final int defaultLockCount;
75
  private final int defaultInquiryCount;
76
  private final int defaultUnlockCount;
77

78
  @Override
79
  public AutoReleaseLock tryLock(String key, long expire, TimeUnit unit) {
80
    return acquireLock(key, expire, unit, defaultLockCount, defaultInquiryCount, defaultUnlockCount);
12✔
81
  }
82

83
  @Override
84
  public AutoReleaseLock tryLock(
85
    String key,
86
    long expire,
87
    TimeUnit unit,
88
    int lockCount,
89
    int inquiryCount,
90
    int unlockCount
91
  ) {
92
    int effectiveLock = lockCount >= 0 ? lockCount : defaultLockCount;
7✔
93
    int effectiveInquiry = inquiryCount >= 0 ? inquiryCount : defaultInquiryCount;
7✔
94
    int effectiveUnlock = unlockCount >= 0 ? unlockCount : defaultUnlockCount;
7✔
95

96
    if (lockCount < 0 || inquiryCount < 0 || unlockCount < 0) {
6!
97
      log.warn(
8✔
98
        "Invalid lock counts [{}, {}, {}], fallback to defaults [{}, {}, {}]",
99
        lockCount,
5✔
100
        inquiryCount,
5✔
101
        unlockCount,
5✔
102
        effectiveLock,
5✔
103
        effectiveInquiry,
5✔
104
        effectiveUnlock
2✔
105
      );
106
    }
107

108
    return acquireLock(key, expire, unit, effectiveLock, effectiveInquiry, effectiveUnlock);
9✔
109
  }
110

111
  /**
112
   * Core lock acquisition logic with exponential backoff between retries.
113
   *
114
   * <p>Algorithm:
115
   * <ol>
116
   *   <li>Atomically {@code SET key uuid NX PX ttl}</li>
117
   *   <li>On transient failure ({@code null}) — single {@code GET} to inquire;
118
   *       if the value is our UUID the lock is considered acquired
119
   *       (optimistic recovery)</li>
120
   *   <li>Between retries, sleep with exponential backoff
121
   *       ({@code min(10ms × 2^i, 100ms)})</li>
122
   * </ol>
123
   *
124
   * @param key           the lock key
125
   * @param expire        lock TTL duration
126
   * @param unit          time unit for {@code expire}
127
   * @param lockCount     SET NX retries
128
   * @param inquiryCount  GET inquiries after transient failure;
129
   *                      continues retrying while {@code value == null}
130
   * @param unlockCount   DEL retries on release
131
   * @return a handle if acquired, or {@code null} on failure
132
   */
133
  private AutoReleaseLock acquireLock(
134
    String key,
135
    long expire,
136
    TimeUnit unit,
137
    int lockCount,
138
    int inquiryCount,
139
    int unlockCount
140
  ) {
141
    if (invalidCacheKey(key)) {
3✔
142
      return null;
2✔
143
    }
144

145
    String lockKey = HotKeyConstants.REDIS_LOCK_KEY_PREFIX + key;
3✔
146
    String uuid = lockId();
2✔
147
    long expireMs = unit.toMillis(expire);
4✔
148
    long expireTimestamp = System.currentTimeMillis() + expireMs;
4✔
149

150
    for (int i = 0; i < lockCount; i++) {
7✔
151
      if (i > 0) {
2✔
152
        try {
153
          Thread.sleep(Math.min(10L * (1L << i), 100L));
8✔
154
        } catch (InterruptedException e) {
×
155
          Thread.currentThread().interrupt();
×
156
          return null;
×
157
        }
1✔
158
      }
159

160
      try {
161
        Boolean acquired = redisTemplate.opsForValue().setIfAbsent(lockKey, uuid, Duration.ofMillis(expireMs));
9✔
162

163
        if (Boolean.TRUE.equals(acquired)) {
4✔
164
          if (i > 0) {
2✔
165
            log.debug("Lock '{}' acquired after {} retries", key, i);
6✔
166
          }
167
          return new RedisLockHandle(redisTemplate, lockKey, uuid, expireTimestamp, unlockCount);
10✔
168
        }
169

170
        if (acquired == null) {
2✔
171
          for (int j = 0; j < inquiryCount; j++) {
7✔
172
            String value = redisTemplate.opsForValue().get(lockKey);
7✔
173

174
            if (uuid.equals(value)) {
4✔
175
              if (i > 0) {
2!
176
                log.debug("Lock '{}' acquired after {} retries (inquiry path)", key, i);
×
177
              }
178
              return new RedisLockHandle(redisTemplate, lockKey, uuid, expireTimestamp, unlockCount);
10✔
179
            }
180
            if (value != null) {
2✔
181
              break;
1✔
182
            }
183
          }
184
        }
185
      } catch (RedisSystemException e) {
×
186
        log.error("Redis system error occurred while acquiring lock '{}'", key, e);
×
187
      }
1✔
188
    }
189
    log.warn("Failed to acquire lock '{}' after {} attempts", key, lockCount);
6✔
190
    return null;
2✔
191
  }
192

193
  @RequiredArgsConstructor
194
  @Slf4j
4✔
195
  public static class RedisLockHandle implements AutoReleaseLock {
196

197
    private final StringRedisTemplate redisTemplate;
198
    private final String lockKey;
199
    private final String uuid;
200
    private final long expireTimestamp;
201
    private final int unlockCount;
202

203
    /**
204
     * Release the lock atomically.
205
     *
206
     * <p>Uses Lua {@code GET + DEL} to ensure only the lock owner can
207
     * delete the key.  Retries up to {@code unlockCount} times on
208
     * transient Redis failures.  Silently skips when the lock has
209
     * already expired (Redis TTL handles cleanup).
210
     */
211
    @Override
212
    public void close() {
213
      for (int i = 0; i < unlockCount; i++) {
8✔
214
        if (System.currentTimeMillis() < expireTimestamp) {
5✔
215
          if (i > 0) {
2✔
216
            try {
217
              Thread.sleep(1L);
2✔
218
            } catch (InterruptedException e) {
×
219
              Thread.currentThread().interrupt();
×
220
              return;
×
221
            }
1✔
222
          }
223
          try {
224
            Long result = redisTemplate.execute(UNLOCK_SCRIPT, List.of(lockKey), uuid);
16✔
225
            if (Objects.equals(result, 1L)) {
5✔
226
              return;
1✔
227
            }
228
          } catch (RedisSystemException e) {
1✔
229
            log.warn("Redis unlock attempt {}/{} failed for key {}", i + 1, unlockCount, lockKey, e);
27✔
230
          }
2✔
231
        } else {
232
          return;
1✔
233
        }
234
      }
235
      log.warn("Failed to release lock '{}' after {} attempts; lock may persist until TTL", lockKey, unlockCount);
8✔
236
    }
1✔
237
  }
238
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc