1 /*
2 * Logback: the reliable, generic, fast and flexible logging framework.
3 * Copyright (C) 1999-2025, QOS.ch. All rights reserved.
4 *
5 * This program and the accompanying materials are dual-licensed under
6 * either the terms of the Eclipse Public License v1.0 as published by
7 * the Eclipse Foundation
8 *
9 * or (per the licensee's choosing)
10 *
11 * under the terms of the GNU Lesser General Public License version 2.1
12 * as published by the Free Software Foundation.
13 */
14
15 package ch.qos.logback.core.util;
16
17 import java.util.concurrent.atomic.AtomicLong;
18
19 /**
20 * A simple time-based guard that limits the number of allowed operations within a sliding time window.
21 * This class is useful for rate limiting or preventing excessive actions over time periods.
22 * It supports time injection for testing purposes.
23 *
24 * @author Ceki Gülcü
25 * @since 1.5.22
26 */
27 public class SimpleTimeBasedGuard {
28
29 private final long windowDurationMs;
30 private final int maxAllows;
31
32 /**
33 * Default window duration in milliseconds: 30 minutes.
34 */
35 public static final long DEFAULT_WINDOW_MS = 30*60_000L; // 30 minutes
36
37 /**
38 * Default maximum number of allows per window: 2.
39 */
40 public static final int DEFAULT_MAX_ALLOWS = 2;
41
42 // Injectable time
43 private final AtomicLong artificialTime = new AtomicLong(-1L);
44
45 // Current window state
46 private volatile long windowStartMs = 0;
47 private volatile int allowsUsed = 0;
48
49 /**
50 * Creates a guard with custom limits.
51 *
52 * @param windowDurationMs how many millis per window (e.g. 30_000 for 30 minutes)
53 * @param maxAllows how many allows per window (e.g. 2)
54 */
55 public SimpleTimeBasedGuard(long windowDurationMs, int maxAllows) {
56 if (windowDurationMs <= 0) throw new IllegalArgumentException("windowDurationMs must be > 0");
57 if (maxAllows < 1) throw new IllegalArgumentException("maxAllows must be >= 1");
58
59 this.windowDurationMs = windowDurationMs;
60 this.maxAllows = maxAllows;
61 }
62
63 /**
64 * Convenience: uses defaults — 2 allows every 30 minutes
65 */
66 public SimpleTimeBasedGuard() {
67 this(DEFAULT_WINDOW_MS, DEFAULT_MAX_ALLOWS);
68 }
69
70 /**
71 * Checks if an operation is allowed based on the current time window.
72 * If allowed, increments the usage count for the current window.
73 * If the window has expired, resets the window and allows the operation.
74 *
75 * @return true if the operation is allowed, false otherwise
76 */
77 public synchronized boolean allow() {
78 long now = currentTimeMillis();
79
80 // First call ever
81 if (windowStartMs == 0) {
82 windowStartMs = now;
83 allowsUsed = 1;
84 return true;
85 }
86
87 // Still in current window?
88 if (now < windowStartMs + windowDurationMs) {
89 if (allowsUsed < maxAllows) {
90 allowsUsed++;
91 return true;
92 }
93 return false;
94 }
95
96 // New window → reset
97 windowStartMs = now;
98 allowsUsed = 1;
99 return true;
100 }
101
102 // --- Time injection for testing ---
103
104 /**
105 * Sets the artificial current time for testing purposes.
106 * When set, {@link #currentTimeMillis()} will return this value instead of {@link System#currentTimeMillis()}.
107 *
108 * @param timestamp the artificial timestamp in milliseconds
109 */
110 public void setCurrentTimeMillis(long timestamp) {
111 this.artificialTime.set(timestamp);
112 }
113
114 /**
115 * Clears the artificial time, reverting to using {@link System#currentTimeMillis()}.
116 */
117 public void clearCurrentTime() {
118 this.artificialTime.set(-1L);
119 }
120
121 private long currentTimeMillis() {
122 long t = artificialTime.get();
123 return t >= 0 ? t : System.currentTimeMillis();
124 }
125
126 void incCurrentTimeMillis(long increment) {
127 artificialTime.getAndAdd(increment);
128 }
129
130 // --- Helpful getters ---
131
132 /**
133 * Returns the number of allows used in the current window.
134 *
135 * @return the number of allows used
136 */
137 public int getAllowsUsed() {
138 return allowsUsed;
139 }
140
141 /**
142 * Returns the number of allows remaining in the current window.
143 *
144 * @return the number of allows remaining
145 */
146 public int getAllowsRemaining() {
147 return Math.max(0, maxAllows - allowsUsed);
148 }
149
150 /**
151 * Returns the window duration in milliseconds.
152 *
153 * @return the window duration in milliseconds
154 */
155 public long getWindowDuration() {
156 return windowDurationMs;
157 }
158
159 /**
160 * Returns the maximum number of allows per window.
161 *
162 * @return the maximum number of allows
163 */
164 public int getMaxAllows() {
165 return maxAllows;
166 }
167
168 /**
169 * Returns the number of milliseconds until the next window starts.
170 * If no window has started yet, returns the full window duration.
171 *
172 * @return milliseconds until next window
173 */
174 public long getMillisUntilNextWindow() {
175 if (windowStartMs == 0) return windowDurationMs;
176 long nextWindowStart = windowStartMs + windowDurationMs;
177 long now = currentTimeMillis();
178 return Math.max(0, nextWindowStart - now);
179 }
180 }