Skip to content

Commit 04a7ba5

Browse files
committed
most subclasses of UnsynchronizedAppenderBase do not need a reentry guard
Signed-off-by: ceki <ceki@qos.ch>
1 parent ab6a006 commit 04a7ba5

File tree

5 files changed

+293
-25
lines changed

5 files changed

+293
-25
lines changed

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

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,25 +17,53 @@
1717
import ch.qos.logback.core.spi.FilterAttachable;
1818
import ch.qos.logback.core.spi.LifeCycle;
1919

20+
/**
21+
* Contract for components responsible for delivering logging events to their
22+
* final destination (console, file, remote server, etc.).
23+
*
24+
* <p>Implementations are typically configured and managed by a LoggerContext.
25+
* The type parameter E represents the event type the appender consumes (for
26+
* example a log event object). Implementations should honor lifecycle methods
27+
* from {@link LifeCycle} and may be {@link ContextAware} and
28+
* {@link FilterAttachable} to support contextual information and filtering.</p>
29+
*
30+
* <p>Concurrency: appenders are generally invoked by multiple threads. Implementations
31+
* must ensure thread-safety where applicable (for example when writing to shared
32+
* resources). The {@link #doAppend(Object)} method may be called concurrently.</p>
33+
*
34+
* @param <E> the event type accepted by this appender
35+
*/
2036
public interface Appender<E> extends LifeCycle, ContextAware, FilterAttachable<E> {
2137

2238
/**
23-
* Get the name of this appender. The name uniquely identifies the appender.
39+
* Get the name of this appender. The name uniquely identifies the appender
40+
* within its context and is used for configuration and lookup.
41+
*
42+
* @return the appender name, or {@code null} if not set
2443
*/
2544
String getName();
2645

2746
/**
28-
* This is where an appender accomplishes its work. Note that the argument is of
29-
* type Object.
30-
*
31-
* @param event
47+
* This is where an appender accomplishes its work: format and deliver the
48+
* provided event to the appender's destination.
49+
*
50+
* <p>Implementations should apply any configured filters before outputting
51+
* the event. Implementations should avoid throwing runtime exceptions;
52+
* if an error occurs that cannot be handled internally, a {@link LogbackException}
53+
* (or a subtype) may be thrown to indicate a failure during append.</p>
54+
*
55+
* @param event the event to append; may not be {@code null}
56+
* @throws LogbackException if the append fails in a way that needs to be
57+
* propagated to the caller
3258
*/
3359
void doAppend(E event) throws LogbackException;
3460

3561
/**
3662
* Set the name of this appender. The name is used by other components to
37-
* identify this appender.
38-
*
63+
* identify and reference this appender (for example in configuration or for
64+
* status messages).
65+
*
66+
* @param name the new name for this appender; may be {@code null} to unset
3967
*/
4068
void setName(String name);
4169

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
import ch.qos.logback.core.status.Status;
2626
import ch.qos.logback.core.status.WarnStatus;
2727
import ch.qos.logback.core.util.Loader;
28+
import ch.qos.logback.core.util.ReentryGuard;
29+
import ch.qos.logback.core.util.ReentryGuardFactory;
2830

2931
/**
3032
* ConsoleAppender appends log events to <code>System.out</code> or
@@ -100,6 +102,14 @@ public void start() {
100102
super.start();
101103
}
102104

105+
/**
106+
* Create a ThreadLocal ReentryGuard to prevent recursive appender invocations.
107+
* @return a ReentryGuard instance of type {@link ReentryGuardFactory.GuardType#THREAD_LOCAL THREAD_LOCAL}.
108+
*/
109+
protected ReentryGuard buildReentryGuard() {
110+
return ReentryGuardFactory.makeGuard(ReentryGuardFactory.GuardType.THREAD_LOCAL);
111+
}
112+
103113
private OutputStream wrapWithJansi(OutputStream targetStream) {
104114
try {
105115
addInfo("Enabling JANSI AnsiPrintStream for the console.");

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

Lines changed: 42 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
import ch.qos.logback.core.spi.FilterAttachableImpl;
2121
import ch.qos.logback.core.spi.FilterReply;
2222
import ch.qos.logback.core.status.WarnStatus;
23+
import ch.qos.logback.core.util.ReentryGuard;
24+
import ch.qos.logback.core.util.ReentryGuardFactory;
2325

2426
/**
2527
* Similar to {@link AppenderBase} except that derived appenders need to handle thread
@@ -31,16 +33,13 @@
3133
abstract public class UnsynchronizedAppenderBase<E> extends ContextAwareBase implements Appender<E> {
3234

3335
protected volatile boolean started = false;
34-
35-
// using a ThreadLocal instead of a boolean add 75 nanoseconds per
36-
// doAppend invocation. This is tolerable as doAppend takes at least a few
37-
// microseconds
38-
// on a real appender
3936
/**
4037
* The guard prevents an appender from repeatedly calling its own doAppend
4138
* method.
39+
*
40+
* @since 1.5.21
4241
*/
43-
private ThreadLocal<Boolean> guard = new ThreadLocal<Boolean>();
42+
private ReentryGuard reentryGuard;
4443

4544
/**
4645
* Appenders are named.
@@ -59,23 +58,20 @@ public String getName() {
5958
static final int ALLOWED_REPEATS = 3;
6059

6160
public void doAppend(E eventObject) {
62-
// WARNING: The guard check MUST be the first statement in the
63-
// doAppend() method.
61+
if (!this.started) {
62+
if (statusRepeatCount++ < ALLOWED_REPEATS) {
63+
addStatus(new WarnStatus("Attempted to append to non started appender [" + name + "].", this));
64+
}
65+
return;
66+
}
6467

6568
// prevent re-entry.
66-
if (Boolean.TRUE.equals(guard.get())) {
69+
if (reentryGuard.isLocked()) {
6770
return;
6871
}
6972

7073
try {
71-
guard.set(Boolean.TRUE);
72-
73-
if (!this.started) {
74-
if (statusRepeatCount++ < ALLOWED_REPEATS) {
75-
addStatus(new WarnStatus("Attempted to append to non started appender [" + name + "].", this));
76-
}
77-
return;
78-
}
74+
reentryGuard.lock();
7975

8076
if (getFilterChainDecision(eventObject) == FilterReply.DENY) {
8177
return;
@@ -89,7 +85,7 @@ public void doAppend(E eventObject) {
8985
addError("Appender [" + name + "] failed to append.", e);
9086
}
9187
} finally {
92-
guard.set(Boolean.FALSE);
88+
reentryGuard.unlock();
9389
}
9490
}
9591

@@ -103,9 +99,37 @@ public void setName(String name) {
10399
}
104100

105101
public void start() {
102+
this.reentryGuard = buildReentryGuard();
106103
started = true;
107104
}
108105

106+
/**
107+
* Create a {@link ReentryGuard} instance used by this appender to prevent
108+
* recursive/re-entrant calls to {@link #doAppend(Object)}.
109+
*
110+
* <p>The default implementation returns a no-op guard produced by
111+
* {@link ReentryGuardFactory#makeGuard(ch.qos.logback.core.util.ReentryGuardFactory.GuardType)}
112+
* using {@code GuardType.NOP}. Subclasses that require actual re-entry
113+
* protection (for example using a thread-local or lock-based guard) should
114+
* override this method to return an appropriate {@link ReentryGuard}
115+
* implementation.</p>
116+
*
117+
* <p>Contract/expectations:
118+
* <ul>
119+
* <li>Called from {@link #start()} to initialize the appender's guard.</li>
120+
* <li>Implementations should be lightweight and thread-safe.</li>
121+
* <li>Return value must not be {@code null}.</li>
122+
* </ul>
123+
* </p>
124+
*
125+
* @return a non-null {@link ReentryGuard} used to detect and prevent
126+
* re-entrant appends. By default this is a no-op guard.
127+
* @since 1.5.21
128+
*/
129+
protected ReentryGuard buildReentryGuard() {
130+
return ReentryGuardFactory.makeGuard(ReentryGuardFactory.GuardType.NOP);
131+
}
132+
109133
public void stop() {
110134
started = false;
111135
}
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
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+
/**
18+
* Guard used to prevent re-entrant (recursive) appender invocations on a per-thread basis.
19+
*
20+
* <p>Implementations are used by appenders and other components that must avoid
21+
* recursively calling back into themselves (for example when an error causes
22+
* logging while handling a logging event). Typical usage: check {@link #isLocked()}
23+
* before proceeding and call {@link #lock()} / {@link #unlock()} around the
24+
* guarded region.</p>
25+
*
26+
* <p>Concurrency: guards operate on a per-thread basis; callers should treat the
27+
* guard as thread-local state. Implementations must document their semantics;
28+
* the provided {@link ReentryGuardImpl} uses a {@link ThreadLocal} to track the
29+
* locked state for the current thread.</p>
30+
*
31+
* @since 1.5.21
32+
*/
33+
public interface ReentryGuard {
34+
35+
/**
36+
* Return true if the current thread holds the guard (i.e. is inside a guarded region).
37+
*
38+
* <p>Implementations typically return {@code false} if the current thread has not
39+
* previously called {@link #lock()} or if the stored value is {@code null}.</p>
40+
*
41+
* @return {@code true} if the guard is locked for the current thread, {@code false} otherwise
42+
*/
43+
boolean isLocked();
44+
45+
/**
46+
* Mark the guard as locked for the current thread.
47+
*
48+
* <p>Callers must ensure {@link #unlock()} is invoked in a finally block to
49+
* avoid leaving the guard permanently locked for the thread.</p>
50+
*/
51+
void lock();
52+
53+
/**
54+
* Release the guard for the current thread.
55+
*
56+
* <p>After calling {@code unlock()} the {@link #isLocked()} should return
57+
* {@code false} for the current thread (unless {@code lock()} is called again).</p>
58+
*/
59+
void unlock();
60+
61+
62+
/**
63+
* Default per-thread implementation backed by a {@link ThreadLocal<Boolean>}.
64+
*
65+
* <p>Semantics: a value of {@link Boolean#TRUE} indicates the current thread
66+
* is inside a guarded region. If the ThreadLocal has no value ({@code null}),
67+
* {@link #isLocked()} treats this as unlocked (returns {@code false}).</p>
68+
*
69+
* <p>Note: this implementation intentionally uses {@code ThreadLocal<Boolean>}
70+
* to avoid global synchronization. The initial state is unlocked.</p>
71+
*
72+
* Typical usage:
73+
* <pre>
74+
* if (!guard.isLocked()) {
75+
* guard.lock();
76+
* try {
77+
* // guarded work
78+
* } finally {
79+
* guard.unlock();
80+
* }
81+
* }
82+
* </pre>
83+
*
84+
*/
85+
class ReentryGuardImpl implements ReentryGuard {
86+
87+
private ThreadLocal<Boolean> guard = new ThreadLocal<Boolean>();
88+
89+
90+
@Override
91+
public boolean isLocked() {
92+
// the guard is considered locked if the ThreadLocal contains Boolean.TRUE
93+
// note that initially the ThreadLocal contains null
94+
return (Boolean.TRUE.equals(guard.get()));
95+
}
96+
97+
@Override
98+
public void lock() {
99+
guard.set(Boolean.TRUE);
100+
}
101+
102+
@Override
103+
public void unlock() {
104+
guard.set(Boolean.FALSE);
105+
}
106+
}
107+
108+
/**
109+
* No-op implementation that never locks. Useful in contexts where re-entrancy
110+
* protection is not required.
111+
*
112+
* <p>{@link #isLocked()} always returns {@code false}. {@link #lock()} and
113+
* {@link #unlock()} are no-ops.</p>
114+
*
115+
* <p>Use this implementation when the caller explicitly wants to disable
116+
* reentrancy protection (for example in tests or in environments where the
117+
* cost of thread-local checks is undesirable and re-entrancy cannot occur).</p>
118+
*
119+
*/
120+
class NOPRentryGuard implements ReentryGuard {
121+
@Override
122+
public boolean isLocked() {
123+
return false;
124+
}
125+
126+
@Override
127+
public void lock() {
128+
// NOP
129+
}
130+
131+
@Override
132+
public void unlock() {
133+
// NOP
134+
}
135+
}
136+
137+
}

0 commit comments

Comments
 (0)