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

Hyshmily / hotkey / 28026418224

23 Jun 2026 12:32PM UTC coverage: 92.562% (-0.6%) from 93.188%
28026418224

push

github

Hyshmily
Release 1.1.51

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

1176 of 1323 branches covered (88.89%)

Branch coverage included in aggregate %.

61 of 76 new or added lines in 6 files covered. (80.26%)

3 existing lines in 1 file now uncovered.

3366 of 3584 relevant lines covered (93.92%)

4.24 hits per line

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

93.68
/common/src/main/java/io/github/hyshmily/hotkey/cache/fluentAPI/HotKeyReadQuery.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.cache.fluentAPI;
17

18
import io.github.hyshmily.hotkey.HotKey;
19
import io.github.hyshmily.hotkey.exception.HotKeyBlockedException;
20
import io.github.hyshmily.hotkey.rule.Rule;
21
import java.util.ArrayList;
22
import java.util.List;
23
import java.util.Optional;
24
import java.util.concurrent.atomic.AtomicBoolean;
25
import java.util.function.Supplier;
26
import org.springframework.cache.support.NullValue;
27

28
/**
29
 * Fluent read query for the HotKey cache.
30
 *
31
 * <p>Provides a builder-pattern API for reading from the L1 cache with an
32
 * optional primary data-source reader, a chain of fallback readers, and
33
 * fine-grained control over null caching, broadcast behaviour, and TTL
34
 * overrides.
35
 *
36
 * <h3>Usage</h3>
37
 * <pre>
38
 *   Optional&lt;User&gt; user = hotKey.read("user:42")
39
 *       .withPrimary(userRepository::findById)
40
 *       .thenExecute(backupRepository::findById)
41
 *       .withHardTtl(30_000)
42
 *       .withSoftTtl(10_000)
43
 *       .allowBroadcast()
44
 *       .execute();
45
 * </pre>
46
 *
47
 * <p>Created via {@link HotKey#read(String)}. Instances are single-use;
48
 * call {@link #execute()} exactly once.
49
 */
50
public class HotKeyReadQuery<T> {
51

52
  private final HotKey hotKey;
53
  private final String cacheKey;
54
  private Supplier<T> primaryReader;
55
  private CacheMode cacheMode = CacheMode.GET;
3✔
56
  private long hardTtlMs = 0;
3✔
57
  private long softTtlMs = 0;
3✔
58
  private boolean isAllowNullCaching = true;
3✔
59
  private boolean isAllowBroadcast = false;
3✔
60
  private final List<Supplier<T>> fallbacks = new ArrayList<>();
5✔
61
  private T defaultValThisTime = null;
3✔
62
  private boolean defaultSetThisTime = false;
3✔
63
  private final AtomicBoolean executed = new AtomicBoolean(false);
6✔
64

65
  /**
66
   * Creates a new read query bound to the given cache key.
67
   *
68
   * @param hotKey   the HotKey facade
69
   * @param cacheKey the cache key to read
70
   */
71
  public HotKeyReadQuery(HotKey hotKey, String cacheKey) {
2✔
72
    this.hotKey = hotKey;
3✔
73
    this.cacheKey = cacheKey;
3✔
74
  }
1✔
75

76
  /**
77
   * Enable cross-instance broadcast for fallback values.
78
   *
79
   * <p>When enabled, any value resolved from a fallback reader will be
80
   * broadcast to peer instances via AMQP.  When disabled (default), the
81
   * value is written only to the local L1 cache via {@link HotKey#putLocal}.
82
   *
83
   * @return this query instance
84
   */
85
  public HotKeyReadQuery<T> allowBroadcast() {
86
    this.isAllowBroadcast = true;
3✔
87
    return this;
2✔
88
  }
89

90
  /**
91
   * Disable cross-instance broadcast for fallback values (default).
92
   *
93
   * @return this query instance
94
   */
95
  public HotKeyReadQuery<T> notAllowBroadcast() {
96
    this.isAllowBroadcast = false;
3✔
97
    return this;
2✔
98
  }
99

100
  /**
101
   * Set the primary data-source reader with the default {@link CacheMode}.
102
   *
103
   * @param reader a supplier that loads the value from the data source
104
   * @return this query instance
105
   */
106
  public HotKeyReadQuery<T> withPrimary(Supplier<T> reader) {
107
    return withPrimary(reader, cacheMode);
6✔
108
  }
109

110
  /**
111
   * Set the primary data-source reader with an explicit {@link CacheMode}.
112
   *
113
   * @param reader a supplier that loads the value from the data source
114
   * @param mode   the cache access mode
115
   * @return this query instance
116
   */
117
  public HotKeyReadQuery<T> withPrimary(Supplier<T> reader, CacheMode mode) {
118
    this.primaryReader = reader;
3✔
119
    this.cacheMode = mode;
3✔
120
    return this;
2✔
121
  }
122

123
  /**
124
   * Override the hard TTL for this query.
125
   *
126
   * @param hardTtlMs hard TTL in milliseconds (0 = use configured default)
127
   * @return this query instance
128
   */
129
  public HotKeyReadQuery<T> withHardTtl(long hardTtlMs) {
130
    this.hardTtlMs = hardTtlMs;
3✔
131
    return this;
2✔
132
  }
133

134
  /**
135
   * Override the soft TTL for this query.
136
   *
137
   * @param softTtlMs soft TTL in milliseconds (0 = use configured default)
138
   * @return this query instance
139
   */
140
  public HotKeyReadQuery<T> withSoftTtl(long softTtlMs) {
141
    this.softTtlMs = softTtlMs;
3✔
142
    return this;
2✔
143
  }
144

145
  /**
146
   * Override both hard and soft TTL for this query.
147
   *
148
   * @param hardTtlMs hard TTL in milliseconds (0 = use configured default)
149
   * @param softTtlMs soft TTL in milliseconds (0 = use configured default)
150
   * @return this query instance
151
   */
152
  public HotKeyReadQuery<T> withTtl(long hardTtlMs, long softTtlMs) {
153
    this.hardTtlMs = hardTtlMs;
3✔
154
    this.softTtlMs = softTtlMs;
3✔
155
    return this;
2✔
156
  }
157

158
  /**
159
   * Disable null-value caching.
160
   *
161
   * <p>When the primary or fallback reader returns {@code null}, the value
162
   * will not be cached.
163
   *
164
   * @return this query instance
165
   */
166
  public HotKeyReadQuery<T> notAllowNull() {
167
    this.isAllowNullCaching = false;
3✔
168
    return this;
2✔
169
  }
170

171
  /**
172
   * Enable null-value caching (default).
173
   *
174
   * <p>When the primary or fallback reader returns {@code null}, a sentinel
175
   * value ({@link NullValue#INSTANCE}) is cached so that subsequent reads
176
   * for the same key return {@link Optional#empty()} without invoking the
177
   * reader again.
178
   *
179
   * @return this query instance
180
   */
181
  public HotKeyReadQuery<T> allowNull() {
182
    this.isAllowNullCaching = true;
3✔
183
    return this;
2✔
184
  }
185

186
  /**
187
   * Add a fallback reader to the chain.
188
   *
189
   * <p>Fallbacks are executed sequentially in registration order when the
190
   * primary reader does not return a value (either cache miss or reader
191
   * returns {@code null}).  Each non-null result is cached before being
192
   * returned.
193
   *
194
   * @param reader a supplier that loads the value from a fallback data source
195
   * @return this query instance
196
   */
197
  public HotKeyReadQuery<T> thenExecute(Supplier<T> reader) {
198
    fallbacks.add(reader);
5✔
199
    return this;
2✔
200
  }
201

202
  /**
203
   * Set a default value to return when all readers return {@code null}.
204
   *
205
   * <p>The default value is <b>not</b> cached.  It is returned only for
206
   * this single invocation of {@link #execute()}.  If a value is set via
207
   * both {@code orElseThisTime} and a fallback reader, the fallback value
208
   * takes precedence and is cached.
209
   *
210
   * @param defaultValue the value to return if all readers return {@code null}
211
   * @return this query instance
212
   */
213
  public HotKeyReadQuery<T> orElseThisTime(T defaultValue) {
214
    this.defaultValThisTime = defaultValue;
3✔
215
    this.defaultSetThisTime = true;
3✔
216
    return this;
2✔
217
  }
218

219
  /**
220
   * Execute the read query.
221
   *
222
   * <p>The execution order is:
223
   * <ol>
224
   *   <li>Check the blocklist ({@link Rule} evaluation) — throws
225
   *       {@link HotKeyBlockedException} if the key is blocked.</li>
226
   *   <li>Try the L1 cache via the configured {@link CacheMode}.  On cache
227
   *       miss, invoke the primary reader and cache the result.</li>
228
   *   <li>If no value is available, iterate fallback readers in registration
229
   *       order.  Each non-null result is cached (locally or with broadcast
230
   *       depending on {@code isAllowBroadcast}) and returned.</li>
231
   *   <li>If all readers return {@code null}, return the default value
232
   *       (if set) or {@link Optional#empty()}.</li>
233
   * </ol>
234
   *
235
   * @return an {@link Optional} containing the resolved value, or empty
236
   * @throws HotKeyBlockedException if the key matches a block rule
237
   */
238
  @SuppressWarnings("unchecked")
239
  public Optional<T> execute() {
240
    if (!executed.compareAndSet(false, true)) {
6!
NEW
241
      throw new IllegalStateException("HotKeyReadQuery can only be executed once");
×
242
    }
243

244
    if (hotKey.evaluateRule(cacheKey) == Rule.RuleAction.BLOCK) {
7✔
245
      throw new HotKeyBlockedException("Cache key is blocked by HotKey rules: ", cacheKey);
7✔
246
    }
247

248
    Supplier<Object> wrappedPrimary = () -> {
3✔
249
      T val = primaryReader.get();
4✔
250
      if (val == null) {
2!
251
        return isAllowNullCaching ? NullValue.INSTANCE : null;
×
252
      }
253
      return val;
2✔
254
    };
255

256
    Optional<Object> result = switch (cacheMode) {
6✔
257
      case GET -> hotKey.get(cacheKey, wrappedPrimary, hardTtlMs, softTtlMs);
11✔
258
      case GET_WITH_SOFT_EXPIRE -> hotKey.getWithSoftExpire(cacheKey, wrappedPrimary, hardTtlMs, softTtlMs);
11✔
259
    };
260

261
    if (result.isPresent()) {
3✔
262
      Object val = result.get();
3✔
263
      if (val == NullValue.INSTANCE) {
3✔
264
        return Optional.empty();
2✔
265
      }
266
      return Optional.of((T) val);
3✔
267
    }
268

269
    for (Supplier<T> fallback : fallbacks) {
11✔
270
      T val = fallback.get();
3✔
271

272
      if (val != null) {
2✔
273
        if (isAllowBroadcast) {
3✔
274
          hotKey.putThrough(cacheKey, val, () -> {}, hardTtlMs, softTtlMs);
12✔
275
        } else {
276
          hotKey.putLocal(cacheKey, val, hardTtlMs, softTtlMs);
10✔
277
        }
278
        return Optional.of(val);
3✔
279
      }
280

281
      if (isAllowNullCaching) {
3✔
282
        if (isAllowBroadcast) {
3✔
283
          hotKey.putThrough(cacheKey, NullValue.INSTANCE, () -> {}, hardTtlMs, softTtlMs);
12✔
284
        } else {
285
          hotKey.putLocal(cacheKey, NullValue.INSTANCE, hardTtlMs, softTtlMs);
10✔
286
        }
287
      }
288
    }
1✔
289

290
    if (defaultSetThisTime) {
3✔
291
      return Optional.ofNullable(defaultValThisTime);
4✔
292
    }
293
    return Optional.empty();
2✔
294
  }
295
}
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