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

Hyshmily / hotkey / 27925388821

22 Jun 2026 02:14AM UTC coverage: 92.403% (-1.2%) from 93.615%
27925388821

push

github

Hyshmily
fix:fix known bugs and promote performance

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

1143 of 1289 branches covered (88.67%)

Branch coverage included in aggregate %.

184 of 236 new or added lines in 19 files covered. (77.97%)

6 existing lines in 3 files now uncovered.

3321 of 3542 relevant lines covered (93.76%)

4.22 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.CacheKeysPolicy.invalidCacheKey;
19

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

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

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

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

71
  private final StringRedisTemplate redisTemplate;
72
  private final int defaultLockCount;
73
  private final int defaultInquiryCount;
74
  private final int defaultUnlockCount;
75

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

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

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

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

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

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

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

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

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

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

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

191
  @RequiredArgsConstructor
192
  @Slf4j
4✔
193
  public static class RedisLockHandle implements AutoReleaseLock {
194

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

201
    /**
202
     * Release the lock atomically.
203
     *
204
     * <p>Uses Lua {@code GET + DEL} to ensure only the lock owner can
205
     * delete the key.  Retries up to {@code unlockCount} times on
206
     * transient Redis failures.  Silently skips when the lock has
207
     * already expired (Redis TTL handles cleanup).
208
     */
209
    @Override
210
    public void close() {
211
      for (int i = 0; i < unlockCount; i++) {
8✔
212
        if (System.currentTimeMillis() < expireTimestamp) {
5✔
213
          if (i > 0) {
2✔
214
            try {
215
              Thread.sleep(1L);
2✔
216
            } catch (InterruptedException e) {
×
217
              Thread.currentThread().interrupt();
×
218
              return;
×
219
            }
1✔
220
          }
221
          try {
222
            Long result = redisTemplate.execute(UNLOCK_SCRIPT, List.of(lockKey), uuid);
16✔
223
            if (Objects.equals(result, 1L)) {
5✔
224
              return;
1✔
225
            }
226
          } catch (RedisSystemException e) {
1✔
227
            log.warn("Redis unlock attempt {}/{} failed for key {}", i + 1, unlockCount, lockKey, e);
27✔
228
          }
2✔
229
        } else {
230
          return;
1✔
231
        }
232
      }
233
      log.warn("Failed to release lock '{}' after {} attempts; lock may persist until TTL", lockKey, unlockCount);
8✔
234
    }
1✔
235
  }
236
}
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