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}