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 }