SimpleTimeBasedGuard.java
/*
* Logback: the reliable, generic, fast and flexible logging framework.
* Copyright (C) 1999-2026, QOS.ch. All rights reserved.
*
* This program and the accompanying materials are dual-licensed under
* either the terms of the Eclipse Public License v2.0 as published by
* the Eclipse Foundation
*
* or (per the licensee's choosing)
*
* under the terms of the GNU Lesser General Public License version 2.1
* as published by the Free Software Foundation.
*/
package ch.qos.logback.core.util;
import java.util.concurrent.atomic.AtomicLong;
/**
* A simple time-based guard that limits the number of allowed operations within a sliding time window.
* This class is useful for rate limiting or preventing excessive actions over time periods.
* It supports time injection for testing purposes.
*
* @author Ceki Gülcü
* @since 1.5.22
*/
public class SimpleTimeBasedGuard {
private final long windowDurationMs;
private final int maxAllows;
/**
* Default window duration in milliseconds: 30 minutes.
*/
public static final long DEFAULT_WINDOW_MS = 30*60_000L; // 30 minutes
/**
* Default maximum number of allows per window: 2.
*/
public static final int DEFAULT_MAX_ALLOWS = 2;
// Injectable time
private final AtomicLong artificialTime = new AtomicLong(-1L);
// Current window state
private volatile long windowStartMs = 0;
private volatile int allowsUsed = 0;
/**
* Creates a guard with custom limits.
*
* @param windowDurationMs how many millis per window (e.g. 30_000 for 30 minutes)
* @param maxAllows how many allows per window (e.g. 2)
*/
public SimpleTimeBasedGuard(long windowDurationMs, int maxAllows) {
if (windowDurationMs <= 0) throw new IllegalArgumentException("windowDurationMs must be > 0");
if (maxAllows < 1) throw new IllegalArgumentException("maxAllows must be >= 1");
this.windowDurationMs = windowDurationMs;
this.maxAllows = maxAllows;
}
/**
* Convenience: uses defaults ��� 2 allows every 30 minutes
*/
public SimpleTimeBasedGuard() {
this(DEFAULT_WINDOW_MS, DEFAULT_MAX_ALLOWS);
}
/**
* Checks if an operation is allowed based on the current time window.
* If allowed, increments the usage count for the current window.
* If the window has expired, resets the window and allows the operation.
*
* @return true if the operation is allowed, false otherwise
*/
public synchronized boolean allow() {
long now = currentTimeMillis();
// First call ever
if (windowStartMs == 0) {
windowStartMs = now;
allowsUsed = 1;
return true;
}
// Still in current window?
if (now < windowStartMs + windowDurationMs) {
if (allowsUsed < maxAllows) {
allowsUsed++;
return true;
}
return false;
}
// New window ��� reset
windowStartMs = now;
allowsUsed = 1;
return true;
}
// --- Time injection for testing ---
/**
* Sets the artificial current time for testing purposes.
* When set, {@link #currentTimeMillis()} will return this value instead of {@link System#currentTimeMillis()}.
*
* @param timestamp the artificial timestamp in milliseconds
*/
public void setCurrentTimeMillis(long timestamp) {
this.artificialTime.set(timestamp);
}
/**
* Clears the artificial time, reverting to using {@link System#currentTimeMillis()}.
*/
public void clearCurrentTime() {
this.artificialTime.set(-1L);
}
private long currentTimeMillis() {
long t = artificialTime.get();
return t >= 0 ? t : System.currentTimeMillis();
}
void incCurrentTimeMillis(long increment) {
artificialTime.getAndAdd(increment);
}
// --- Helpful getters ---
/**
* Returns the number of allows used in the current window.
*
* @return the number of allows used
*/
public int getAllowsUsed() {
return allowsUsed;
}
/**
* Returns the number of allows remaining in the current window.
*
* @return the number of allows remaining
*/
public int getAllowsRemaining() {
return Math.max(0, maxAllows - allowsUsed);
}
/**
* Returns the window duration in milliseconds.
*
* @return the window duration in milliseconds
*/
public long getWindowDuration() {
return windowDurationMs;
}
/**
* Returns the maximum number of allows per window.
*
* @return the maximum number of allows
*/
public int getMaxAllows() {
return maxAllows;
}
/**
* Returns the number of milliseconds until the next window starts.
* If no window has started yet, returns the full window duration.
*
* @return milliseconds until next window
*/
public long getMillisUntilNextWindow() {
if (windowStartMs == 0) return windowDurationMs;
long nextWindowStart = windowStartMs + windowDurationMs;
long now = currentTimeMillis();
return Math.max(0, nextWindowStart - now);
}
}