From 9647fff087ea44d5daccf263118a89ee31786020 Mon Sep 17 00:00:00 2001 From: zhangyukun03 Date: Tue, 23 Dec 2025 21:32:43 +0800 Subject: [PATCH 1/6] feature:support random delay compression --- .../rolling/DefaultRolloverStrategy.java | 78 ++++--- .../action/DelayedCompressionAction.java | 200 ++++++++++++++++++ 2 files changed, 252 insertions(+), 26 deletions(-) create mode 100644 log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/action/DelayedCompressionAction.java diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/DefaultRolloverStrategy.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/DefaultRolloverStrategy.java index ea1bae76696..1641c253e82 100644 --- a/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/DefaultRolloverStrategy.java +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/DefaultRolloverStrategy.java @@ -16,34 +16,21 @@ */ package org.apache.logging.log4j.core.appender.rolling; +import org.apache.logging.log4j.core.Core; +import org.apache.logging.log4j.core.appender.rolling.action.*; +import org.apache.logging.log4j.core.config.Configuration; +import org.apache.logging.log4j.core.config.plugins.*; +import org.apache.logging.log4j.core.internal.annotation.SuppressFBWarnings; +import org.apache.logging.log4j.core.lookup.StrSubstitutor; +import org.apache.logging.log4j.core.util.Integers; + import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.SortedMap; +import java.util.*; import java.util.concurrent.TimeUnit; import java.util.zip.Deflater; -import org.apache.logging.log4j.core.Core; -import org.apache.logging.log4j.core.appender.rolling.action.Action; -import org.apache.logging.log4j.core.appender.rolling.action.CompositeAction; -import org.apache.logging.log4j.core.appender.rolling.action.FileRenameAction; -import org.apache.logging.log4j.core.appender.rolling.action.PathCondition; -import org.apache.logging.log4j.core.appender.rolling.action.PosixViewAttributeAction; -import org.apache.logging.log4j.core.config.Configuration; -import org.apache.logging.log4j.core.config.plugins.Plugin; -import org.apache.logging.log4j.core.config.plugins.PluginAttribute; -import org.apache.logging.log4j.core.config.plugins.PluginBuilderAttribute; -import org.apache.logging.log4j.core.config.plugins.PluginBuilderFactory; -import org.apache.logging.log4j.core.config.plugins.PluginConfiguration; -import org.apache.logging.log4j.core.config.plugins.PluginElement; -import org.apache.logging.log4j.core.config.plugins.PluginFactory; -import org.apache.logging.log4j.core.internal.annotation.SuppressFBWarnings; -import org.apache.logging.log4j.core.lookup.StrSubstitutor; -import org.apache.logging.log4j.core.util.Integers; /** * When rolling over, DefaultRolloverStrategy renames files according to an algorithm as described below. @@ -109,6 +96,11 @@ public static class Builder implements org.apache.logging.log4j.core.util.Builde @PluginBuilderAttribute(value = "tempCompressedFilePattern") private String tempCompressedFilePattern; + @PluginBuilderAttribute("delayedCompressionSeconds") + private String delayedCompressionSecondsStr; + + private long delayedCompressionSeconds; + @PluginConfiguration private Configuration config; @@ -145,6 +137,7 @@ public DefaultRolloverStrategy build() { final String trimmedCompressionLevelStr = compressionLevelStr != null ? compressionLevelStr.trim() : compressionLevelStr; final int compressionLevel = Integers.parseInt(trimmedCompressionLevelStr, Deflater.DEFAULT_COMPRESSION); + final int delayedCompressionSeconds = Integer.parseInt(delayedCompressionSecondsStr, 0); // The config object can be null when this object is built programmatically. final StrSubstitutor nonNullStrSubstitutor = config != null ? config.getStrSubstitutor() : new StrSubstitutor(); @@ -156,7 +149,8 @@ public DefaultRolloverStrategy build() { nonNullStrSubstitutor, customActions, stopCustomActionsOnError, - tempCompressedFilePattern); + tempCompressedFilePattern, + delayedCompressionSeconds); } public String getMax() { @@ -272,6 +266,18 @@ public Builder setTempCompressedFilePattern(final String tempCompressedFilePatte return this; } + /** + * Defines the maximum delay in seconds for compression. + * + * @param delayedCompressionSeconds the maximum delay in seconds (0 means immediate) + * @return This builder for chaining convenience + * @since 2.26.0 + */ + public Builder setDelayedCompressionSeconds(final String delayedCompressionSeconds) { + this.delayedCompressionSecondsStr = delayedCompressionSeconds; + return this; + } + public Configuration getConfig() { return config; } @@ -419,6 +425,7 @@ public static DefaultRolloverStrategy createStrategy( private final List customActions; private final boolean stopCustomActionsOnError; private final PatternProcessor tempCompressedFilePattern; + private final int delayedCompressionSeconds; /** * Constructs a new instance. @@ -446,7 +453,8 @@ protected DefaultRolloverStrategy( strSubstitutor, customActions, stopCustomActionsOnError, - null); + null, + 0L); } /** @@ -467,7 +475,8 @@ protected DefaultRolloverStrategy( final StrSubstitutor strSubstitutor, final Action[] customActions, final boolean stopCustomActionsOnError, - final String tempCompressedFilePatternString) { + final String tempCompressedFilePatternString, + final int delayedCompressionSeconds) { super(strSubstitutor); this.minIndex = minIndex; this.maxIndex = maxIndex; @@ -477,6 +486,7 @@ protected DefaultRolloverStrategy( this.customActions = customActions == null ? Collections.emptyList() : Arrays.asList(customActions); this.tempCompressedFilePattern = tempCompressedFilePatternString != null ? new PatternProcessor(tempCompressedFilePatternString) : null; + this.delayedCompressionSeconds = delayedCompressionSeconds; } public int getCompressionLevel() { @@ -713,7 +723,13 @@ public RolloverDescription rollover(final RollingFileManager manager) throws Sec final FileRenameAction renameAction = new FileRenameAction(new File(currentFileName), new File(renameTo), manager.isRenameEmptyFiles()); - final Action asyncAction = merge(compressAction, customActions, stopCustomActionsOnError); + Action asyncAction = merge(compressAction, customActions, stopCustomActionsOnError); + + // Apply delayed compression if configured + if (asyncAction != null && delayedCompressionSeconds > 0) { + asyncAction = new DelayedCompressionAction(asyncAction, delayedCompressionSeconds); + } + return new RolloverDescriptionImpl(currentFileName, false, renameAction, asyncAction); } @@ -721,4 +737,14 @@ public RolloverDescription rollover(final RollingFileManager manager) throws Sec public String toString() { return "DefaultRolloverStrategy(min=" + minIndex + ", max=" + maxIndex + ", useMax=" + useMax + ")"; } + + /** + * Gets the maximum delay in seconds for compression. + * + * @return the maximum delay in seconds + */ + public int getDelayedCompressionSeconds() { + return delayedCompressionSeconds; + } + } diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/action/DelayedCompressionAction.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/action/DelayedCompressionAction.java new file mode 100644 index 00000000000..0aa4d5cfb28 --- /dev/null +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/action/DelayedCompressionAction.java @@ -0,0 +1,200 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to you under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.logging.log4j.core.appender.rolling.action; + +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.status.StatusLogger; + +import java.io.IOException; +import java.util.concurrent.ThreadLocalRandom; + +import static java.util.Objects.requireNonNull; + +/** + * Wrapper action that schedules compression for delayed execution. + * This action wraps another action and schedules it to be executed + * after a random delay within a specified time window. + */ +public class DelayedCompressionAction implements Action { + + private static final Logger LOGGER = StatusLogger.getLogger(); + + private final Action originalAction; + private final int maxDelaySeconds; + private boolean complete = false; + private boolean interrupted = false; + + /** + * Creates a new DelayedCompressionAction. + * + * @param originalAction the action to be executed with delay + * @param maxDelaySeconds the maximum delay in seconds (0 means immediate execution) + */ + public DelayedCompressionAction(Action originalAction, int maxDelaySeconds) { + this.originalAction = requireNonNull(originalAction, "originalAction"); + this.maxDelaySeconds = maxDelaySeconds; + } + + /** + * Executes the action with a delay using sleep. + * + * @return true if the action was successfully executed + * @throws IOException if an error occurs during execution + */ + @Override + public boolean execute() throws IOException { + if (interrupted) { + return false; + } + + try { + // Extract source file name for logging (if possible) + String sourceFile = extractSourceFileName(originalAction); + + // Calculate delay + int delaySeconds = 0; + if (maxDelaySeconds > 0) { + delaySeconds = ThreadLocalRandom.current().nextInt(maxDelaySeconds + 1); + LOGGER.debug("Scheduling compression of {} with delay of {} seconds", sourceFile, delaySeconds); + } + + // Sleep for the delay period + if (delaySeconds > 0) { + try { + Thread.sleep(delaySeconds * 1000L); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + interrupted = true; + LOGGER.warn("Delayed compression interrupted for {}", sourceFile); + return false; + } + } + + // Execute the original action after delay + if (!interrupted) { + executeAction(originalAction, sourceFile); + } + + return !interrupted; + } catch (Exception e) { + complete = true; + if (e instanceof IOException) { + throw (IOException) e; + } + throw new IOException(e); + } + } + + /** + * Runs the action. This method is called when the action is executed + * as a Runnable. + */ + @Override + public void run() { + if (!interrupted) { + try { + execute(); + } catch (final RuntimeException ex) { + LOGGER.warn("Exception during delayed compression execution", ex); + } catch (final IOException ex) { + LOGGER.warn("IOException during delayed compression execution", ex); + } catch (final Error e) { + LOGGER.warn("Error during delayed compression execution", e); + } + complete = true; + } + } + + /** + * Cancels the compression task by interrupting the current thread. + */ + @Override + public void close() { + interrupted = true; + // Interrupt the current thread if it's sleeping + Thread.currentThread().interrupt(); + } + + /** + * Executes the compression action. + * + * @param action the action to execute + * @param sourceFile the source file name (for logging) + */ + private void executeAction(Action action, String sourceFile) { + try { + LOGGER.debug("Starting delayed compression of {}", sourceFile); + boolean success = action.execute(); + if (success) { + LOGGER.debug("Successfully completed delayed compression of {}", sourceFile); + } else { + LOGGER.warn("Failed to execute delayed compression of {}", sourceFile); + } + } catch (Exception e) { + LOGGER.warn("Exception during delayed compression of {}", sourceFile, e); + } finally { + // 在finally块中设置complete状态 + complete = true; + } + } + + /** + * Checks if the action has completed. + * + * @return true if the action is complete + */ + @Override + public boolean isComplete() { + return complete; + } + + /** + * Attempts to extract the source file name from the action for logging purposes. + * + * @param action the action to extract file name from + * @return the source file name or action toString if cannot be determined + */ + private String extractSourceFileName(Action action) { + // This is a best-effort attempt to get the source file name + // The actual implementation may need to be enhanced based on the action type + return action.toString(); + } + + /** + * Gets the original action being wrapped. + * + * @return the original action + */ + public Action getOriginalAction() { + return originalAction; + } + + /** + * Gets the maximum delay in seconds for compression. + * + * @return the maximum delay in seconds + */ + public int getMaxDelaySeconds() { + return maxDelaySeconds; + } + + @Override + public String toString() { + return "DelayedCompressionAction[originalAction=" + originalAction + ", maxDelaySeconds=" + maxDelaySeconds + "]"; + } +} + From 5bc00b8d417778c961ee8a8b4a6924c8362c4035 Mon Sep 17 00:00:00 2001 From: zhangyukun03 Date: Wed, 24 Dec 2025 09:44:36 +0800 Subject: [PATCH 2/6] rename DeferredCompressionAction --- .../rolling/DefaultRolloverStrategy.java | 38 ++++++------ ...on.java => DeferredCompressionAction.java} | 60 +++++++++---------- 2 files changed, 49 insertions(+), 49 deletions(-) rename log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/action/{DelayedCompressionAction.java => DeferredCompressionAction.java} (72%) diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/DefaultRolloverStrategy.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/DefaultRolloverStrategy.java index 1641c253e82..9248e34895a 100644 --- a/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/DefaultRolloverStrategy.java +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/DefaultRolloverStrategy.java @@ -96,10 +96,10 @@ public static class Builder implements org.apache.logging.log4j.core.util.Builde @PluginBuilderAttribute(value = "tempCompressedFilePattern") private String tempCompressedFilePattern; - @PluginBuilderAttribute("delayedCompressionSeconds") - private String delayedCompressionSecondsStr; + @PluginBuilderAttribute("deferredCompressionSeconds") + private String deferredCompressionSecondsStr; - private long delayedCompressionSeconds; + private long deferredCompressionSeconds; @PluginConfiguration private Configuration config; @@ -137,7 +137,7 @@ public DefaultRolloverStrategy build() { final String trimmedCompressionLevelStr = compressionLevelStr != null ? compressionLevelStr.trim() : compressionLevelStr; final int compressionLevel = Integers.parseInt(trimmedCompressionLevelStr, Deflater.DEFAULT_COMPRESSION); - final int delayedCompressionSeconds = Integer.parseInt(delayedCompressionSecondsStr, 0); + final int deferredCompressionSeconds = Integer.parseInt(deferredCompressionSecondsStr, 0); // The config object can be null when this object is built programmatically. final StrSubstitutor nonNullStrSubstitutor = config != null ? config.getStrSubstitutor() : new StrSubstitutor(); @@ -150,7 +150,7 @@ public DefaultRolloverStrategy build() { customActions, stopCustomActionsOnError, tempCompressedFilePattern, - delayedCompressionSeconds); + deferredCompressionSeconds); } public String getMax() { @@ -267,14 +267,14 @@ public Builder setTempCompressedFilePattern(final String tempCompressedFilePatte } /** - * Defines the maximum delay in seconds for compression. + * Defines the maximum defer in seconds for compression. * - * @param delayedCompressionSeconds the maximum delay in seconds (0 means immediate) + * @param deferredCompressionSeconds the maximum defer in seconds (0 means immediate) * @return This builder for chaining convenience * @since 2.26.0 */ - public Builder setDelayedCompressionSeconds(final String delayedCompressionSeconds) { - this.delayedCompressionSecondsStr = delayedCompressionSeconds; + public Builder setDeferredCompressionSeconds(final String deferredCompressionSeconds) { + this.deferredCompressionSecondsStr = deferredCompressionSeconds; return this; } @@ -425,7 +425,7 @@ public static DefaultRolloverStrategy createStrategy( private final List customActions; private final boolean stopCustomActionsOnError; private final PatternProcessor tempCompressedFilePattern; - private final int delayedCompressionSeconds; + private final int deferredCompressionSeconds; /** * Constructs a new instance. @@ -476,7 +476,7 @@ protected DefaultRolloverStrategy( final Action[] customActions, final boolean stopCustomActionsOnError, final String tempCompressedFilePatternString, - final int delayedCompressionSeconds) { + final int deferredCompressionSeconds) { super(strSubstitutor); this.minIndex = minIndex; this.maxIndex = maxIndex; @@ -486,7 +486,7 @@ protected DefaultRolloverStrategy( this.customActions = customActions == null ? Collections.emptyList() : Arrays.asList(customActions); this.tempCompressedFilePattern = tempCompressedFilePatternString != null ? new PatternProcessor(tempCompressedFilePatternString) : null; - this.delayedCompressionSeconds = delayedCompressionSeconds; + this.deferredCompressionSeconds = deferredCompressionSeconds; } public int getCompressionLevel() { @@ -725,9 +725,9 @@ public RolloverDescription rollover(final RollingFileManager manager) throws Sec Action asyncAction = merge(compressAction, customActions, stopCustomActionsOnError); - // Apply delayed compression if configured - if (asyncAction != null && delayedCompressionSeconds > 0) { - asyncAction = new DelayedCompressionAction(asyncAction, delayedCompressionSeconds); + // Apply deferred compression if configured + if (asyncAction != null && deferredCompressionSeconds > 0) { + asyncAction = new DeferredCompressionAction(asyncAction, deferredCompressionSeconds); } return new RolloverDescriptionImpl(currentFileName, false, renameAction, asyncAction); @@ -739,12 +739,12 @@ public String toString() { } /** - * Gets the maximum delay in seconds for compression. + * Gets the maximum defer in seconds for compression. * - * @return the maximum delay in seconds + * @return the maximum defer in seconds */ - public int getDelayedCompressionSeconds() { - return delayedCompressionSeconds; + public int getDeferredCompressionSeconds() { + return deferredCompressionSeconds; } } diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/action/DelayedCompressionAction.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/action/DeferredCompressionAction.java similarity index 72% rename from log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/action/DelayedCompressionAction.java rename to log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/action/DeferredCompressionAction.java index 0aa4d5cfb28..ebea20f533b 100644 --- a/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/action/DelayedCompressionAction.java +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/action/DeferredCompressionAction.java @@ -25,32 +25,32 @@ import static java.util.Objects.requireNonNull; /** - * Wrapper action that schedules compression for delayed execution. + * Wrapper action that schedules compression for defferred execution. * This action wraps another action and schedules it to be executed - * after a random delay within a specified time window. + * after a random defer within a specified time window. */ -public class DelayedCompressionAction implements Action { +public class DeferredCompressionAction implements Action { private static final Logger LOGGER = StatusLogger.getLogger(); private final Action originalAction; - private final int maxDelaySeconds; + private final int maxDeferSeconds; private boolean complete = false; private boolean interrupted = false; /** * Creates a new DelayedCompressionAction. * - * @param originalAction the action to be executed with delay - * @param maxDelaySeconds the maximum delay in seconds (0 means immediate execution) + * @param originalAction the action to be executed with defer + * @param maxDeferSeconds the maximum defer in seconds (0 means immediate execution) */ - public DelayedCompressionAction(Action originalAction, int maxDelaySeconds) { + public DeferredCompressionAction(Action originalAction, int maxDeferSeconds) { this.originalAction = requireNonNull(originalAction, "originalAction"); - this.maxDelaySeconds = maxDelaySeconds; + this.maxDeferSeconds = maxDeferSeconds; } /** - * Executes the action with a delay using sleep. + * Executes the action with a defer using sleep. * * @return true if the action was successfully executed * @throws IOException if an error occurs during execution @@ -65,17 +65,17 @@ public boolean execute() throws IOException { // Extract source file name for logging (if possible) String sourceFile = extractSourceFileName(originalAction); - // Calculate delay - int delaySeconds = 0; - if (maxDelaySeconds > 0) { - delaySeconds = ThreadLocalRandom.current().nextInt(maxDelaySeconds + 1); - LOGGER.debug("Scheduling compression of {} with delay of {} seconds", sourceFile, delaySeconds); + // Calculate defer + int deferredSeconds = 0; + if (maxDeferSeconds > 0) { + deferredSeconds = ThreadLocalRandom.current().nextInt(maxDeferSeconds + 1); + LOGGER.debug("Scheduling compression of {} with defer of {} seconds", sourceFile, deferredSeconds); } - // Sleep for the delay period - if (delaySeconds > 0) { + // Sleep for the defer period + if (deferredSeconds > 0) { try { - Thread.sleep(delaySeconds * 1000L); + Thread.sleep(deferredSeconds * 1000L); } catch (InterruptedException e) { Thread.currentThread().interrupt(); interrupted = true; @@ -84,7 +84,7 @@ public boolean execute() throws IOException { } } - // Execute the original action after delay + // Execute the original action after defer if (!interrupted) { executeAction(originalAction, sourceFile); } @@ -109,11 +109,11 @@ public void run() { try { execute(); } catch (final RuntimeException ex) { - LOGGER.warn("Exception during delayed compression execution", ex); + LOGGER.warn("Exception during deferred compression execution", ex); } catch (final IOException ex) { - LOGGER.warn("IOException during delayed compression execution", ex); + LOGGER.warn("IOException during deferred compression execution", ex); } catch (final Error e) { - LOGGER.warn("Error during delayed compression execution", e); + LOGGER.warn("Error during deferred compression execution", e); } complete = true; } @@ -137,15 +137,15 @@ public void close() { */ private void executeAction(Action action, String sourceFile) { try { - LOGGER.debug("Starting delayed compression of {}", sourceFile); + LOGGER.debug("Starting deferred compression of {}", sourceFile); boolean success = action.execute(); if (success) { - LOGGER.debug("Successfully completed delayed compression of {}", sourceFile); + LOGGER.debug("Successfully completed deferred compression of {}", sourceFile); } else { - LOGGER.warn("Failed to execute delayed compression of {}", sourceFile); + LOGGER.warn("Failed to execute deferred compression of {}", sourceFile); } } catch (Exception e) { - LOGGER.warn("Exception during delayed compression of {}", sourceFile, e); + LOGGER.warn("Exception during deferred compression of {}", sourceFile, e); } finally { // 在finally块中设置complete状态 complete = true; @@ -184,17 +184,17 @@ public Action getOriginalAction() { } /** - * Gets the maximum delay in seconds for compression. + * Gets the maximum defer in seconds for compression. * - * @return the maximum delay in seconds + * @return the maximum defer in seconds */ - public int getMaxDelaySeconds() { - return maxDelaySeconds; + public int getMaxDeferSeconds() { + return maxDeferSeconds; } @Override public String toString() { - return "DelayedCompressionAction[originalAction=" + originalAction + ", maxDelaySeconds=" + maxDelaySeconds + "]"; + return "DeferredCompressionAction[originalAction=" + originalAction + ", maxDeferSeconds=" + maxDeferSeconds + "]"; } } From 6eeac67b79e7329b16dcb6422bd31ddc42b6ab79 Mon Sep 17 00:00:00 2001 From: zhangyukun03 Date: Wed, 31 Dec 2025 10:37:40 +0800 Subject: [PATCH 3/6] The ScheduledExecutorService is used to realize the delayed compression function, replacing the sleep in the original Action --- .../rolling/DefaultRolloverStrategy.java | 36 +-- .../appender/rolling/RollingFileManager.java | 28 ++- .../action/DeferredCompressionAction.java | 200 --------------- .../action/DelayedCompressionAction.java | 236 ++++++++++++++++++ 4 files changed, 276 insertions(+), 224 deletions(-) delete mode 100644 log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/action/DeferredCompressionAction.java create mode 100644 log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/action/DelayedCompressionAction.java diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/DefaultRolloverStrategy.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/DefaultRolloverStrategy.java index 9248e34895a..379c9211503 100644 --- a/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/DefaultRolloverStrategy.java +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/DefaultRolloverStrategy.java @@ -96,10 +96,10 @@ public static class Builder implements org.apache.logging.log4j.core.util.Builde @PluginBuilderAttribute(value = "tempCompressedFilePattern") private String tempCompressedFilePattern; - @PluginBuilderAttribute("deferredCompressionSeconds") - private String deferredCompressionSecondsStr; + @PluginBuilderAttribute("delayedCompressionSeconds") + private String delayedCompressionSecondsStr; - private long deferredCompressionSeconds; + private long delayedCompressionSeconds; @PluginConfiguration private Configuration config; @@ -137,7 +137,7 @@ public DefaultRolloverStrategy build() { final String trimmedCompressionLevelStr = compressionLevelStr != null ? compressionLevelStr.trim() : compressionLevelStr; final int compressionLevel = Integers.parseInt(trimmedCompressionLevelStr, Deflater.DEFAULT_COMPRESSION); - final int deferredCompressionSeconds = Integer.parseInt(deferredCompressionSecondsStr, 0); + final int delayedCompressionSeconds = Integer.parseInt(delayedCompressionSecondsStr, 0); // The config object can be null when this object is built programmatically. final StrSubstitutor nonNullStrSubstitutor = config != null ? config.getStrSubstitutor() : new StrSubstitutor(); @@ -150,7 +150,7 @@ public DefaultRolloverStrategy build() { customActions, stopCustomActionsOnError, tempCompressedFilePattern, - deferredCompressionSeconds); + delayedCompressionSeconds); } public String getMax() { @@ -267,14 +267,14 @@ public Builder setTempCompressedFilePattern(final String tempCompressedFilePatte } /** - * Defines the maximum defer in seconds for compression. + * Defines the maximum delay in seconds for compression. * - * @param deferredCompressionSeconds the maximum defer in seconds (0 means immediate) + * @param delayedCompressionSeconds the maximum delay in seconds (0 means immediate) * @return This builder for chaining convenience * @since 2.26.0 */ - public Builder setDeferredCompressionSeconds(final String deferredCompressionSeconds) { - this.deferredCompressionSecondsStr = deferredCompressionSeconds; + public Builder setDelayedCompressionSeconds(final String delayedCompressionSeconds) { + this.delayedCompressionSecondsStr = delayedCompressionSeconds; return this; } @@ -425,7 +425,7 @@ public static DefaultRolloverStrategy createStrategy( private final List customActions; private final boolean stopCustomActionsOnError; private final PatternProcessor tempCompressedFilePattern; - private final int deferredCompressionSeconds; + private final int delayedCompressionSeconds; /** * Constructs a new instance. @@ -476,7 +476,7 @@ protected DefaultRolloverStrategy( final Action[] customActions, final boolean stopCustomActionsOnError, final String tempCompressedFilePatternString, - final int deferredCompressionSeconds) { + final int delayedCompressionSeconds) { super(strSubstitutor); this.minIndex = minIndex; this.maxIndex = maxIndex; @@ -486,7 +486,7 @@ protected DefaultRolloverStrategy( this.customActions = customActions == null ? Collections.emptyList() : Arrays.asList(customActions); this.tempCompressedFilePattern = tempCompressedFilePatternString != null ? new PatternProcessor(tempCompressedFilePatternString) : null; - this.deferredCompressionSeconds = deferredCompressionSeconds; + this.delayedCompressionSeconds = delayedCompressionSeconds; } public int getCompressionLevel() { @@ -726,8 +726,8 @@ public RolloverDescription rollover(final RollingFileManager manager) throws Sec Action asyncAction = merge(compressAction, customActions, stopCustomActionsOnError); // Apply deferred compression if configured - if (asyncAction != null && deferredCompressionSeconds > 0) { - asyncAction = new DeferredCompressionAction(asyncAction, deferredCompressionSeconds); + if (asyncAction != null && delayedCompressionSeconds > 0) { + asyncAction = new DelayedCompressionAction(asyncAction, delayedCompressionSeconds, manager.getAsyncExecutor()); } return new RolloverDescriptionImpl(currentFileName, false, renameAction, asyncAction); @@ -739,12 +739,12 @@ public String toString() { } /** - * Gets the maximum defer in seconds for compression. + * Gets the maximum delay in seconds for compression. * - * @return the maximum defer in seconds + * @return the maximum delay in seconds */ - public int getDeferredCompressionSeconds() { - return deferredCompressionSeconds; + public int getDelayedCompressionSeconds() { + return delayedCompressionSeconds; } } diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/RollingFileManager.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/RollingFileManager.java index afdb7416585..ee8bbe72276 100644 --- a/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/RollingFileManager.java +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/RollingFileManager.java @@ -30,9 +30,9 @@ import java.util.Date; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.CopyOnWriteArrayList; -import java.util.concurrent.ExecutorService; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.Semaphore; -import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; import org.apache.logging.log4j.core.Layout; @@ -72,10 +72,18 @@ public class RollingFileManager extends FileManager { private final boolean directWrite; private final CopyOnWriteArrayList rolloverListeners = new CopyOnWriteArrayList<>(); - /* This executor pool will create a new Thread for every work async action to be performed. Using it allows - us to make sure all the Threads are completed when the Manager is stopped. */ - private final ExecutorService asyncExecutor = - new ThreadPoolExecutor(0, Integer.MAX_VALUE, 0, TimeUnit.MILLISECONDS, new EmptyQueue(), threadFactory); + /* This scheduled executor pool handles both immediate async actions and delayed compression actions. + * For immediate tasks, it creates new threads like the original ThreadPoolExecutor. + * For delayed tasks, it uses the scheduling capabilities. + * Using it allows us to make sure all the Threads are completed when the Manager is stopped. */ + private final ScheduledExecutorService asyncExecutor = new ScheduledThreadPoolExecutor(0, threadFactory) { + @Override + public void execute(Runnable command) { + // For immediate execution, create a new thread to maintain original behavior + Thread thread = getThreadFactory().newThread(command); + thread.start(); + } + }; private static final AtomicReferenceFieldUpdater triggeringPolicyUpdater = AtomicReferenceFieldUpdater.newUpdater( @@ -638,6 +646,14 @@ public RolloverStrategy getRolloverStrategy() { return this.rolloverStrategy; } + /** + * Returns the async executor service for both immediate and delayed actions. + * @return The ScheduledExecutorService + */ + public ScheduledExecutorService getAsyncExecutor() { + return this.asyncExecutor; + } + private boolean rollover(final RolloverStrategy strategy) { boolean outputStreamClosed = false; diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/action/DeferredCompressionAction.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/action/DeferredCompressionAction.java deleted file mode 100644 index ebea20f533b..00000000000 --- a/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/action/DeferredCompressionAction.java +++ /dev/null @@ -1,200 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to you under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.logging.log4j.core.appender.rolling.action; - -import org.apache.logging.log4j.Logger; -import org.apache.logging.log4j.status.StatusLogger; - -import java.io.IOException; -import java.util.concurrent.ThreadLocalRandom; - -import static java.util.Objects.requireNonNull; - -/** - * Wrapper action that schedules compression for defferred execution. - * This action wraps another action and schedules it to be executed - * after a random defer within a specified time window. - */ -public class DeferredCompressionAction implements Action { - - private static final Logger LOGGER = StatusLogger.getLogger(); - - private final Action originalAction; - private final int maxDeferSeconds; - private boolean complete = false; - private boolean interrupted = false; - - /** - * Creates a new DelayedCompressionAction. - * - * @param originalAction the action to be executed with defer - * @param maxDeferSeconds the maximum defer in seconds (0 means immediate execution) - */ - public DeferredCompressionAction(Action originalAction, int maxDeferSeconds) { - this.originalAction = requireNonNull(originalAction, "originalAction"); - this.maxDeferSeconds = maxDeferSeconds; - } - - /** - * Executes the action with a defer using sleep. - * - * @return true if the action was successfully executed - * @throws IOException if an error occurs during execution - */ - @Override - public boolean execute() throws IOException { - if (interrupted) { - return false; - } - - try { - // Extract source file name for logging (if possible) - String sourceFile = extractSourceFileName(originalAction); - - // Calculate defer - int deferredSeconds = 0; - if (maxDeferSeconds > 0) { - deferredSeconds = ThreadLocalRandom.current().nextInt(maxDeferSeconds + 1); - LOGGER.debug("Scheduling compression of {} with defer of {} seconds", sourceFile, deferredSeconds); - } - - // Sleep for the defer period - if (deferredSeconds > 0) { - try { - Thread.sleep(deferredSeconds * 1000L); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - interrupted = true; - LOGGER.warn("Delayed compression interrupted for {}", sourceFile); - return false; - } - } - - // Execute the original action after defer - if (!interrupted) { - executeAction(originalAction, sourceFile); - } - - return !interrupted; - } catch (Exception e) { - complete = true; - if (e instanceof IOException) { - throw (IOException) e; - } - throw new IOException(e); - } - } - - /** - * Runs the action. This method is called when the action is executed - * as a Runnable. - */ - @Override - public void run() { - if (!interrupted) { - try { - execute(); - } catch (final RuntimeException ex) { - LOGGER.warn("Exception during deferred compression execution", ex); - } catch (final IOException ex) { - LOGGER.warn("IOException during deferred compression execution", ex); - } catch (final Error e) { - LOGGER.warn("Error during deferred compression execution", e); - } - complete = true; - } - } - - /** - * Cancels the compression task by interrupting the current thread. - */ - @Override - public void close() { - interrupted = true; - // Interrupt the current thread if it's sleeping - Thread.currentThread().interrupt(); - } - - /** - * Executes the compression action. - * - * @param action the action to execute - * @param sourceFile the source file name (for logging) - */ - private void executeAction(Action action, String sourceFile) { - try { - LOGGER.debug("Starting deferred compression of {}", sourceFile); - boolean success = action.execute(); - if (success) { - LOGGER.debug("Successfully completed deferred compression of {}", sourceFile); - } else { - LOGGER.warn("Failed to execute deferred compression of {}", sourceFile); - } - } catch (Exception e) { - LOGGER.warn("Exception during deferred compression of {}", sourceFile, e); - } finally { - // 在finally块中设置complete状态 - complete = true; - } - } - - /** - * Checks if the action has completed. - * - * @return true if the action is complete - */ - @Override - public boolean isComplete() { - return complete; - } - - /** - * Attempts to extract the source file name from the action for logging purposes. - * - * @param action the action to extract file name from - * @return the source file name or action toString if cannot be determined - */ - private String extractSourceFileName(Action action) { - // This is a best-effort attempt to get the source file name - // The actual implementation may need to be enhanced based on the action type - return action.toString(); - } - - /** - * Gets the original action being wrapped. - * - * @return the original action - */ - public Action getOriginalAction() { - return originalAction; - } - - /** - * Gets the maximum defer in seconds for compression. - * - * @return the maximum defer in seconds - */ - public int getMaxDeferSeconds() { - return maxDeferSeconds; - } - - @Override - public String toString() { - return "DeferredCompressionAction[originalAction=" + originalAction + ", maxDeferSeconds=" + maxDeferSeconds + "]"; - } -} - diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/action/DelayedCompressionAction.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/action/DelayedCompressionAction.java new file mode 100644 index 00000000000..76e60ed2614 --- /dev/null +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/action/DelayedCompressionAction.java @@ -0,0 +1,236 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to you under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.logging.log4j.core.appender.rolling.action; + +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.status.StatusLogger; + +import java.io.IOException; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; + +import static java.util.Objects.requireNonNull; + +/** + * Wrapper action that schedules compression for delayed execution. + * This action wraps another action and schedules it to be executed + * after a random delay within a specified time window using ScheduledExecutorService. + */ +public class DelayedCompressionAction implements Action { + + private static final Logger LOGGER = StatusLogger.getLogger(); + + private final Action originalAction; + private final int maxDelaySeconds; + private final ScheduledExecutorService scheduler; + private final AtomicBoolean complete = new AtomicBoolean(false); + private final AtomicBoolean interrupted = new AtomicBoolean(false); + private final AtomicReference> scheduledTask = new AtomicReference<>(); + + /** + * Creates a new DelayedCompressionAction. + * + * @param originalAction the action to be executed with delay + * @param maxDelaySeconds the maximum delay in seconds (0 means immediate execution) + * @param scheduler the ScheduledExecutorService to use for scheduling + */ + public DelayedCompressionAction(Action originalAction, int maxDelaySeconds, ScheduledExecutorService scheduler) { + this.originalAction = requireNonNull(originalAction, "originalAction"); + this.maxDelaySeconds = maxDelaySeconds; + this.scheduler = requireNonNull(scheduler, "scheduler"); + } + + /** + * Executes the action with a delay using ScheduledExecutorService. + * + * @return true if the action was successfully executed or scheduled + * @throws IOException if an error occurs during execution + */ + @Override + public boolean execute() throws IOException { + if (interrupted.get()) { + return false; + } + + // Extract source file name for logging (if possible) + String sourceFile = extractSourceFileName(originalAction); +// Calculate delay + int delayedSeconds = 0; + try { + if (maxDelaySeconds > 0) { + delayedSeconds = ThreadLocalRandom.current().nextInt(maxDelaySeconds + 1); + LOGGER.debug("Scheduling compression of {} with delay of {} seconds", sourceFile, delayedSeconds); + } + + // Schedule the task if there's a delay period + if (delayedSeconds > 0) { + ScheduledFuture future = scheduler.schedule( + new CompressionTask(originalAction, sourceFile), + delayedSeconds, + TimeUnit.SECONDS + ); + scheduledTask.set(future); + LOGGER.debug("Compression task scheduled for {} seconds from now", delayedSeconds); + return true; + } else { + // Execute immediately if no delay + return executeAction(originalAction, sourceFile); + } + } catch (Exception e) { + if (delayedSeconds == 0) { + complete.set(true); + } + if (e instanceof IOException) { + throw (IOException) e; + } + throw new IOException(e); + } + } + + /** + * Runs the action. This method is called when the action is executed + * as a Runnable. + */ + @Override + public void run() { + if (!interrupted.get()) { + try { + execute(); + } catch (final RuntimeException ex) { + LOGGER.warn("Exception during delayed compression execution", ex); + complete.set(true); + } catch (final IOException ex) { + LOGGER.warn("IOException during delayed compression execution", ex); + complete.set(true); + } catch (final Error e) { + LOGGER.warn("Error during delayed compression execution", e); + complete.set(true); + } + } + } + + /** + * Cancels the compression task. + */ + @Override + public void close() { + interrupted.set(true); + ScheduledFuture future = scheduledTask.get(); + if (future != null) { + future.cancel(true); + } + complete.set(true); + } + + /** + * Executes the compression action immediately. + * + * @param action the action to execute + * @param sourceFile the source file name (for logging) + * @return true if execution was successful + */ + private boolean executeAction(Action action, String sourceFile) { + try { + LOGGER.debug("Starting delayed compression of {}", sourceFile); + boolean success = action.execute(); + if (success) { + LOGGER.debug("Successfully completed delayed compression of {}", sourceFile); + } else { + LOGGER.warn("Failed to execute delayed compression of {}", sourceFile); + } + return success; + } catch (Exception e) { + LOGGER.warn("Exception during delayed compression of {}", sourceFile, e); + return false; + } finally { + complete.set(true); + } + } + + /** + * Checks if the action has completed. + * + * @return true if the action is complete + */ + @Override + public boolean isComplete() { + return complete.get(); + } + + /** + * Attempts to extract the source file name from the action for logging purposes. + * + * @param action the action to extract file name from + * @return the source file name or action toString if cannot be determined + */ + private String extractSourceFileName(Action action) { + // This is a best-effort attempt to get the source file name + // The actual implementation may need to be enhanced based on the action type + return action.toString(); + } + + /** + * Gets the original action being wrapped. + * + * @return the original action + */ + public Action getOriginalAction() { + return originalAction; + } + + /** + * Gets the maximum delay in seconds for compression. + * + * @return the maximum delay in seconds + */ + public int getMaxDelaySeconds() { + return maxDelaySeconds; + } + + /** + * Task that executes the compression action. + */ + private class CompressionTask implements Runnable { + private final Action action; + private final String sourceFile; + + CompressionTask(Action action, String sourceFile) { + this.action = action; + this.sourceFile = sourceFile; + } + + @Override + public void run() { + if (!interrupted.get()) { + executeAction(action, sourceFile); + } else { + complete.set(true); + LOGGER.debug("Compression task was interrupted for {}", sourceFile); + } + } + } + + @Override + public String toString() { + return "DelayedCompressionAction[originalAction=" + originalAction + ", maxDelaySeconds=" + maxDelaySeconds + "]"; + } +} + From b719066d9537d19e4684ee2774afe7f89cbe89dc Mon Sep 17 00:00:00 2001 From: zhangyukun03 Date: Wed, 31 Dec 2025 10:40:52 +0800 Subject: [PATCH 4/6] evert imports to avoid unnecessary changes --- .../rolling/DefaultRolloverStrategy.java | 32 +++++++++++++------ 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/DefaultRolloverStrategy.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/DefaultRolloverStrategy.java index 379c9211503..f0722a120a1 100644 --- a/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/DefaultRolloverStrategy.java +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/DefaultRolloverStrategy.java @@ -16,21 +16,35 @@ */ package org.apache.logging.log4j.core.appender.rolling; -import org.apache.logging.log4j.core.Core; -import org.apache.logging.log4j.core.appender.rolling.action.*; -import org.apache.logging.log4j.core.config.Configuration; -import org.apache.logging.log4j.core.config.plugins.*; -import org.apache.logging.log4j.core.internal.annotation.SuppressFBWarnings; -import org.apache.logging.log4j.core.lookup.StrSubstitutor; -import org.apache.logging.log4j.core.util.Integers; - import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; -import java.util.*; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.SortedMap; import java.util.concurrent.TimeUnit; import java.util.zip.Deflater; +import org.apache.logging.log4j.core.Core; +import org.apache.logging.log4j.core.appender.rolling.action.Action; +import org.apache.logging.log4j.core.appender.rolling.action.CompositeAction; +import org.apache.logging.log4j.core.appender.rolling.action.DelayedCompressionAction; +import org.apache.logging.log4j.core.appender.rolling.action.FileRenameAction; +import org.apache.logging.log4j.core.appender.rolling.action.PathCondition; +import org.apache.logging.log4j.core.appender.rolling.action.PosixViewAttributeAction; +import org.apache.logging.log4j.core.config.Configuration; +import org.apache.logging.log4j.core.config.plugins.Plugin; +import org.apache.logging.log4j.core.config.plugins.PluginAttribute; +import org.apache.logging.log4j.core.config.plugins.PluginBuilderAttribute; +import org.apache.logging.log4j.core.config.plugins.PluginBuilderFactory; +import org.apache.logging.log4j.core.config.plugins.PluginConfiguration; +import org.apache.logging.log4j.core.config.plugins.PluginElement; +import org.apache.logging.log4j.core.config.plugins.PluginFactory; +import org.apache.logging.log4j.core.internal.annotation.SuppressFBWarnings; +import org.apache.logging.log4j.core.lookup.StrSubstitutor; +import org.apache.logging.log4j.core.util.Integers; /** * When rolling over, DefaultRolloverStrategy renames files according to an algorithm as described below. From e6c7db2f855cb4873285e2bdc53e6af5c1bf2c36 Mon Sep 17 00:00:00 2001 From: zhangyukun03 Date: Wed, 31 Dec 2025 10:49:49 +0800 Subject: [PATCH 5/6] add comment --- .../core/appender/rolling/DefaultRolloverStrategy.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/DefaultRolloverStrategy.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/DefaultRolloverStrategy.java index f0722a120a1..c32d5416e97 100644 --- a/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/DefaultRolloverStrategy.java +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/DefaultRolloverStrategy.java @@ -739,7 +739,12 @@ public RolloverDescription rollover(final RollingFileManager manager) throws Sec Action asyncAction = merge(compressAction, customActions, stopCustomActionsOnError); - // Apply deferred compression if configured + // Apply delayed compression if configured + // DelayedCompressionAction is applied here (rather than wrapping compressAction earlier) to ensure: + // 1. Custom actions execute immediately without delay + // 2. Only compression-related actions are delayed + // 3. POSIX attribute propagation happens before delay scheduling + // 4. Clean separation between immediate and delayed operations if (asyncAction != null && delayedCompressionSeconds > 0) { asyncAction = new DelayedCompressionAction(asyncAction, delayedCompressionSeconds, manager.getAsyncExecutor()); } From 5c0f38c0bf292513acd87ca03e01e334a906bc5d Mon Sep 17 00:00:00 2001 From: zhangyukun03 Date: Sun, 4 Jan 2026 21:22:37 +0800 Subject: [PATCH 6/6] Move the action submission to the RollingFileManager --- .../rolling/DefaultRolloverStrategy.java | 10 +- .../appender/rolling/RollingFileManager.java | 75 +++++++- .../action/DelayedCompressionAction.java | 169 +++--------------- .../appender/rolling/action/Schedulable.java | 43 +++++ 4 files changed, 147 insertions(+), 150 deletions(-) create mode 100644 log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/action/Schedulable.java diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/DefaultRolloverStrategy.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/DefaultRolloverStrategy.java index c32d5416e97..042e114cd9f 100644 --- a/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/DefaultRolloverStrategy.java +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/DefaultRolloverStrategy.java @@ -739,14 +739,10 @@ public RolloverDescription rollover(final RollingFileManager manager) throws Sec Action asyncAction = merge(compressAction, customActions, stopCustomActionsOnError); - // Apply delayed compression if configured - // DelayedCompressionAction is applied here (rather than wrapping compressAction earlier) to ensure: - // 1. Custom actions execute immediately without delay - // 2. Only compression-related actions are delayed - // 3. POSIX attribute propagation happens before delay scheduling - // 4. Clean separation between immediate and delayed operations if (asyncAction != null && delayedCompressionSeconds > 0) { - asyncAction = new DelayedCompressionAction(asyncAction, delayedCompressionSeconds, manager.getAsyncExecutor()); + // Wrap the entire async action with delay - DelayedCompressionAction will provide + // delay configuration for RollingFileManager to handle scheduling + asyncAction = new DelayedCompressionAction(asyncAction, delayedCompressionSeconds); } return new RolloverDescriptionImpl(currentFileName, false, renameAction, asyncAction); diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/RollingFileManager.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/RollingFileManager.java index ee8bbe72276..4b9965843ab 100644 --- a/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/RollingFileManager.java +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/RollingFileManager.java @@ -44,6 +44,8 @@ import org.apache.logging.log4j.core.appender.FileManager; import org.apache.logging.log4j.core.appender.rolling.action.AbstractAction; import org.apache.logging.log4j.core.appender.rolling.action.Action; +import org.apache.logging.log4j.core.appender.rolling.action.CompositeAction; +import org.apache.logging.log4j.core.appender.rolling.action.Schedulable; import org.apache.logging.log4j.core.config.Configuration; import org.apache.logging.log4j.core.internal.annotation.SuppressFBWarnings; import org.apache.logging.log4j.core.util.Constants; @@ -686,7 +688,7 @@ private boolean rollover(final RolloverStrategy strategy) { if (syncActionSuccess && descriptor.getAsynchronous() != null) { LOGGER.debug("RollingFileManager executing async {}", descriptor.getAsynchronous()); - asyncExecutor.execute(new AsyncAction(descriptor.getAsynchronous(), this)); + scheduleActionsWithDelay(descriptor.getAsynchronous()); asyncActionStarted = false; } } @@ -891,4 +893,75 @@ public boolean addAll(final Collection collection) { return false; } } + + /** + * Schedules actions with delays based on their Schedulable interface implementation. + * Actions implementing Schedulable with positive delays are scheduled individually, + * while others are executed immediately. + * + * @param action the action to schedule + */ + private void scheduleActionsWithDelay(Action action) { + if (action instanceof Schedulable) { + Schedulable schedulableAction = (Schedulable) action; + int delaySeconds = schedulableAction.getDelaySeconds(); + + if (delaySeconds > 0) { + // Schedule the action with its specific delay + LOGGER.debug("Scheduling action with {} seconds delay", delaySeconds); + asyncExecutor.schedule( + new AsyncAction(action, this), + delaySeconds, + TimeUnit.SECONDS + ); + return; + } + } + + if (action instanceof CompositeAction) { + // Handle each action in the composite separately + scheduleCompositeAction((CompositeAction) action); + } else { + // Execute immediately if no delay + asyncExecutor.execute(new AsyncAction(action, this)); + } + } + + /** + * Schedules actions within a CompositeAction based on their individual delays. + * + * @param compositeAction the CompositeAction to schedule + */ + private void scheduleCompositeAction(CompositeAction compositeAction) { + Action[] actions = compositeAction.getActions(); + for (Action action : actions) { + scheduleIndividualAction(action); + } + } + + /** + * Schedules an individual action based on its Schedulable interface. + * + * @param action the action to schedule + */ + private void scheduleIndividualAction(Action action) { + if (action instanceof Schedulable) { + Schedulable schedulableAction = (Schedulable) action; + int delaySeconds = schedulableAction.getDelaySeconds(); + + if (delaySeconds > 0) { + // Schedule the action with its specific delay + LOGGER.debug("Scheduling individual action with {} seconds delay", delaySeconds); + asyncExecutor.schedule( + new AsyncAction(action, this), + delaySeconds, + TimeUnit.SECONDS + ); + return; + } + } + + // Execute immediately if no delay + asyncExecutor.execute(new AsyncAction(action, this)); + } } diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/action/DelayedCompressionAction.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/action/DelayedCompressionAction.java index 76e60ed2614..5a61dda472d 100644 --- a/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/action/DelayedCompressionAction.java +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/action/DelayedCompressionAction.java @@ -20,137 +20,47 @@ import org.apache.logging.log4j.status.StatusLogger; import java.io.IOException; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.ThreadLocalRandom; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicReference; import static java.util.Objects.requireNonNull; /** - * Wrapper action that schedules compression for delayed execution. - * This action wraps another action and schedules it to be executed - * after a random delay within a specified time window using ScheduledExecutorService. + * Wrapper action that provides delay configuration for compression actions. + * This action wraps another action and specifies a delay time, allowing + * the scheduling to be handled externally by RollingFileManager. */ -public class DelayedCompressionAction implements Action { +public class DelayedCompressionAction extends AbstractAction implements Schedulable { private static final Logger LOGGER = StatusLogger.getLogger(); private final Action originalAction; - private final int maxDelaySeconds; - private final ScheduledExecutorService scheduler; - private final AtomicBoolean complete = new AtomicBoolean(false); - private final AtomicBoolean interrupted = new AtomicBoolean(false); - private final AtomicReference> scheduledTask = new AtomicReference<>(); + private final int delaySeconds; /** * Creates a new DelayedCompressionAction. * * @param originalAction the action to be executed with delay - * @param maxDelaySeconds the maximum delay in seconds (0 means immediate execution) - * @param scheduler the ScheduledExecutorService to use for scheduling + * @param delaySeconds the delay in seconds (0 means immediate execution) */ - public DelayedCompressionAction(Action originalAction, int maxDelaySeconds, ScheduledExecutorService scheduler) { + public DelayedCompressionAction(Action originalAction, int delaySeconds) { this.originalAction = requireNonNull(originalAction, "originalAction"); - this.maxDelaySeconds = maxDelaySeconds; - this.scheduler = requireNonNull(scheduler, "scheduler"); + this.delaySeconds = delaySeconds; } /** - * Executes the action with a delay using ScheduledExecutorService. + * Executes the wrapped action immediately. + * The delay is handled externally by RollingFileManager based on the + * getDelaySeconds() method. * - * @return true if the action was successfully executed or scheduled + * @return true if the action was successfully executed * @throws IOException if an error occurs during execution */ @Override public boolean execute() throws IOException { - if (interrupted.get()) { - return false; - } - - // Extract source file name for logging (if possible) String sourceFile = extractSourceFileName(originalAction); -// Calculate delay - int delayedSeconds = 0; - try { - if (maxDelaySeconds > 0) { - delayedSeconds = ThreadLocalRandom.current().nextInt(maxDelaySeconds + 1); - LOGGER.debug("Scheduling compression of {} with delay of {} seconds", sourceFile, delayedSeconds); - } - - // Schedule the task if there's a delay period - if (delayedSeconds > 0) { - ScheduledFuture future = scheduler.schedule( - new CompressionTask(originalAction, sourceFile), - delayedSeconds, - TimeUnit.SECONDS - ); - scheduledTask.set(future); - LOGGER.debug("Compression task scheduled for {} seconds from now", delayedSeconds); - return true; - } else { - // Execute immediately if no delay - return executeAction(originalAction, sourceFile); - } - } catch (Exception e) { - if (delayedSeconds == 0) { - complete.set(true); - } - if (e instanceof IOException) { - throw (IOException) e; - } - throw new IOException(e); - } - } - - /** - * Runs the action. This method is called when the action is executed - * as a Runnable. - */ - @Override - public void run() { - if (!interrupted.get()) { - try { - execute(); - } catch (final RuntimeException ex) { - LOGGER.warn("Exception during delayed compression execution", ex); - complete.set(true); - } catch (final IOException ex) { - LOGGER.warn("IOException during delayed compression execution", ex); - complete.set(true); - } catch (final Error e) { - LOGGER.warn("Error during delayed compression execution", e); - complete.set(true); - } - } - } - - /** - * Cancels the compression task. - */ - @Override - public void close() { - interrupted.set(true); - ScheduledFuture future = scheduledTask.get(); - if (future != null) { - future.cancel(true); - } - complete.set(true); - } - /** - * Executes the compression action immediately. - * - * @param action the action to execute - * @param sourceFile the source file name (for logging) - * @return true if execution was successful - */ - private boolean executeAction(Action action, String sourceFile) { try { LOGGER.debug("Starting delayed compression of {}", sourceFile); - boolean success = action.execute(); + boolean success = originalAction.execute(); if (success) { LOGGER.debug("Successfully completed delayed compression of {}", sourceFile); } else { @@ -159,22 +69,13 @@ private boolean executeAction(Action action, String sourceFile) { return success; } catch (Exception e) { LOGGER.warn("Exception during delayed compression of {}", sourceFile, e); - return false; - } finally { - complete.set(true); + if (e instanceof IOException) { + throw (IOException) e; + } + throw new IOException(e); } } - /** - * Checks if the action has completed. - * - * @return true if the action is complete - */ - @Override - public boolean isComplete() { - return complete.get(); - } - /** * Attempts to extract the source file name from the action for logging purposes. * @@ -197,40 +98,24 @@ public Action getOriginalAction() { } /** - * Gets the maximum delay in seconds for compression. + * Gets the delay in seconds for compression. * - * @return the maximum delay in seconds + * @return the delay in seconds */ - public int getMaxDelaySeconds() { - return maxDelaySeconds; + public int getDelaySeconds() { + return delaySeconds; } - /** - * Task that executes the compression action. - */ - private class CompressionTask implements Runnable { - private final Action action; - private final String sourceFile; - - CompressionTask(Action action, String sourceFile) { - this.action = action; - this.sourceFile = sourceFile; - } - - @Override - public void run() { - if (!interrupted.get()) { - executeAction(action, sourceFile); - } else { - complete.set(true); - LOGGER.debug("Compression task was interrupted for {}", sourceFile); - } - } + @Override + public boolean isComplete() { + // For simplicity, we consider this action complete after execution + // In a more sophisticated implementation, we might track the wrapped action's completion + return true; } @Override public String toString() { - return "DelayedCompressionAction[originalAction=" + originalAction + ", maxDelaySeconds=" + maxDelaySeconds + "]"; + return "DelayedCompressionAction[originalAction=" + originalAction + ", delaySeconds=" + delaySeconds + "]"; } } diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/action/Schedulable.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/action/Schedulable.java new file mode 100644 index 00000000000..eba4fbde87d --- /dev/null +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/action/Schedulable.java @@ -0,0 +1,43 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to you under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.logging.log4j.core.appender.rolling.action; + +/** + * Interface for objects that can be scheduled with a delay. + * This provides a generic way to define delayed execution for any type of object, + * not just actions. The delay time is specified in seconds. + */ +public interface Schedulable { + + /** + * Gets the delay time in seconds before this object should be executed or processed. + * A return value of 0 or negative means the object should be executed immediately. + * + * @return the delay in seconds (0 means execute immediately) + */ + int getDelaySeconds(); + + /** + * Checks if this object should be delayed. + * + * @return true if the object has a positive delay + */ + default boolean isDelayed() { + return getDelaySeconds() > 0; + } +} +