This is an automated email from the ASF dual-hosted git repository.
vy pushed a commit to branch 2.x
in repository https://gitbox.apache.org/repos/asf/logging-log4j2.git
The following commit(s) were added to refs/heads/2.x by this push:
new 446d2baa20 Add support for max compression delay in compression
actions (#4071)
446d2baa20 is described below
commit 446d2baa2063a90e1e03545d16cabcff093495b1
Author: Ramanathan <[email protected]>
AuthorDate: Thu May 28 14:51:18 2026 +0530
Add support for max compression delay in compression actions (#4071)
Co-authored-by: Volkan Yazıcı <[email protected]>
---
.../rolling/FileExtensionCompressDelayTest.java | 179 +++++++++++++++++++++
.../rolling/action/GzCompressActionTest.java | 120 ++++++++++++++
.../rolling/action/ZipCompressActionTest.java | 120 ++++++++++++++
.../appender/rolling/DefaultRolloverStrategy.java | 84 +++++++++-
.../log4j/core/appender/rolling/FileExtension.java | 108 ++++++++++++-
.../appender/rolling/action/AbstractAction.java | 19 +++
.../rolling/action/CommonsCompressAction.java | 27 ++++
.../appender/rolling/action/GzCompressAction.java | 50 +++++-
.../appender/rolling/action/ZipCompressAction.java | 47 +++++-
.../core/appender/rolling/action/package-info.java | 2 +-
.../log4j/core/appender/rolling/package-info.java | 2 +-
.../.2.x.x/4012_add_max_compression_delay.xml | 14 ++
.../ROOT/pages/manual/appenders/rolling-file.adoc | 9 ++
13 files changed, 758 insertions(+), 23 deletions(-)
diff --git
a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/rolling/FileExtensionCompressDelayTest.java
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/rolling/FileExtensionCompressDelayTest.java
new file mode 100644
index 0000000000..98ebf8a691
--- /dev/null
+++
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/rolling/FileExtensionCompressDelayTest.java
@@ -0,0 +1,179 @@
+/*
+ * 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;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import org.apache.logging.log4j.core.appender.rolling.action.Action;
+import org.apache.logging.log4j.core.appender.rolling.action.GzCompressAction;
+import org.apache.logging.log4j.core.appender.rolling.action.ZipCompressAction;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+/**
+ * Issue #4012 — verifies that FileExtension.GZ and FileExtension.ZIP
correctly pass
+ * maxCompressionDelaySeconds through to the compression action.
+ *
+ * This was the root cause of the bug: the 5-argument createCompressAction()
in GZ and ZIP
+ * fell back to the 4-argument version, silently dropping the delay value.
+ */
+class FileExtensionCompressDelayTest {
+
+ @Test
+ void testGzCreateCompressActionRejectsInvalidCompressionLevel(@TempDir
File tempDir) {
+ File source = new File(tempDir, "invalid-level.log");
+ File dest = new File(tempDir, "invalid-level.log.gz");
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> FileExtension.GZ.createCompressAction(source.getPath(),
dest.getPath(), true, -2, 0));
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> FileExtension.GZ.createCompressAction(source.getPath(),
dest.getPath(), true, 10, 0));
+ }
+
+ @Test
+ void testZipCreateCompressActionRejectsInvalidCompressionLevel(@TempDir
File tempDir) {
+ File source = new File(tempDir, "invalid-level.log");
+ File dest = new File(tempDir, "invalid-level.log.zip");
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> FileExtension.ZIP.createCompressAction(source.getPath(),
dest.getPath(), true, -2, 0));
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> FileExtension.ZIP.createCompressAction(source.getPath(),
dest.getPath(), true, 10, 0));
+ }
+
+ // ── GZ ────────────────────────────────────────────────────────────────
+
+ /**
+ * FileExtension.GZ.createCompressAction(5-args) must produce a
GzCompressAction
+ * that applies the random delay — not fall back to 0.
+ */
+ @Test
+ void testGzCreateCompressActionWithDelay(@TempDir File tempDir) throws
Exception {
+ File source = new File(tempDir, "app.log");
+ File dest = new File(tempDir, "app.log.gz");
+ writeContent(source, "gz test content");
+
+ int maxDelay = 2;
+ Action action =
FileExtension.GZ.createCompressAction(source.getPath(), dest.getPath(), true,
-1, maxDelay);
+
+ // Must return a GzCompressAction (not some other type)
+ assertInstanceOf(GzCompressAction.class, action, "Expected
GzCompressAction");
+
+ long start = System.currentTimeMillis();
+ action.execute();
+ long elapsed = System.currentTimeMillis() - start;
+
+ // Must finish within maxDelay + margin (delay IS applied via
FileExtension)
+ assertTrue(
+ elapsed <= (maxDelay * 1000L) + 500,
+ "GZ compress via FileExtension exceeded maxDelay=" + maxDelay
+ "s: " + elapsed + "ms");
+ assertTrue(dest.exists(), "Compressed .gz file must exist");
+ assertFalse(source.exists(), "Source must be deleted after
compression");
+ }
+
+ /**
+ * FileExtension.GZ.createCompressAction(5-args, delay=0) must behave
identically
+ * to the 4-arg version — instant compression, no delay.
+ */
+ @Test
+ void testGzCreateCompressActionNoDelay(@TempDir File tempDir) throws
Exception {
+ File source = new File(tempDir, "app-nodelay.log");
+ File dest = new File(tempDir, "app-nodelay.log.gz");
+ writeContent(source, "gz no-delay content");
+
+ Action action =
FileExtension.GZ.createCompressAction(source.getPath(), dest.getPath(), true,
-1, 0);
+
+ assertInstanceOf(GzCompressAction.class, action);
+
+ long start = System.currentTimeMillis();
+ action.execute();
+ long elapsed = System.currentTimeMillis() - start;
+
+ assertTrue(elapsed < 500, "GZ with delay=0 should be instant, took " +
elapsed + "ms");
+ assertTrue(dest.exists(), "Compressed .gz file must exist");
+ assertFalse(source.exists(), "Source must be deleted");
+ }
+
+ // ── ZIP ───────────────────────────────────────────────────────────────
+
+ /**
+ * FileExtension.ZIP.createCompressAction(5-args) must produce a
ZipCompressAction
+ * that applies the random delay — not fall back to 0.
+ */
+ @Test
+ void testZipCreateCompressActionWithDelay(@TempDir File tempDir) throws
Exception {
+ File source = new File(tempDir, "app.log");
+ File dest = new File(tempDir, "app.log.zip");
+ writeContent(source, "zip test content");
+
+ int maxDelay = 2;
+ Action action =
FileExtension.ZIP.createCompressAction(source.getPath(), dest.getPath(), true,
0, maxDelay);
+
+ assertInstanceOf(ZipCompressAction.class, action, "Expected
ZipCompressAction");
+
+ long start = System.currentTimeMillis();
+ action.execute();
+ long elapsed = System.currentTimeMillis() - start;
+
+ assertTrue(
+ elapsed <= (maxDelay * 1000L) + 500,
+ "ZIP compress via FileExtension exceeded maxDelay=" + maxDelay
+ "s: " + elapsed + "ms");
+ assertTrue(dest.exists(), "Compressed .zip file must exist");
+ assertFalse(source.exists(), "Source must be deleted after
compression");
+ }
+
+ /**
+ * FileExtension.ZIP.createCompressAction(5-args, delay=0) must behave
identically
+ * to the 4-arg version — instant compression, no delay.
+ */
+ @Test
+ void testZipCreateCompressActionNoDelay(@TempDir File tempDir) throws
Exception {
+ File source = new File(tempDir, "app-nodelay.log");
+ File dest = new File(tempDir, "app-nodelay.log.zip");
+ writeContent(source, "zip no-delay content");
+
+ Action action =
FileExtension.ZIP.createCompressAction(source.getPath(), dest.getPath(), true,
0, 0);
+
+ assertInstanceOf(ZipCompressAction.class, action);
+
+ long start = System.currentTimeMillis();
+ action.execute();
+ long elapsed = System.currentTimeMillis() - start;
+
+ assertTrue(elapsed < 500, "ZIP with delay=0 should be instant, took "
+ elapsed + "ms");
+ assertTrue(dest.exists(), "Compressed .zip file must exist");
+ assertFalse(source.exists(), "Source must be deleted");
+ }
+
+ // ── helpers ───────────────────────────────────────────────────────────
+
+ private static void writeContent(final File file, final String content)
throws IOException {
+ try (FileWriter writer = new FileWriter(file)) {
+ writer.write(content);
+ }
+ }
+}
diff --git
a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/rolling/action/GzCompressActionTest.java
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/rolling/action/GzCompressActionTest.java
new file mode 100644
index 0000000000..14d72c56dd
--- /dev/null
+++
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/rolling/action/GzCompressActionTest.java
@@ -0,0 +1,120 @@
+/*
+ * 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 static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.util.zip.Deflater;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+class GzCompressActionTest {
+
+ @Test
+ void testRejectsCompressionLevelLowerThanDefault(@TempDir File tempDir) {
+ File source = new File(tempDir, "invalid-low.log");
+ File dest = new File(tempDir, "invalid-low.log.gz");
+
+ assertThrows(IllegalArgumentException.class, () -> new
GzCompressAction(source, dest, true, -2, 0));
+ }
+
+ @Test
+ void testRejectsCompressionLevelHigherThanBest(@TempDir File tempDir) {
+ File source = new File(tempDir, "invalid-high.log");
+ File dest = new File(tempDir, "invalid-high.log.gz");
+
+ assertThrows(IllegalArgumentException.class, () -> new
GzCompressAction(source, dest, true, 10, 0));
+ }
+
+ @Test
+ void testAcceptsDeflaterRangeBounds(@TempDir File tempDir) {
+ File source = new File(tempDir, "valid.log");
+ File dest = new File(tempDir, "valid.log.gz");
+
+ new GzCompressAction(source, dest, true, Deflater.DEFAULT_COMPRESSION,
0);
+ new GzCompressAction(source, dest, true, Deflater.BEST_COMPRESSION, 0);
+ }
+
+ /** Issue #4012 — when maxDelaySeconds > 0, compression must be deferred
by a random 0..max seconds. */
+ @Test
+ void testRandomDelayBeforeCompression(@TempDir File tempDir) throws
IOException {
+ File source = new File(tempDir, "test.log");
+ File dest = new File(tempDir, "test.log.gz");
+ try (FileWriter writer = new FileWriter(source)) {
+ writer.write("test data");
+ }
+ int maxDelay = 2; // seconds
+ GzCompressAction action = new GzCompressAction(source, dest, true, 0,
maxDelay);
+ long start = System.currentTimeMillis();
+ action.execute();
+ long elapsed = System.currentTimeMillis() - start;
+
+ // Must complete within maxDelay + small margin
+ assertTrue(
+ elapsed <= (maxDelay * 1000L) + 500,
+ "Compression should not exceed maxDelay=" + maxDelay + "s, but
took " + elapsed + "ms");
+ // Destination must be created
+ assertTrue(dest.exists(), "Compressed file must exist after
execute()");
+ // Source must be deleted (deleteSource=true)
+ assertFalse(source.exists(), "Source file must be deleted after
compression");
+ }
+
+ /**
+ * Issue #4012 — when maxDelaySeconds=0, no delay is applied (backward
compatibility).
+ * Compression must complete well under 500ms.
+ */
+ @Test
+ void testNoDelayWhenMaxDelayIsZero(@TempDir File tempDir) throws
IOException {
+ File source = new File(tempDir, "test-nodelay.log");
+ File dest = new File(tempDir, "test-nodelay.log.gz");
+ try (FileWriter writer = new FileWriter(source)) {
+ writer.write("test data no delay");
+ }
+ GzCompressAction action = new GzCompressAction(source, dest, true, 0,
0);
+ long start = System.currentTimeMillis();
+ action.execute();
+ long elapsed = System.currentTimeMillis() - start;
+
+ // No delay: must complete in well under 500ms
+ assertTrue(elapsed < 500, "Compression with maxDelay=0 should be
instant, but took " + elapsed + "ms");
+ assertTrue(dest.exists(), "Compressed file must exist after
execute()");
+ assertFalse(source.exists(), "Source file must be deleted after
compression");
+ }
+
+ /** Legacy 4-arg constructor must still work with no delay (backward
compatibility). */
+ @Test
+ void testLegacyConstructorNoDelay(@TempDir File tempDir) throws
IOException {
+ File source = new File(tempDir, "test-legacy.log");
+ File dest = new File(tempDir, "test-legacy.log.gz");
+ try (FileWriter writer = new FileWriter(source)) {
+ writer.write("legacy test data");
+ }
+ GzCompressAction action = new GzCompressAction(source, dest, true, 0);
+ long start = System.currentTimeMillis();
+ action.execute();
+ long elapsed = System.currentTimeMillis() - start;
+
+ assertTrue(elapsed < 500, "Legacy constructor should have no delay,
but took " + elapsed + "ms");
+ assertTrue(dest.exists(), "Compressed file must exist");
+ assertFalse(source.exists(), "Source file must be deleted");
+ }
+}
diff --git
a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/rolling/action/ZipCompressActionTest.java
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/rolling/action/ZipCompressActionTest.java
new file mode 100644
index 0000000000..b8bc76ce53
--- /dev/null
+++
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/rolling/action/ZipCompressActionTest.java
@@ -0,0 +1,120 @@
+/*
+ * 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 static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+class ZipCompressActionTest {
+
+ @Test
+ void testRejectsCompressionLevelBelowRange(@TempDir File tempDir) {
+ File source = new File(tempDir, "invalid-low.log");
+ File dest = new File(tempDir, "invalid-low.log.zip");
+
+ assertThrows(IllegalArgumentException.class, () -> new
ZipCompressAction(source, dest, true, -2, 0));
+ }
+
+ @Test
+ void testRejectsCompressionLevelAboveRange(@TempDir File tempDir) {
+ File source = new File(tempDir, "invalid-high.log");
+ File dest = new File(tempDir, "invalid-high.log.zip");
+
+ assertThrows(IllegalArgumentException.class, () -> new
ZipCompressAction(source, dest, true, 10, 0));
+ }
+
+ @Test
+ void testAcceptsCompressionLevelRangeBounds(@TempDir File tempDir) {
+ File source = new File(tempDir, "valid.log");
+ File dest = new File(tempDir, "valid.log.zip");
+
+ new ZipCompressAction(source, dest, true, -1, 0);
+ new ZipCompressAction(source, dest, true, 0, 0);
+ new ZipCompressAction(source, dest, true, 9, 0);
+ }
+
+ /** Issue #4012 — when maxDelaySeconds > 0, compression must be deferred
by a random 0..max seconds. */
+ @Test
+ void testRandomDelayBeforeCompression(@TempDir File tempDir) throws
IOException {
+ File source = new File(tempDir, "test.log");
+ File dest = new File(tempDir, "test.log.zip");
+ try (FileWriter writer = new FileWriter(source)) {
+ writer.write("test data");
+ }
+ int maxDelay = 2; // seconds
+ ZipCompressAction action = new ZipCompressAction(source, dest, true,
0, maxDelay);
+ long start = System.currentTimeMillis();
+ action.execute();
+ long elapsed = System.currentTimeMillis() - start;
+
+ // Must complete within maxDelay + small margin
+ assertTrue(
+ elapsed <= (maxDelay * 1000L) + 500,
+ "Compression should not exceed maxDelay=" + maxDelay + "s, but
took " + elapsed + "ms");
+ // Destination must be created
+ assertTrue(dest.exists(), "Compressed file must exist after
execute()");
+ // Source must be deleted (deleteSource=true)
+ assertFalse(source.exists(), "Source file must be deleted after
compression");
+ }
+
+ /**
+ * Issue #4012 — when maxDelaySeconds=0, no delay is applied (backward
compatibility).
+ * Compression must complete well under 500ms.
+ */
+ @Test
+ void testNoDelayWhenMaxDelayIsZero(@TempDir File tempDir) throws
IOException {
+ File source = new File(tempDir, "test-nodelay.log");
+ File dest = new File(tempDir, "test-nodelay.log.zip");
+ try (FileWriter writer = new FileWriter(source)) {
+ writer.write("test data no delay");
+ }
+ ZipCompressAction action = new ZipCompressAction(source, dest, true,
0, 0);
+ long start = System.currentTimeMillis();
+ action.execute();
+ long elapsed = System.currentTimeMillis() - start;
+
+ // No delay: must complete in well under 500ms
+ assertTrue(elapsed < 500, "Compression with maxDelay=0 should be
instant, but took " + elapsed + "ms");
+ assertTrue(dest.exists(), "Compressed file must exist after
execute()");
+ assertFalse(source.exists(), "Source file must be deleted after
compression");
+ }
+
+ /** Legacy 4-arg constructor must still work with no delay (backward
compatibility). */
+ @Test
+ void testLegacyConstructorNoDelay(@TempDir File tempDir) throws
IOException {
+ File source = new File(tempDir, "test-legacy.log");
+ File dest = new File(tempDir, "test-legacy.log.zip");
+ try (FileWriter writer = new FileWriter(source)) {
+ writer.write("legacy test data");
+ }
+ ZipCompressAction action = new ZipCompressAction(source, dest, true,
0);
+ long start = System.currentTimeMillis();
+ action.execute();
+ long elapsed = System.currentTimeMillis() - start;
+
+ assertTrue(elapsed < 500, "Legacy constructor should have no delay,
but took " + elapsed + "ms");
+ assertTrue(dest.exists(), "Compressed file must exist");
+ assertFalse(source.exists(), "Source file must be deleted");
+ }
+}
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 ea1bae7669..74fbbf8359 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
@@ -109,6 +109,9 @@ public class DefaultRolloverStrategy extends
AbstractRolloverStrategy {
@PluginBuilderAttribute(value = "tempCompressedFilePattern")
private String tempCompressedFilePattern;
+ @PluginBuilderAttribute("maxCompressionDelaySeconds")
+ private int maxCompressionDelaySeconds = 0;
+
@PluginConfiguration
private Configuration config;
@@ -156,7 +159,8 @@ public class DefaultRolloverStrategy extends
AbstractRolloverStrategy {
nonNullStrSubstitutor,
customActions,
stopCustomActionsOnError,
- tempCompressedFilePattern);
+ tempCompressedFilePattern,
+ maxCompressionDelaySeconds);
}
public String getMax() {
@@ -359,6 +363,18 @@ public class DefaultRolloverStrategy extends
AbstractRolloverStrategy {
this.config = config;
return this;
}
+
+ /**
+ * Defines maximum delay in seconds before compression.
+ *
+ * @param maxCompressionDelaySeconds maximum delay in seconds before
compression.
+ * @return This builder for chaining convenience
+ * @since 2.27.0
+ */
+ public Builder setMaxCompressionDelaySeconds(final int
maxCompressionDelaySeconds) {
+ this.maxCompressionDelaySeconds = maxCompressionDelaySeconds;
+ return this;
+ }
}
@PluginBuilderFactory
@@ -380,10 +396,14 @@ public class DefaultRolloverStrategy extends
AbstractRolloverStrategy {
* @return A DefaultRolloverStrategy.
* @deprecated Since 2.9 Usage of Builder API is preferable
*/
+
+ /**
+ * Legacy factory method for backward compatibility (no delay parameter).
+ * @deprecated Since 2.9 Usage of Builder API is preferable
+ */
@PluginFactory
@Deprecated
public static DefaultRolloverStrategy createStrategy(
- // @formatter:off
@PluginAttribute("max") final String max,
@PluginAttribute("min") final String min,
@PluginAttribute("fileIndex") final String fileIndex,
@@ -401,7 +421,6 @@ public class DefaultRolloverStrategy extends
AbstractRolloverStrategy {
.setStopCustomActionsOnError(stopCustomActionsOnError)
.setConfig(config)
.build();
- // @formatter:on
}
/**
@@ -419,6 +438,7 @@ public class DefaultRolloverStrategy extends
AbstractRolloverStrategy {
private final List<Action> customActions;
private final boolean stopCustomActionsOnError;
private final PatternProcessor tempCompressedFilePattern;
+ private final int maxCompressionDelaySeconds;
/**
* Constructs a new instance.
@@ -446,7 +466,8 @@ public class DefaultRolloverStrategy extends
AbstractRolloverStrategy {
strSubstitutor,
customActions,
stopCustomActionsOnError,
- null);
+ null,
+ 0);
}
/**
@@ -468,6 +489,40 @@ public class DefaultRolloverStrategy extends
AbstractRolloverStrategy {
final Action[] customActions,
final boolean stopCustomActionsOnError,
final String tempCompressedFilePatternString) {
+ this(
+ minIndex,
+ maxIndex,
+ useMax,
+ compressionLevel,
+ strSubstitutor,
+ customActions,
+ stopCustomActionsOnError,
+ tempCompressedFilePatternString,
+ 0);
+ }
+
+ /**
+ * Constructs a new instance.
+ *
+ * @param minIndex The minimum index.
+ * @param maxIndex The maximum index.
+ * @param customActions custom actions to perform asynchronously after
rollover
+ * @param stopCustomActionsOnError whether to stop executing asynchronous
actions if an error occurs
+ * @param tempCompressedFilePatternString File pattern of the working file
+ * used during compression, if null no
temporary file are used
+ * @param maxCompressionDelaySeconds maximum delay in seconds before
compression.
+ * @since 2.27.0
+ */
+ protected DefaultRolloverStrategy(
+ final int minIndex,
+ final int maxIndex,
+ final boolean useMax,
+ final int compressionLevel,
+ final StrSubstitutor strSubstitutor,
+ final Action[] customActions,
+ final boolean stopCustomActionsOnError,
+ final String tempCompressedFilePatternString,
+ final int maxCompressionDelaySeconds) {
super(strSubstitutor);
this.minIndex = minIndex;
this.maxIndex = maxIndex;
@@ -477,6 +532,7 @@ public class DefaultRolloverStrategy extends
AbstractRolloverStrategy {
this.customActions = customActions == null ?
Collections.<Action>emptyList() : Arrays.asList(customActions);
this.tempCompressedFilePattern =
tempCompressedFilePatternString != null ? new
PatternProcessor(tempCompressedFilePatternString) : null;
+ this.maxCompressionDelaySeconds = maxCompressionDelaySeconds;
}
public int getCompressionLevel() {
@@ -507,6 +563,16 @@ public class DefaultRolloverStrategy extends
AbstractRolloverStrategy {
return tempCompressedFilePattern;
}
+ /**
+ * Returns the maximum delay in seconds before compression.
+ *
+ * @return maximum delay in seconds before compression.
+ * @since 2.27.0
+ */
+ public int getMaxCompressionDelaySeconds() {
+ return maxCompressionDelaySeconds;
+ }
+
private int purge(final int lowIndex, final int highIndex, final
RollingFileManager manager) {
return useMax ? purgeAscending(lowIndex, highIndex, manager) :
purgeDescending(lowIndex, highIndex, manager);
}
@@ -680,11 +746,17 @@ public class DefaultRolloverStrategy extends
AbstractRolloverStrategy {
}
compressAction = new CompositeAction(
Arrays.asList(
- fileExtension.createCompressAction(renameTo,
tmpCompressedName, true, compressionLevel),
+ fileExtension.createCompressAction(
+ renameTo,
+ tmpCompressedName,
+ true,
+ compressionLevel,
+ maxCompressionDelaySeconds),
new FileRenameAction(tmpCompressedNameFile,
renameToFile, true)),
true);
} else {
- compressAction = fileExtension.createCompressAction(renameTo,
compressedName, true, compressionLevel);
+ compressAction = fileExtension.createCompressAction(
+ renameTo, compressedName, true, compressionLevel,
maxCompressionDelaySeconds);
}
}
diff --git
a/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/FileExtension.java
b/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/FileExtension.java
index e62419b685..eb453628e8 100644
---
a/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/FileExtension.java
+++
b/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/FileExtension.java
@@ -35,7 +35,22 @@ public enum FileExtension {
final String compressedName,
final boolean deleteSource,
final int compressionLevel) {
- return new ZipCompressAction(source(renameTo),
target(compressedName), deleteSource, compressionLevel);
+ return createCompressAction(renameTo, compressedName,
deleteSource, compressionLevel, 0);
+ }
+
+ @Override
+ public Action createCompressAction(
+ final String renameTo,
+ final String compressedName,
+ final boolean deleteSource,
+ final int compressionLevel,
+ final int maxCompressionDelaySeconds) {
+ return new ZipCompressAction(
+ new File(renameTo),
+ new File(compressedName),
+ deleteSource,
+ compressionLevel,
+ maxCompressionDelaySeconds);
}
},
GZ(".gz") {
@@ -45,7 +60,22 @@ public enum FileExtension {
final String compressedName,
final boolean deleteSource,
final int compressionLevel) {
- return new GzCompressAction(source(renameTo),
target(compressedName), deleteSource, compressionLevel);
+ return createCompressAction(renameTo, compressedName,
deleteSource, compressionLevel, 0);
+ }
+
+ @Override
+ public Action createCompressAction(
+ final String renameTo,
+ final String compressedName,
+ final boolean deleteSource,
+ final int compressionLevel,
+ final int maxCompressionDelaySeconds) {
+ return new GzCompressAction(
+ new File(renameTo),
+ new File(compressedName),
+ deleteSource,
+ compressionLevel,
+ maxCompressionDelaySeconds);
}
},
BZIP2(".bz2") {
@@ -55,8 +85,19 @@ public enum FileExtension {
final String compressedName,
final boolean deleteSource,
final int compressionLevel) {
+ return createCompressAction(renameTo, compressedName,
deleteSource, compressionLevel, 0);
+ }
+
+ @Override
+ public Action createCompressAction(
+ final String renameTo,
+ final String compressedName,
+ final boolean deleteSource,
+ final int compressionLevel,
+ final int maxCompressionDelaySeconds) {
// One of "gz", "bzip2", "xz", "zst", "pack200", or "deflate".
- return new CommonsCompressAction("bzip2", source(renameTo),
target(compressedName), deleteSource);
+ return new CommonsCompressAction(
+ "bzip2", source(renameTo), target(compressedName),
deleteSource, maxCompressionDelaySeconds);
}
},
DEFLATE(".deflate") {
@@ -66,8 +107,19 @@ public enum FileExtension {
final String compressedName,
final boolean deleteSource,
final int compressionLevel) {
+ return createCompressAction(renameTo, compressedName,
deleteSource, compressionLevel, 0);
+ }
+
+ @Override
+ public Action createCompressAction(
+ final String renameTo,
+ final String compressedName,
+ final boolean deleteSource,
+ final int compressionLevel,
+ final int maxCompressionDelaySeconds) {
// One of "gz", "bzip2", "xz", "zst", "pack200", or "deflate".
- return new CommonsCompressAction("deflate", source(renameTo),
target(compressedName), deleteSource);
+ return new CommonsCompressAction(
+ "deflate", source(renameTo), target(compressedName),
deleteSource, maxCompressionDelaySeconds);
}
},
PACK200(".pack200") {
@@ -77,8 +129,19 @@ public enum FileExtension {
final String compressedName,
final boolean deleteSource,
final int compressionLevel) {
- // One of "gz", "bzip2", "xz", "zst", "pack200", or "deflate".
- return new CommonsCompressAction("pack200", source(renameTo),
target(compressedName), deleteSource);
+ return createCompressAction(renameTo, compressedName,
deleteSource, compressionLevel, 0);
+ }
+
+ @Override
+ public Action createCompressAction(
+ final String renameTo,
+ final String compressedName,
+ final boolean deleteSource,
+ final int compressionLevel,
+ final int maxCompressionDelaySeconds) {
+ // One of "gz", "bzip2", "xz", "zstd", "pack200", or "deflate".
+ return new CommonsCompressAction(
+ "pack200", source(renameTo), target(compressedName),
deleteSource, maxCompressionDelaySeconds);
}
},
XZ(".xz") {
@@ -88,8 +151,19 @@ public enum FileExtension {
final String compressedName,
final boolean deleteSource,
final int compressionLevel) {
+ return createCompressAction(renameTo, compressedName,
deleteSource, compressionLevel, 0);
+ }
+
+ @Override
+ public Action createCompressAction(
+ final String renameTo,
+ final String compressedName,
+ final boolean deleteSource,
+ final int compressionLevel,
+ final int maxCompressionDelaySeconds) {
// One of "gz", "bzip2", "xz", "zstd", "pack200", or "deflate".
- return new CommonsCompressAction("xz", source(renameTo),
target(compressedName), deleteSource);
+ return new CommonsCompressAction(
+ "xz", source(renameTo), target(compressedName),
deleteSource, maxCompressionDelaySeconds);
}
},
ZSTD(".zst") {
@@ -99,8 +173,19 @@ public enum FileExtension {
final String compressedName,
final boolean deleteSource,
final int compressionLevel) {
+ return createCompressAction(renameTo, compressedName,
deleteSource, compressionLevel, 0);
+ }
+
+ @Override
+ public Action createCompressAction(
+ final String renameTo,
+ final String compressedName,
+ final boolean deleteSource,
+ final int compressionLevel,
+ final int maxCompressionDelaySeconds) {
// One of "gz", "bzip2", "xz", "zstd", "pack200", or "deflate".
- return new CommonsCompressAction("zstd", source(renameTo),
target(compressedName), deleteSource);
+ return new CommonsCompressAction(
+ "zstd", source(renameTo), target(compressedName),
deleteSource, maxCompressionDelaySeconds);
}
};
@@ -132,6 +217,13 @@ public enum FileExtension {
public abstract Action createCompressAction(
String renameTo, String compressedName, boolean deleteSource, int
compressionLevel);
+ public abstract Action createCompressAction(
+ String renameTo,
+ String compressedName,
+ boolean deleteSource,
+ int compressionLevel,
+ int maxCompressionDelaySeconds);
+
public String getExtension() {
return extension;
}
diff --git
a/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/action/AbstractAction.java
b/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/action/AbstractAction.java
index 2ca052835a..14258eec3f 100644
---
a/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/action/AbstractAction.java
+++
b/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/action/AbstractAction.java
@@ -82,6 +82,25 @@ public abstract class AbstractAction implements Action {
interrupted = true;
}
+ /**
+ * Blocks the current thread for a random delay up to {@code
maxDelaySeconds}.
+ *
+ * @param maxDelaySeconds maximum delay in seconds before returning.
+ * @since 2.27.0
+ */
+ static void blockThread(final int maxDelaySeconds) {
+ if (maxDelaySeconds > 0) {
+ int delay =
java.util.concurrent.ThreadLocalRandom.current().nextInt(maxDelaySeconds + 1);
+ if (delay > 0) {
+ try {
+ Thread.sleep(delay * 1000L);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ }
+ }
+ }
+
/**
* Tests if the action is complete.
*
diff --git
a/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/action/CommonsCompressAction.java
b/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/action/CommonsCompressAction.java
index 1688969578..c01b2cb992 100644
---
a/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/action/CommonsCompressAction.java
+++
b/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/action/CommonsCompressAction.java
@@ -54,6 +54,11 @@ public final class CommonsCompressAction extends
AbstractAction {
*/
private final boolean deleteSource;
+ /**
+ * Maximum delay in seconds before compression.
+ */
+ private final int maxDelaySeconds;
+
/**
* Creates new instance of Bzip2CompressAction.
*
@@ -65,12 +70,33 @@ public final class CommonsCompressAction extends
AbstractAction {
*/
public CommonsCompressAction(
final String name, final File source, final File destination,
final boolean deleteSource) {
+ this(name, source, destination, deleteSource, 0);
+ }
+
+ /**
+ * Creates new instance of Bzip2CompressAction.
+ *
+ * @param name the compressor name. One of "gz", "bzip2", "xz", "zst",
"pack200", or "deflate".
+ * @param source file to compress, may not be null.
+ * @param destination compressed file, may not be null.
+ * @param deleteSource if true, attempt to delete file on completion.
Failure to delete does not cause an exception
+ * to be thrown or affect return value.
+ * @param maxDelaySeconds maximum delay in seconds before compression.
+ * @since 2.27.0
+ */
+ public CommonsCompressAction(
+ final String name,
+ final File source,
+ final File destination,
+ final boolean deleteSource,
+ final int maxDelaySeconds) {
Objects.requireNonNull(source, "source");
Objects.requireNonNull(destination, "destination");
this.name = name;
this.source = source;
this.destination = destination;
this.deleteSource = deleteSource;
+ this.maxDelaySeconds = maxDelaySeconds;
}
/**
@@ -81,6 +107,7 @@ public final class CommonsCompressAction extends
AbstractAction {
*/
@Override
public boolean execute() throws IOException {
+ blockThread(maxDelaySeconds);
return execute(name, source, destination, deleteSource);
}
diff --git
a/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/action/GzCompressAction.java
b/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/action/GzCompressAction.java
index acad1f5116..5747c638cd 100644
---
a/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/action/GzCompressAction.java
+++
b/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/action/GzCompressAction.java
@@ -55,6 +55,26 @@ public final class GzCompressAction extends AbstractAction {
*/
private final int compressionLevel;
+ /**
+ * Maximum delay in seconds before compression.
+ */
+ private final int maxDelaySeconds;
+
+ private static int checkCompressionLevel(final int compressionLevel) {
+ final int minCompressionLevel = Deflater.DEFAULT_COMPRESSION;
+ final int maxCompressionLevel = Deflater.BEST_COMPRESSION;
+
+ if (compressionLevel < minCompressionLevel || compressionLevel >
maxCompressionLevel) {
+ throw new IllegalArgumentException("GZIP compression level must be
in the range ["
+ + minCompressionLevel
+ + ", "
+ + maxCompressionLevel
+ + "], got: "
+ + compressionLevel);
+ }
+ return compressionLevel;
+ }
+
/**
* Create new instance of GzCompressAction.
*
@@ -64,26 +84,46 @@ public final class GzCompressAction extends AbstractAction {
* does not cause an exception to be thrown or affect
return value.
* @param compressionLevel
* Gzip deflater compression level.
+ * @since 2.27.0
+ * @param maxDelaySeconds
+ * Maximum delay in seconds before compression.
*/
public GzCompressAction(
- final File source, final File destination, final boolean
deleteSource, final int compressionLevel) {
+ final File source,
+ final File destination,
+ final boolean deleteSource,
+ final int compressionLevel,
+ final int maxDelaySeconds) {
Objects.requireNonNull(source, "source");
Objects.requireNonNull(destination, "destination");
this.source = source;
this.destination = destination;
this.deleteSource = deleteSource;
- this.compressionLevel = compressionLevel;
+ this.compressionLevel = checkCompressionLevel(compressionLevel);
+ this.maxDelaySeconds = maxDelaySeconds;
+ }
+
+ /**
+ * Creates a new instance.
+ * @param source file to compress, may not be null.
+ * @param destination compressed file, may not be null.
+ * @param deleteSource if true, attempt to delete file on completion.
+ * @param compressionLevel Gzip deflater compression level.
+ */
+ public GzCompressAction(
+ final File source, final File destination, final boolean
deleteSource, final int compressionLevel) {
+ this(source, destination, deleteSource, compressionLevel, 0);
}
/**
* Prefer the constructor with compression level.
*
- * @deprecated Prefer {@link GzCompressAction#GzCompressAction(File, File,
boolean, int)}.
+ * @deprecated Prefer {@link GzCompressAction#GzCompressAction(File, File,
boolean, int, int)}.
*/
@Deprecated
public GzCompressAction(final File source, final File destination, final
boolean deleteSource) {
- this(source, destination, deleteSource, Deflater.DEFAULT_COMPRESSION);
+ this(source, destination, deleteSource, Deflater.DEFAULT_COMPRESSION,
0);
}
/**
@@ -94,6 +134,7 @@ public final class GzCompressAction extends AbstractAction {
*/
@Override
public boolean execute() throws IOException {
+ blockThread(maxDelaySeconds);
return execute(source, destination, deleteSource, compressionLevel);
}
@@ -129,6 +170,7 @@ public final class GzCompressAction extends AbstractAction {
public static boolean execute(
final File source, final File destination, final boolean
deleteSource, final int compressionLevel)
throws IOException {
+ checkCompressionLevel(compressionLevel);
if (source.exists()) {
try (final FileInputStream fis = new FileInputStream(source);
final OutputStream fos = new FileOutputStream(destination);
diff --git
a/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/action/ZipCompressAction.java
b/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/action/ZipCompressAction.java
index f29a7391ce..d8e7c39738 100644
---
a/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/action/ZipCompressAction.java
+++
b/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/action/ZipCompressAction.java
@@ -51,6 +51,25 @@ public final class ZipCompressAction extends AbstractAction {
*/
private final int level;
+ /**
+ * Maximum delay in seconds before compression.
+ */
+ private final int maxDelaySeconds;
+
+ /**
+ * Validates that the compression level is in the valid range [-1, 9].
+ *
+ * @param level the compression level to validate
+ * @return the level if valid
+ * @throws IllegalArgumentException if level is not in the range [-1, 9]
+ */
+ private static int checkLevel(final int level) {
+ if (level < java.util.zip.Deflater.DEFAULT_COMPRESSION || level > 9) {
+ throw new IllegalArgumentException("Compression level must be in
the range [-1, 9], got: " + level);
+ }
+ return level;
+ }
+
/**
* Creates new instance of GzCompressAction.
*
@@ -58,16 +77,37 @@ public final class ZipCompressAction extends AbstractAction
{
* @param destination compressed file, may not be null.
* @param deleteSource if true, attempt to delete file on completion.
Failure to delete does not cause an exception
* to be thrown or affect return value.
- * @param level TODO
+ * @param level the compression level
+ * @param maxDelaySeconds maximum delay in seconds before compression.
+ * @since 2.27.0
*/
- public ZipCompressAction(final File source, final File destination, final
boolean deleteSource, final int level) {
+ public ZipCompressAction(
+ final File source,
+ final File destination,
+ final boolean deleteSource,
+ final int level,
+ final int maxDelaySeconds) {
Objects.requireNonNull(source, "source");
Objects.requireNonNull(destination, "destination");
this.source = source;
this.destination = destination;
this.deleteSource = deleteSource;
- this.level = level;
+ this.level = checkLevel(level);
+ this.maxDelaySeconds = maxDelaySeconds;
+ }
+
+ /**
+ * Creates new instance.
+ *
+ * @param source file to compress, may not be null.
+ * @param destination compressed file, may not be null.
+ * @param deleteSource if true, attempt to delete file on completion.
Failure to delete does not cause an exception
+ * to be thrown or affect return value.
+ * @param level the compression level
+ */
+ public ZipCompressAction(final File source, final File destination, final
boolean deleteSource, final int level) {
+ this(source, destination, deleteSource, level, 0);
}
/**
@@ -78,6 +118,7 @@ public final class ZipCompressAction extends AbstractAction {
*/
@Override
public boolean execute() throws IOException {
+ blockThread(maxDelaySeconds);
return execute(source, destination, deleteSource, level);
}
diff --git
a/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/action/package-info.java
b/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/action/package-info.java
index 3737053030..85222541ad 100644
---
a/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/action/package-info.java
+++
b/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/action/package-info.java
@@ -18,7 +18,7 @@
* Support classes for the Rolling File Appender.
*/
@Export
-@Version("2.26.0")
+@Version("2.27.0")
package org.apache.logging.log4j.core.appender.rolling.action;
import org.osgi.annotation.bundle.Export;
diff --git
a/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/package-info.java
b/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/package-info.java
index 9f4ea07804..21a4fd72e4 100644
---
a/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/package-info.java
+++
b/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/package-info.java
@@ -18,7 +18,7 @@
* Rolling File Appender and support classes.
*/
@Export
-@Version("2.26.0")
+@Version("2.27.0")
package org.apache.logging.log4j.core.appender.rolling;
import org.osgi.annotation.bundle.Export;
diff --git a/src/changelog/.2.x.x/4012_add_max_compression_delay.xml
b/src/changelog/.2.x.x/4012_add_max_compression_delay.xml
new file mode 100644
index 0000000000..39cc883a15
--- /dev/null
+++ b/src/changelog/.2.x.x/4012_add_max_compression_delay.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<entry xmlns="https://logging.apache.org/xml/ns"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="
+ https://logging.apache.org/xml/ns
+ https://logging.apache.org/xml/ns/log4j-changelog-0.xsd"
+ type="added">
+ <issue id="4012"
link="https://github.com/apache/logging-log4j2/issues/4012"/>
+ <issue id="4071"
link="https://github.com/apache/logging-log4j2/pull/4071"/>
+ <description format="asciidoc">
+ Added support for `maxCompressionDelaySeconds` in compression actions
to proactively defer compression and reduce disk I/O pressure during rollover.
+ </description>
+</entry>
+
diff --git
a/src/site/antora/modules/ROOT/pages/manual/appenders/rolling-file.adoc
b/src/site/antora/modules/ROOT/pages/manual/appenders/rolling-file.adoc
index 1982182fd2..8dd9c91c0c 100644
--- a/src/site/antora/modules/ROOT/pages/manual/appenders/rolling-file.adoc
+++ b/src/site/antora/modules/ROOT/pages/manual/appenders/rolling-file.adoc
@@ -713,6 +713,15 @@ Minimum value for the <<conversion-pattern-integer,`%i`>>
conversion pattern.
Maximum value for the <<conversion-pattern-integer,`%i`>> conversion pattern.
This attribute is **ignored** if
<<DefaultRolloverStrategy-attr-fileIndex,`fileIndex`>> is set to `nomax`.
+
+|
[[DefaultRolloverStrategy-attr-maxCompressionDelaySeconds]]maxCompressionDelaySeconds
+| `int`
+| `0`
+|
+Maximum random delay, in seconds, before compressing archived log files.
+
+Use this attribute to spread compression workload when many applications roll
over at the same time.
+A value of `0` disables the delay and starts compression immediately.
|===
xref:plugin-reference.adoc#org-apache-logging-log4j_log4j-core_org-apache-logging-log4j-core-appender-rolling-DefaultRolloverStrategy[{plugin-reference-marker}
Plugin reference for `DefaultRolloverStrategy`]