|
9 | 9 |
|
10 | 10 | import org.apache.logging.log4j.LogManager; |
11 | 11 | import org.apache.logging.log4j.Logger; |
| 12 | +import org.elasticsearch.common.Strings; |
12 | 13 | import org.elasticsearch.core.TimeValue; |
13 | 14 | import org.elasticsearch.threadpool.Scheduler; |
14 | 15 | import org.elasticsearch.threadpool.ThreadPool; |
15 | 16 |
|
16 | 17 | import java.io.Closeable; |
17 | 18 | import java.time.Clock; |
18 | | -import java.time.Duration; |
19 | 19 | import java.time.Instant; |
20 | 20 | import java.util.Objects; |
21 | 21 | import java.util.concurrent.ConcurrentHashMap; |
22 | 22 | import java.util.concurrent.ConcurrentMap; |
23 | 23 | import java.util.concurrent.atomic.AtomicBoolean; |
| 24 | +import java.util.concurrent.atomic.AtomicLong; |
24 | 25 | import java.util.concurrent.atomic.AtomicReference; |
25 | 26 | import java.util.function.Consumer; |
26 | 27 |
|
27 | | -import static org.elasticsearch.core.Strings.format; |
28 | 28 | import static org.elasticsearch.xpack.inference.InferencePlugin.UTILITY_THREAD_POOL_NAME; |
29 | 29 |
|
30 | 30 | /** |
31 | | - * A class that throttles calls to a logger. If a log call is made during the throttle period a counter is incremented. |
32 | | - * If a log call occurs after the throttle period, then the call will proceed, and it will include a message like |
33 | | - * "repeated X times" to indicate how often the message was attempting to be logged. |
| 31 | + * A class that throttles calls to a logger. The first unique log message is permitted to emit a message. Any subsequent log messages |
| 32 | + * matching a message that has already been emitted will only increment a counter. A thread runs on an interval |
| 33 | + * to emit any log messages that have been repeated beyond the initial emitted message. Once the thread emits a repeated |
| 34 | + * message the counter is reset. If another message is received matching a previously emitted message by the thread, it will be consider |
| 35 | + * the first time a unique message is received and will be logged. |
34 | 36 | */ |
35 | 37 | public class Throttler implements Closeable { |
36 | 38 |
|
37 | 39 | private static final Logger classLogger = LogManager.getLogger(Throttler.class); |
38 | 40 |
|
39 | | - private final TimeValue resetInterval; |
40 | | - private Duration durationToWait; |
| 41 | + private final TimeValue loggingInterval; |
41 | 42 | private final Clock clock; |
42 | 43 | private final ConcurrentMap<String, LogExecutor> logExecutors; |
43 | 44 | private final AtomicReference<Scheduler.Cancellable> cancellableTask = new AtomicReference<>(); |
44 | 45 | private final AtomicBoolean isRunning = new AtomicBoolean(true); |
| 46 | + private final ThreadPool threadPool; |
45 | 47 |
|
46 | 48 | /** |
47 | | - * Constructs the throttler and kicks of a scheduled tasks to clear the internal stats. |
48 | | - * |
49 | | - * @param resetInterval the frequency for clearing the internal stats. This protects against an ever growing |
50 | | - * cache |
51 | | - * @param durationToWait the amount of time to wait before logging a message after the threshold |
52 | | - * is reached |
| 49 | + * @param loggingInterval the frequency to run a task to emit repeated log messages |
53 | 50 | * @param threadPool a thread pool for running a scheduled task to clear the internal stats |
54 | 51 | */ |
55 | | - public Throttler(TimeValue resetInterval, TimeValue durationToWait, ThreadPool threadPool) { |
56 | | - this(resetInterval, durationToWait, Clock.systemUTC(), threadPool, new ConcurrentHashMap<>()); |
| 52 | + public Throttler(TimeValue loggingInterval, ThreadPool threadPool) { |
| 53 | + this(loggingInterval, Clock.systemUTC(), threadPool, new ConcurrentHashMap<>()); |
| 54 | + } |
| 55 | + |
| 56 | + /** |
| 57 | + * @param oldThrottler a previous throttler that is being replaced |
| 58 | + * @param loggingInterval the frequency to run a task to emit repeated log messages |
| 59 | + */ |
| 60 | + public Throttler(Throttler oldThrottler, TimeValue loggingInterval) { |
| 61 | + this(loggingInterval, oldThrottler.clock, oldThrottler.threadPool, new ConcurrentHashMap<>(oldThrottler.logExecutors)); |
57 | 62 | } |
58 | 63 |
|
59 | 64 | /** |
60 | 65 | * This should only be used directly for testing. |
61 | 66 | */ |
62 | | - Throttler( |
63 | | - TimeValue resetInterval, |
64 | | - TimeValue durationToWait, |
65 | | - Clock clock, |
66 | | - ThreadPool threadPool, |
67 | | - ConcurrentMap<String, LogExecutor> logExecutors |
68 | | - ) { |
69 | | - Objects.requireNonNull(durationToWait); |
70 | | - Objects.requireNonNull(threadPool); |
71 | | - |
72 | | - this.resetInterval = Objects.requireNonNull(resetInterval); |
73 | | - this.durationToWait = Duration.ofMillis(durationToWait.millis()); |
| 67 | + Throttler(TimeValue loggingInterval, Clock clock, ThreadPool threadPool, ConcurrentMap<String, LogExecutor> logExecutors) { |
| 68 | + this.threadPool = Objects.requireNonNull(threadPool); |
| 69 | + this.loggingInterval = Objects.requireNonNull(loggingInterval); |
74 | 70 | this.clock = Objects.requireNonNull(clock); |
75 | 71 | this.logExecutors = Objects.requireNonNull(logExecutors); |
| 72 | + } |
76 | 73 |
|
77 | | - this.cancellableTask.set(startResetTask(threadPool)); |
| 74 | + public void init() { |
| 75 | + cancellableTask.set(startRepeatingLogEmitter()); |
78 | 76 | } |
79 | 77 |
|
80 | | - private Scheduler.Cancellable startResetTask(ThreadPool threadPool) { |
81 | | - classLogger.debug(() -> format("Reset task scheduled with interval [%s]", resetInterval)); |
| 78 | + private Scheduler.Cancellable startRepeatingLogEmitter() { |
| 79 | + classLogger.debug(() -> Strings.format("Scheduling repeating log emitter with interval [%s]", loggingInterval)); |
82 | 80 |
|
83 | | - return threadPool.scheduleWithFixedDelay(logExecutors::clear, resetInterval, threadPool.executor(UTILITY_THREAD_POOL_NAME)); |
| 81 | + return threadPool.scheduleWithFixedDelay(this::emitRepeatedLogs, loggingInterval, threadPool.executor(UTILITY_THREAD_POOL_NAME)); |
84 | 82 | } |
85 | 83 |
|
86 | | - public void setDurationToWait(TimeValue durationToWait) { |
87 | | - this.durationToWait = Duration.ofMillis(durationToWait.millis()); |
| 84 | + private void emitRepeatedLogs() { |
| 85 | + if (isRunning.get() == false) { |
| 86 | + return; |
| 87 | + } |
| 88 | + |
| 89 | + for (var iter = logExecutors.values().iterator(); iter.hasNext();) { |
| 90 | + var executor = iter.next(); |
| 91 | + iter.remove(); |
| 92 | + executor.logRepeatedMessages(); |
| 93 | + } |
88 | 94 | } |
89 | 95 |
|
90 | | - public void execute(String message, Consumer<String> consumer) { |
| 96 | + public void execute(String message, Consumer<String> logCallback) { |
91 | 97 | if (isRunning.get() == false) { |
92 | 98 | return; |
93 | 99 | } |
94 | 100 |
|
95 | | - LogExecutor logExecutor = logExecutors.compute(message, (key, value) -> { |
| 101 | + var logExecutor = logExecutors.compute(message, (key, value) -> { |
96 | 102 | if (value == null) { |
97 | | - return new LogExecutor(clock, consumer); |
| 103 | + return new LogExecutor(clock, logCallback, message); |
98 | 104 | } |
99 | 105 |
|
100 | | - return value.compute(consumer, durationToWait); |
| 106 | + return value; |
101 | 107 | }); |
102 | 108 |
|
103 | | - // This executes an internal consumer that wraps the passed in one, it will either log the message passed here |
104 | | - // unchanged, do nothing if it is in the throttled period, or log this message + some text saying how many times it was repeated |
105 | | - logExecutor.log(message); |
| 109 | + logExecutor.logFirstMessage(); |
106 | 110 | } |
107 | 111 |
|
108 | 112 | @Override |
109 | 113 | public void close() { |
110 | 114 | isRunning.set(false); |
111 | | - cancellableTask.get().cancel(); |
| 115 | + if (cancellableTask.get() != null) { |
| 116 | + cancellableTask.get().cancel(); |
| 117 | + } |
112 | 118 | logExecutors.clear(); |
113 | 119 | } |
114 | 120 |
|
115 | | - private static class LogExecutor { |
116 | | - private final long skippedLogCalls; |
117 | | - private final Instant timeOfLastLogCall; |
| 121 | + // default for testing |
| 122 | + static class LogExecutor { |
| 123 | + // -1 here because we need to determine if we haven't logged the first time |
| 124 | + // After the first time we'll set it to 0, then the thread that runs on an interval |
| 125 | + // needs to know if there are any repeated message, if it sees 0, it knows there are none |
| 126 | + // and skips emitting the message again. |
| 127 | + private static final long INITIAL_LOG_COUNTER_VALUE = -1; |
| 128 | + |
| 129 | + private final AtomicLong skippedLogCalls; |
| 130 | + private Instant timeOfLastLogCall; |
118 | 131 | private final Clock clock; |
119 | | - private final Consumer<String> consumer; |
| 132 | + private final Consumer<String> throttledConsumer; |
| 133 | + private final String originalMessage; |
120 | 134 |
|
121 | | - LogExecutor(Clock clock, Consumer<String> throttledConsumer) { |
122 | | - this(clock, 0, throttledConsumer); |
| 135 | + LogExecutor(Clock clock, Consumer<String> throttledConsumer, String originalMessage) { |
| 136 | + this(clock, INITIAL_LOG_COUNTER_VALUE, throttledConsumer, originalMessage); |
123 | 137 | } |
124 | 138 |
|
125 | | - LogExecutor(Clock clock, long skippedLogCalls, Consumer<String> consumer) { |
126 | | - this.skippedLogCalls = skippedLogCalls; |
| 139 | + LogExecutor(Clock clock, long skippedLogCalls, Consumer<String> throttledConsumer, String originalMessage) { |
| 140 | + this.skippedLogCalls = new AtomicLong(skippedLogCalls); |
127 | 141 | this.clock = Objects.requireNonNull(clock); |
128 | 142 | timeOfLastLogCall = Instant.now(this.clock); |
129 | | - this.consumer = Objects.requireNonNull(consumer); |
| 143 | + this.throttledConsumer = Objects.requireNonNull(throttledConsumer); |
| 144 | + this.originalMessage = Objects.requireNonNull(originalMessage); |
130 | 145 | } |
131 | 146 |
|
132 | | - void log(String message) { |
133 | | - this.consumer.accept(message); |
| 147 | + void logRepeatedMessages() { |
| 148 | + long numSkippedLogCalls; |
| 149 | + synchronized (skippedLogCalls) { |
| 150 | + numSkippedLogCalls = skippedLogCalls.get(); |
| 151 | + if (hasRepeatedLogsToEmit(numSkippedLogCalls) == false) { |
| 152 | + return; |
| 153 | + } |
| 154 | + } |
| 155 | + |
| 156 | + String enrichedMessage; |
| 157 | + if (numSkippedLogCalls == 1) { |
| 158 | + enrichedMessage = Strings.format("%s, repeated 1 time, last message at [%s]", originalMessage, timeOfLastLogCall); |
| 159 | + } else { |
| 160 | + enrichedMessage = Strings.format( |
| 161 | + "%s, repeated %s times, last message at [%s]", |
| 162 | + originalMessage, |
| 163 | + skippedLogCalls, |
| 164 | + timeOfLastLogCall |
| 165 | + ); |
| 166 | + } |
| 167 | + |
| 168 | + throttledConsumer.accept(enrichedMessage); |
134 | 169 | } |
135 | 170 |
|
136 | | - LogExecutor compute(Consumer<String> executor, Duration durationToWait) { |
137 | | - if (hasDurationExpired(durationToWait)) { |
138 | | - String messageToAppend = ""; |
139 | | - if (this.skippedLogCalls == 1) { |
140 | | - messageToAppend = ", repeated 1 time"; |
141 | | - } else if (this.skippedLogCalls > 1) { |
142 | | - messageToAppend = format(", repeated %s times", this.skippedLogCalls); |
143 | | - } |
| 171 | + private static boolean hasRepeatedLogsToEmit(long numSkippedLogCalls) { |
| 172 | + return numSkippedLogCalls > 0; |
| 173 | + } |
144 | 174 |
|
145 | | - final String stringToAppend = messageToAppend; |
146 | | - return new LogExecutor(this.clock, 0, (message) -> executor.accept(message.concat(stringToAppend))); |
| 175 | + void logFirstMessage() { |
| 176 | + long numSkippedLogCalls; |
| 177 | + synchronized (skippedLogCalls) { |
| 178 | + numSkippedLogCalls = skippedLogCalls.getAndIncrement(); |
147 | 179 | } |
148 | 180 |
|
149 | | - // This creates a consumer that won't do anything because the original consumer is being throttled |
150 | | - return new LogExecutor(this.clock, this.skippedLogCalls + 1, (message) -> {}); |
| 181 | + timeOfLastLogCall = Instant.now(this.clock); |
| 182 | + |
| 183 | + if (hasLoggedOriginalMessage(numSkippedLogCalls) == false) { |
| 184 | + this.throttledConsumer.accept(originalMessage); |
| 185 | + } |
151 | 186 | } |
152 | 187 |
|
153 | | - private boolean hasDurationExpired(Duration durationToWait) { |
154 | | - Instant now = Instant.now(clock); |
155 | | - return now.isAfter(timeOfLastLogCall.plus(durationToWait)); |
| 188 | + private static boolean hasLoggedOriginalMessage(long numSkippedLogCalls) { |
| 189 | + // a negative value indicates that we haven't yet logged the original message |
| 190 | + return numSkippedLogCalls >= 0; |
156 | 191 | } |
157 | 192 | } |
158 | 193 | } |
0 commit comments