001/*
002 * Logback: the reliable, generic, fast and flexible logging framework.
003 *  Copyright (C) 1999-2025, QOS.ch. All rights reserved.
004 *
005 * This program and the accompanying materials are dual-licensed under
006 * either the terms of the Eclipse Public License v1.0 as published by
007 * the Eclipse Foundation
008 *
009 *     or (per the licensee's choosing)
010 *
011 * under the terms of the GNU Lesser General Public License version 2.1
012 * as published by the Free Software Foundation.
013 */
014
015package ch.qos.logback.core.util;
016
017import java.util.concurrent.atomic.AtomicLong;
018
019/**
020 * A simple time-based guard that limits the number of allowed operations within a sliding time window.
021 * This class is useful for rate limiting or preventing excessive actions over time periods.
022 * It supports time injection for testing purposes.
023 *
024 * @author Ceki Gülcü
025 * @since 1.5.22
026 */
027public class SimpleTimeBasedGuard {
028
029    private final long windowDurationMs;
030    private final int maxAllows;
031
032    /**
033     * Default window duration in milliseconds: 30 minutes.
034     */
035    public static final long DEFAULT_WINDOW_MS = 30*60_000L; // 30 minutes
036
037    /**
038     * Default maximum number of allows per window: 2.
039     */
040    public static final int DEFAULT_MAX_ALLOWS = 2;
041
042    // Injectable time
043    private final AtomicLong artificialTime = new AtomicLong(-1L);
044
045    // Current window state
046    private volatile long windowStartMs = 0;
047    private volatile int allowsUsed = 0;
048
049    /**
050     * Creates a guard with custom limits.
051     *
052     * @param windowDurationMs how many millis per window (e.g. 30_000 for 30 minutes)
053     * @param maxAllows        how many allows per window (e.g. 2)
054     */
055    public SimpleTimeBasedGuard(long windowDurationMs, int maxAllows) {
056        if (windowDurationMs <= 0) throw new IllegalArgumentException("windowDurationMs must be > 0");
057        if (maxAllows < 1) throw new IllegalArgumentException("maxAllows must be >= 1");
058
059        this.windowDurationMs = windowDurationMs;
060        this.maxAllows = maxAllows;
061    }
062
063    /**
064     * Convenience: uses defaults — 2 allows every 30 minutes
065     */
066    public SimpleTimeBasedGuard() {
067        this(DEFAULT_WINDOW_MS, DEFAULT_MAX_ALLOWS);
068    }
069
070    /**
071     * Checks if an operation is allowed based on the current time window.
072     * If allowed, increments the usage count for the current window.
073     * If the window has expired, resets the window and allows the operation.
074     *
075     * @return true if the operation is allowed, false otherwise
076     */
077    public synchronized boolean allow() {
078        long now = currentTimeMillis();
079
080        // First call ever
081        if (windowStartMs == 0) {
082            windowStartMs = now;
083            allowsUsed = 1;
084            return true;
085        }
086
087        // Still in current window?
088        if (now < windowStartMs + windowDurationMs) {
089            if (allowsUsed < maxAllows) {
090                allowsUsed++;
091                return true;
092            }
093            return false;
094        }
095
096        // New window → reset
097        windowStartMs = now;
098        allowsUsed = 1;
099        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}