• 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

91.43
/common/src/main/java/io/github/hyshmily/hotkey/rule/Rule.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.rule;
17

18
import com.fasterxml.jackson.annotation.JsonIgnore;
19
import com.fasterxml.jackson.annotation.JsonProperty;
20
import java.util.UUID;
21
import java.util.regex.Matcher;
22
import java.util.regex.Pattern;
23
import lombok.Data;
24

25
/**
26
 * A cache access rule that defines how a key pattern should be handled
27
 * during cache operations.
28
 *
29
 * <p>Supports four pattern types: exact match, prefix match, wildcard glob
30
 * ({@code *}, {@code ?}), and regular expression. Rules are evaluated in
31
 * insertion order and the first matching rule determines the action taken.
32
 *
33
 * <p>Each rule carries a unique identifier and a creation timestamp for
34
 * tracking and cross-instance synchronization. Rules are serialized to
35
 * JSON for Redis persistence and AMQP broadcast.
36
 *
37
 * <p>Instances are mutable (for Jackson deserialization) but should be
38
 * treated as effectively immutable after construction and registration
39
 * with {@link RuleMatcher}.
40
 *
41
 * @see RuleMatcher
42
 * @see RuleAction
43
 * @see RuleType
44
 */
45
@Data
46
public class Rule {
47

48
  /**
49
   * Supported pattern types for matching cache keys against a rule.
50
   *
51
   * <p>The pattern type determines how the rule's {@link #pattern} string
52
   * is compared against candidate cache keys. Choosing the appropriate type
53
   * optimizes evaluation performance: EXACT and PREFIX are O(1) / O(n)
54
   * string operations, while WILDCARD and REGEX require regex compilation
55
   * and matching.
56
   */
57
  public enum RuleType {
3✔
58
    /** Exact string equality via {@link String#equals(Object)}. Fastest match type. */
59
    EXACT,
6✔
60
    /** Prefix (starts-with) match via {@link String#startsWith(String)}. */
61
    PREFIX,
6✔
62
    /**
63
     * Glob-style pattern matching.
64
     * Supports {@code *} (any sequence of characters) and {@code ?}
65
     * (any single character). The glob is converted to a regular expression
66
     * via {@link Rule#prepare()} before matching.
67
     */
68
    WILDCARD,
6✔
69
    /**
70
     * Full Java regular expression matching via {@link java.util.regex.Pattern}.
71
     * The pattern is compiled eagerly in the constructor and lazily on first
72
     * access via {@link Rule#prepare()}. Use the {@code regex:} prefix in
73
     * {@link RuleMatcher#of} to auto-detect this type.
74
     */
75
    REGEX,
6✔
76
  }
77

78
  /**
79
   * Action to take when a cache key matches a rule.
80
   *
81
   * <p>The action determines how the cache layer handles the requested
82
   * operation. Actions are evaluated in rule insertion order; the first
83
   * matching rule's action is applied.
84
   */
85
  public enum RuleAction {
3✔
86
    /**
87
     * Reject access immediately and throw
88
     * {@link io.github.hyshmily.hotkey.exception.HotKeyBlockedException}.
89
     * Used for blacklisting specific keys or patterns.
90
     */
91
    BLOCK,
6✔
92
    /**
93
     * Allow the cache access but <em>suppress</em> the app-to-Worker
94
     * hot-key report for this key. Useful for keys that should not
95
     * contribute to hot-key detection (e.g., health-check endpoints,
96
     * high-frequency operational keys).
97
     */
98
    ALLOW_NO_REPORT,
6✔
99
    /**
100
     * Allow the access with full processing: the key participates in
101
     * local hot-key detection and app-to-Worker reporting as normal.
102
     * This is the default action when no rule matches.
103
     */
104
    ALLOW,
6✔
105
  }
106

107
  /** Unique identifier for this rule. */
108
  private String id;
109
  /** Timestamp (epoch millis) when this rule was created. */
110
  private long createdAt;
111

112
  /** Pattern type used to match cache keys. Defaults to {@link RuleType#EXACT} for Jackson deserialization. */
113
  @JsonProperty(defaultValue = "EXACT")
6✔
114
  private RuleType type = RuleType.EXACT;
115

116
  /** Pattern string (exact value, prefix, glob, or regex). */
117
  private String pattern;
118
  /** Action to take when a key matches this rule. */
119
  private RuleAction action;
120
  /** Compiled regex for WILDCARD and REGEX types; lazily initialised. */
121
  @JsonIgnore
122
  private transient volatile Pattern compiledPattern;
123
  /** Reusable matcher per thread, avoiding allocation on every match call. */
124
  @JsonIgnore
10✔
125
  private final transient ThreadLocal<Matcher> matcherCache = new ThreadLocal<>();
126

127
  /**
128
   * No-arg constructor for frameworks (e.g. Jackson deserialisation).
129
   */
130
  public Rule() {}
3✔
131

132
  /**
133
   * Construct a rule with the given type, pattern, and action.
134
   *
135
   * <p>For {@link RuleType#REGEX} rules, the pattern is compiled eagerly
136
   * in the constructor so that invalid regex patterns are detected at
137
   * construction time rather than at match time. For other types,
138
   * compilation is deferred to {@link #prepare()}.
139
   *
140
   * <p>A unique identifier ({@link UUID}) and creation timestamp
141
   * ({@link System#currentTimeMillis()}) are automatically assigned.
142
   *
143
   * @param type    the pattern type — {@link RuleType#EXACT}, {@link RuleType#PREFIX},
144
   *                {@link RuleType#WILDCARD}, or {@link RuleType#REGEX}
145
   * @param pattern the pattern string to match against cache keys; for REGEX
146
   *                type, this must be a valid Java regular expression
147
   * @param action  the action to take when a cache key matches this rule
148
   * @throws java.util.regex.PatternSyntaxException if {@code type} is {@link RuleType#REGEX}
149
   *         and the given pattern is not a valid regular expression
150
   */
151
  public Rule(RuleType type, String pattern, RuleAction action) {
2✔
152
    this.id = UUID.randomUUID().toString();
4✔
153
    this.createdAt = System.currentTimeMillis();
3✔
154
    this.type = type;
3✔
155
    this.pattern = pattern;
3✔
156
    this.action = action;
3✔
157
    if (type == RuleType.REGEX) {
3✔
158
      this.compiledPattern = Pattern.compile(pattern);
4✔
159
    }
160
  }
1✔
161

162
  /**
163
   * Test whether the given cache key matches this rule's pattern.
164
   *
165
   * <p>Matching behavior depends on the rule type:
166
   * <ul>
167
   *   <li>{@code null} type — returns {@code false} (rule is effectively disabled); may
168
   *       occur when {@link RuleType} is absent during deserialization</li>
169
   *   <li>{@link RuleType#EXACT} — exact string equality via {@link String#equals}</li>
170
   *   <li>{@link RuleType#PREFIX} — prefix check via {@link String#startsWith}</li>
171
   *   <li>{@link RuleType#WILDCARD} — converts the glob to a regex (if not already
172
   *       compiled) and performs a full-region {@link java.util.regex.Matcher#matches}</li>
173
   *   <li>{@link RuleType#REGEX} — uses {@link java.util.regex.Matcher#find} for
174
   *       substring matching</li>
175
   * </ul>
176
   *
177
   * @param key the cache key to test against this rule; must not be {@code null}
178
   * @return {@code true} if the key matches this rule's pattern and the
179
   *         rule's action should be applied; {@code false} if the rule's type is
180
   *         {@code null} (untyped rule is treated as non-matching)
181
   * @throws NullPointerException if {@code key} is {@code null}
182
   * @throws java.util.regex.PatternSyntaxException if the underlying pattern
183
   *         (REGEX or WILDCARD) has not been {@link #prepare() prepared} yet
184
   *         and compilation fails
185
   */
186
  public boolean match(String key) {
187
    if (type == null) {
3!
UNCOV
188
      return false;
×
189
    }
190
    return switch (type) {
7✔
191
      case EXACT -> key.equals(pattern);
5✔
192
      case PREFIX -> key.startsWith(pattern);
5✔
193
      case WILDCARD -> {
194
        if (compiledPattern == null) {
3✔
195
          prepare();
2✔
196
        }
197
        Matcher m = matcherCache.get();
5✔
198
        if (m == null) {
2✔
199
          m = compiledPattern.matcher("");
5✔
200
          matcherCache.set(m);
4✔
201
        }
202
        yield m.reset(key).matches();
5✔
203
      }
204
      case REGEX -> {
205
        if (compiledPattern == null) {
3!
UNCOV
206
          prepare();
×
207
        }
208
        Matcher m = matcherCache.get();
5✔
209
        if (m == null) {
2✔
210
          m = compiledPattern.matcher("");
5✔
211
          matcherCache.set(m);
4✔
212
        }
213
        yield m.reset(key).find();
5✔
214
      }
215
    };
216
  }
217

218
  /**
219
   * Lazily compile the internal regex pattern if not already compiled.
220
   *
221
   * <p>For {@link RuleType#REGEX} rules, the pattern is used as-is.
222
   * For {@link RuleType#WILDCARD} rules, the glob metacharacters
223
   * ({@code *} → {@code .*}, {@code ?} → {@code .}) are converted to their
224
   * regex equivalents, and all other regex special characters
225
   * ({@code . + ^ $ [ ] \ ( ) { } |}) are escaped.
226
   *
227
   * <p>This method is safe for concurrent calls: {@link #compiledPattern}
228
   * is {@code volatile}, and  is
229
   * idempotent (compiling the same pattern twice produces identical,
230
   * interchangeable objects).
231
   *
232
   * <p>Has no effect on {@link RuleType#EXACT} or {@link RuleType#PREFIX}
233
   * rules, which do not use regex matching.
234
   *
235
   * @throws java.util.regex.PatternSyntaxException if the type is
236
   *         {@link RuleType#REGEX} or {@link RuleType#WILDCARD} and the
237
   *         pattern cannot be compiled into a valid regular expression
238
   */
239
  public void prepare() {
240
    if (type == null) {
3!
UNCOV
241
      return;
×
242
    }
243
    if (type == RuleType.REGEX) {
4✔
244
      compiledPattern = Pattern.compile(pattern);
6✔
245
    } else if (type == RuleType.WILDCARD) {
4✔
246
      String regex = pattern.replaceAll("([.+^$\\[\\]\\\\(){}|])", "\\\\$1").replace("*", ".*").replace("?", ".");
12✔
247
      compiledPattern = Pattern.compile(regex);
4✔
248
    }
249
  }
1✔
250
}
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