Skip to content

Commit 6131a3a

Browse files
committed
use a slightly more sophisticated guard for printing status messages
Signed-off-by: ceki <ceki@qos.ch>
1 parent 9efca21 commit 6131a3a

File tree

3 files changed

+291
-6
lines changed

3 files changed

+291
-6
lines changed

logback-core/src/main/java/ch/qos/logback/core/UnsynchronizedAppenderBase.java

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import ch.qos.logback.core.status.WarnStatus;
2323
import ch.qos.logback.core.util.ReentryGuard;
2424
import ch.qos.logback.core.util.ReentryGuardFactory;
25+
import ch.qos.logback.core.util.SimpleTimeBasedGuard;
2526

2627
/**
2728
* Similar to {@link AppenderBase} except that derived appenders need to handle thread
@@ -52,14 +53,14 @@ public String getName() {
5253
return name;
5354
}
5455

55-
private int statusRepeatCount = 0;
56-
private int exceptionCount = 0;
56+
private SimpleTimeBasedGuard notStartedGuard = new SimpleTimeBasedGuard();
57+
private SimpleTimeBasedGuard exceptionGuard = new SimpleTimeBasedGuard();
5758

58-
static final int ALLOWED_REPEATS = 3;
5959

6060
public void doAppend(E eventObject) {
6161
if (!this.started) {
62-
if (statusRepeatCount++ < ALLOWED_REPEATS) {
62+
63+
if (notStartedGuard.allow()) {
6364
addStatus(new WarnStatus("Attempted to append to non started appender [" + name + "].", this));
6465
}
6566
return;
@@ -81,7 +82,7 @@ public void doAppend(E eventObject) {
8182
this.append(eventObject);
8283

8384
} catch (Exception e) {
84-
if (exceptionCount++ < ALLOWED_REPEATS) {
85+
if (exceptionGuard.allow()) {
8586
addError("Appender [" + name + "] failed to append.", e);
8687
}
8788
} finally {
@@ -123,7 +124,7 @@ public void start() {
123124
* </p>
124125
*
125126
* @return a non-null {@link ReentryGuard} used to detect and prevent
126-
* re-entrant appends. By default this is a no-op guard.
127+
* re-entrant appends. By default, this is a no-op guard.
127128
* @since 1.5.21
128129
*/
129130
protected ReentryGuard buildReentryGuard() {
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
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&uuml;lc&uuml;
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+
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
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 org.junit.jupiter.api.*;
18+
19+
import static org.junit.jupiter.api.Assertions.*;
20+
21+
22+
class SimpleTimeBasedGuardTest {
23+
// Nov 24 20:10:52 UTC 2025
24+
long UTC_2025_11_24H201052 = 1764015052000L;
25+
26+
27+
private SimpleTimeBasedGuard guard;
28+
29+
@BeforeEach
30+
void setUp() {
31+
guard = new SimpleTimeBasedGuard();
32+
}
33+
34+
@AfterEach
35+
void tearDown() {
36+
guard.clearCurrentTime();
37+
}
38+
39+
@Test
40+
void allowsTwoPer30Minutes() {
41+
guard.setCurrentTimeMillis(UTC_2025_11_24H201052);
42+
43+
assertTrue(guard.allow());
44+
assertTrue(guard.allow());
45+
assertFalse(guard.allow());
46+
assertFalse(guard.allow());
47+
48+
assertEquals(0, guard.getAllowsRemaining());
49+
}
50+
51+
@Test
52+
void resetsCompletelyAfter30Minutes() {
53+
guard.setCurrentTimeMillis(UTC_2025_11_24H201052);
54+
guard.allow();
55+
guard.allow(); // use both
56+
57+
// Move exactly 30 minutes forward
58+
guard.incCurrentTimeMillis(30 * 60 * 1000);
59+
60+
assertTrue(guard.allow()); // new window!
61+
assertTrue(guard.allow());
62+
assertFalse(guard.allow());
63+
}
64+
65+
@Test
66+
void windowsAreFloorAlignedTo30MinuteBoundaries() {
67+
guard.setCurrentTimeMillis(10_000); // t=10s
68+
guard.allow();
69+
guard.allow();
70+
71+
guard.incCurrentTimeMillis(29 * 60 * 1000L + 48_000L); // still same window
72+
assertFalse(guard.allow());
73+
74+
guard.setCurrentTimeMillis(30 * 60 * 1000L+10_001); // exactly 30 min → new window
75+
assertTrue(guard.allow());
76+
}
77+
78+
@Test
79+
void worksWithRealTimeWhenNotInjected() throws Exception {
80+
guard.clearCurrentTime();
81+
82+
assertTrue(guard.allow());
83+
assertTrue(guard.allow());
84+
assertFalse(guard.allow());
85+
86+
// Fast-forward real time by 30+ minutes (for CI, use fake time instead)
87+
// In real app: just wait 30 min
88+
}
89+
90+
@Test
91+
void configurableLimitsWork() {
92+
int fiveMinutesMs = 5 * 60_000;
93+
SimpleTimeBasedGuard guard = new SimpleTimeBasedGuard(fiveMinutesMs, 3); // 3 allows every 5 minutes
94+
95+
guard.setCurrentTimeMillis(10);
96+
assertTrue(guard.allow());
97+
assertTrue(guard.allow());
98+
assertTrue(guard.allow());
99+
assertFalse(guard.allow());
100+
101+
guard.setCurrentTimeMillis(fiveMinutesMs + 10); // exactly 5 minutes later
102+
assertTrue(guard.allow()); // fresh window!
103+
}
104+
}

0 commit comments

Comments
 (0)