Skip to content

Commit 8c1e625

Browse files
Add support for max compression delay in FileExtension actions
1 parent a3b7124 commit 8c1e625

File tree

4 files changed

+288
-12
lines changed

4 files changed

+288
-12
lines changed
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to you under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package org.apache.logging.log4j.core.appender.rolling;
18+
19+
import static org.junit.jupiter.api.Assertions.assertFalse;
20+
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
21+
import static org.junit.jupiter.api.Assertions.assertTrue;
22+
23+
import java.io.File;
24+
import java.io.FileWriter;
25+
import java.io.IOException;
26+
import org.apache.logging.log4j.core.appender.rolling.action.Action;
27+
import org.apache.logging.log4j.core.appender.rolling.action.GzCompressAction;
28+
import org.apache.logging.log4j.core.appender.rolling.action.ZipCompressAction;
29+
import org.junit.jupiter.api.Test;
30+
import org.junit.jupiter.api.io.TempDir;
31+
32+
/**
33+
* Issue #4012 — verifies that FileExtension.GZ and FileExtension.ZIP correctly pass
34+
* maxCompressionDelaySeconds through to the compression action.
35+
*
36+
* This was the root cause of the bug: the 5-argument createCompressAction() in GZ and ZIP
37+
* fell back to the 4-argument version, silently dropping the delay value.
38+
*/
39+
class FileExtensionCompressDelayTest {
40+
41+
// ── GZ ────────────────────────────────────────────────────────────────
42+
43+
/**
44+
* FileExtension.GZ.createCompressAction(5-args) must produce a GzCompressAction
45+
* that applies the random delay — not fall back to 0.
46+
*/
47+
@Test
48+
void testGzCreateCompressActionWithDelay(@TempDir File tempDir) throws Exception {
49+
File source = new File(tempDir, "app.log");
50+
File dest = new File(tempDir, "app.log.gz");
51+
writeContent(source, "gz test content");
52+
53+
int maxDelay = 2;
54+
Action action = FileExtension.GZ.createCompressAction(source.getPath(), dest.getPath(), true, -1, maxDelay);
55+
56+
// Must return a GzCompressAction (not some other type)
57+
assertInstanceOf(GzCompressAction.class, action, "Expected GzCompressAction");
58+
59+
long start = System.currentTimeMillis();
60+
action.execute();
61+
long elapsed = System.currentTimeMillis() - start;
62+
63+
// Must finish within maxDelay + margin (delay IS applied via FileExtension)
64+
assertTrue(
65+
elapsed <= (maxDelay * 1000L) + 500,
66+
"GZ compress via FileExtension exceeded maxDelay=" + maxDelay + "s: " + elapsed + "ms");
67+
assertTrue(dest.exists(), "Compressed .gz file must exist");
68+
assertFalse(source.exists(), "Source must be deleted after compression");
69+
}
70+
71+
/**
72+
* FileExtension.GZ.createCompressAction(5-args, delay=0) must behave identically
73+
* to the 4-arg version — instant compression, no delay.
74+
*/
75+
@Test
76+
void testGzCreateCompressActionNoDelay(@TempDir File tempDir) throws Exception {
77+
File source = new File(tempDir, "app-nodelay.log");
78+
File dest = new File(tempDir, "app-nodelay.log.gz");
79+
writeContent(source, "gz no-delay content");
80+
81+
Action action = FileExtension.GZ.createCompressAction(source.getPath(), dest.getPath(), true, -1, 0);
82+
83+
assertInstanceOf(GzCompressAction.class, action);
84+
85+
long start = System.currentTimeMillis();
86+
action.execute();
87+
long elapsed = System.currentTimeMillis() - start;
88+
89+
assertTrue(elapsed < 500, "GZ with delay=0 should be instant, took " + elapsed + "ms");
90+
assertTrue(dest.exists(), "Compressed .gz file must exist");
91+
assertFalse(source.exists(), "Source must be deleted");
92+
}
93+
94+
// ── ZIP ───────────────────────────────────────────────────────────────
95+
96+
/**
97+
* FileExtension.ZIP.createCompressAction(5-args) must produce a ZipCompressAction
98+
* that applies the random delay — not fall back to 0.
99+
*/
100+
@Test
101+
void testZipCreateCompressActionWithDelay(@TempDir File tempDir) throws Exception {
102+
File source = new File(tempDir, "app.log");
103+
File dest = new File(tempDir, "app.log.zip");
104+
writeContent(source, "zip test content");
105+
106+
int maxDelay = 2;
107+
Action action = FileExtension.ZIP.createCompressAction(source.getPath(), dest.getPath(), true, 0, maxDelay);
108+
109+
assertInstanceOf(ZipCompressAction.class, action, "Expected ZipCompressAction");
110+
111+
long start = System.currentTimeMillis();
112+
action.execute();
113+
long elapsed = System.currentTimeMillis() - start;
114+
115+
assertTrue(
116+
elapsed <= (maxDelay * 1000L) + 500,
117+
"ZIP compress via FileExtension exceeded maxDelay=" + maxDelay + "s: " + elapsed + "ms");
118+
assertTrue(dest.exists(), "Compressed .zip file must exist");
119+
assertFalse(source.exists(), "Source must be deleted after compression");
120+
}
121+
122+
/**
123+
* FileExtension.ZIP.createCompressAction(5-args, delay=0) must behave identically
124+
* to the 4-arg version — instant compression, no delay.
125+
*/
126+
@Test
127+
void testZipCreateCompressActionNoDelay(@TempDir File tempDir) throws Exception {
128+
File source = new File(tempDir, "app-nodelay.log");
129+
File dest = new File(tempDir, "app-nodelay.log.zip");
130+
writeContent(source, "zip no-delay content");
131+
132+
Action action = FileExtension.ZIP.createCompressAction(source.getPath(), dest.getPath(), true, 0, 0);
133+
134+
assertInstanceOf(ZipCompressAction.class, action);
135+
136+
long start = System.currentTimeMillis();
137+
action.execute();
138+
long elapsed = System.currentTimeMillis() - start;
139+
140+
assertTrue(elapsed < 500, "ZIP with delay=0 should be instant, took " + elapsed + "ms");
141+
assertTrue(dest.exists(), "Compressed .zip file must exist");
142+
assertFalse(source.exists(), "Source must be deleted");
143+
}
144+
145+
// ── helpers ───────────────────────────────────────────────────────────
146+
147+
private static void writeContent(final File file, final String content) throws IOException {
148+
try (FileWriter writer = new FileWriter(file)) {
149+
writer.write(content);
150+
}
151+
}
152+
}

log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/rolling/action/GzCompressActionTest.java

Lines changed: 52 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
*/
1717
package org.apache.logging.log4j.core.appender.rolling.action;
1818

19+
import static org.junit.jupiter.api.Assertions.assertFalse;
1920
import static org.junit.jupiter.api.Assertions.assertTrue;
2021

2122
import java.io.File;
@@ -25,6 +26,8 @@
2526
import org.junit.jupiter.api.io.TempDir;
2627

2728
class GzCompressActionTest {
29+
30+
/** Issue #4012 — when maxDelaySeconds > 0, compression must be deferred by a random 0..max seconds. */
2831
@Test
2932
void testRandomDelayBeforeCompression(@TempDir File tempDir) throws IOException {
3033
File source = new File(tempDir, "test.log");
@@ -37,10 +40,54 @@ void testRandomDelayBeforeCompression(@TempDir File tempDir) throws IOException
3740
long start = System.currentTimeMillis();
3841
action.execute();
3942
long elapsed = System.currentTimeMillis() - start;
40-
// Should be at least 0 and at most maxDelay * 1000 + 500ms margin
41-
assertTrue(elapsed >= 0, "Compression should not be negative time");
42-
assertTrue(elapsed <= (maxDelay * 1000) + 500, "Compression should not exceed maxDelay by much");
43-
// Should be at least some delay (not always 0)
44-
// This is probabilistic, so we allow for rare 0-delay, but in practice should be >0
43+
44+
// Must complete within maxDelay + small margin
45+
assertTrue(
46+
elapsed <= (maxDelay * 1000L) + 500,
47+
"Compression should not exceed maxDelay=" + maxDelay + "s, but took " + elapsed + "ms");
48+
// Destination must be created
49+
assertTrue(dest.exists(), "Compressed file must exist after execute()");
50+
// Source must be deleted (deleteSource=true)
51+
assertFalse(source.exists(), "Source file must be deleted after compression");
52+
}
53+
54+
/**
55+
* Issue #4012 — when maxDelaySeconds=0, no delay is applied (backward compatibility).
56+
* Compression must complete well under 500ms.
57+
*/
58+
@Test
59+
void testNoDelayWhenMaxDelayIsZero(@TempDir File tempDir) throws IOException {
60+
File source = new File(tempDir, "test-nodelay.log");
61+
File dest = new File(tempDir, "test-nodelay.log.gz");
62+
try (FileWriter writer = new FileWriter(source)) {
63+
writer.write("test data no delay");
64+
}
65+
GzCompressAction action = new GzCompressAction(source, dest, true, 0, 0);
66+
long start = System.currentTimeMillis();
67+
action.execute();
68+
long elapsed = System.currentTimeMillis() - start;
69+
70+
// No delay: must complete in well under 500ms
71+
assertTrue(elapsed < 500, "Compression with maxDelay=0 should be instant, but took " + elapsed + "ms");
72+
assertTrue(dest.exists(), "Compressed file must exist after execute()");
73+
assertFalse(source.exists(), "Source file must be deleted after compression");
74+
}
75+
76+
/** Legacy 4-arg constructor must still work with no delay (backward compatibility). */
77+
@Test
78+
void testLegacyConstructorNoDelay(@TempDir File tempDir) throws IOException {
79+
File source = new File(tempDir, "test-legacy.log");
80+
File dest = new File(tempDir, "test-legacy.log.gz");
81+
try (FileWriter writer = new FileWriter(source)) {
82+
writer.write("legacy test data");
83+
}
84+
GzCompressAction action = new GzCompressAction(source, dest, true, 0);
85+
long start = System.currentTimeMillis();
86+
action.execute();
87+
long elapsed = System.currentTimeMillis() - start;
88+
89+
assertTrue(elapsed < 500, "Legacy constructor should have no delay, but took " + elapsed + "ms");
90+
assertTrue(dest.exists(), "Compressed file must exist");
91+
assertFalse(source.exists(), "Source file must be deleted");
4592
}
4693
}

log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/rolling/action/ZipCompressActionTest.java

Lines changed: 52 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
*/
1717
package org.apache.logging.log4j.core.appender.rolling.action;
1818

19+
import static org.junit.jupiter.api.Assertions.assertFalse;
1920
import static org.junit.jupiter.api.Assertions.assertTrue;
2021

2122
import java.io.File;
@@ -25,6 +26,8 @@
2526
import org.junit.jupiter.api.io.TempDir;
2627

2728
class ZipCompressActionTest {
29+
30+
/** Issue #4012 — when maxDelaySeconds > 0, compression must be deferred by a random 0..max seconds. */
2831
@Test
2932
void testRandomDelayBeforeCompression(@TempDir File tempDir) throws IOException {
3033
File source = new File(tempDir, "test.log");
@@ -37,10 +40,54 @@ void testRandomDelayBeforeCompression(@TempDir File tempDir) throws IOException
3740
long start = System.currentTimeMillis();
3841
action.execute();
3942
long elapsed = System.currentTimeMillis() - start;
40-
// Should be at least 0 and at most maxDelay * 1000 + 500ms margin
41-
assertTrue(elapsed >= 0, "Compression should not be negative time");
42-
assertTrue(elapsed <= (maxDelay * 1000) + 500, "Compression should not exceed maxDelay by much");
43-
// Should be at least some delay (not always 0)
44-
// This is probabilistic, so we allow for rare 0-delay, but in practice should be >0
43+
44+
// Must complete within maxDelay + small margin
45+
assertTrue(
46+
elapsed <= (maxDelay * 1000L) + 500,
47+
"Compression should not exceed maxDelay=" + maxDelay + "s, but took " + elapsed + "ms");
48+
// Destination must be created
49+
assertTrue(dest.exists(), "Compressed file must exist after execute()");
50+
// Source must be deleted (deleteSource=true)
51+
assertFalse(source.exists(), "Source file must be deleted after compression");
52+
}
53+
54+
/**
55+
* Issue #4012 — when maxDelaySeconds=0, no delay is applied (backward compatibility).
56+
* Compression must complete well under 500ms.
57+
*/
58+
@Test
59+
void testNoDelayWhenMaxDelayIsZero(@TempDir File tempDir) throws IOException {
60+
File source = new File(tempDir, "test-nodelay.log");
61+
File dest = new File(tempDir, "test-nodelay.log.zip");
62+
try (FileWriter writer = new FileWriter(source)) {
63+
writer.write("test data no delay");
64+
}
65+
ZipCompressAction action = new ZipCompressAction(source, dest, true, 0, 0);
66+
long start = System.currentTimeMillis();
67+
action.execute();
68+
long elapsed = System.currentTimeMillis() - start;
69+
70+
// No delay: must complete in well under 500ms
71+
assertTrue(elapsed < 500, "Compression with maxDelay=0 should be instant, but took " + elapsed + "ms");
72+
assertTrue(dest.exists(), "Compressed file must exist after execute()");
73+
assertFalse(source.exists(), "Source file must be deleted after compression");
74+
}
75+
76+
/** Legacy 4-arg constructor must still work with no delay (backward compatibility). */
77+
@Test
78+
void testLegacyConstructorNoDelay(@TempDir File tempDir) throws IOException {
79+
File source = new File(tempDir, "test-legacy.log");
80+
File dest = new File(tempDir, "test-legacy.log.zip");
81+
try (FileWriter writer = new FileWriter(source)) {
82+
writer.write("legacy test data");
83+
}
84+
ZipCompressAction action = new ZipCompressAction(source, dest, true, 0);
85+
long start = System.currentTimeMillis();
86+
action.execute();
87+
long elapsed = System.currentTimeMillis() - start;
88+
89+
assertTrue(elapsed < 500, "Legacy constructor should have no delay, but took " + elapsed + "ms");
90+
assertTrue(dest.exists(), "Compressed file must exist");
91+
assertFalse(source.exists(), "Source file must be deleted");
4592
}
4693
}

log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/FileExtension.java

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,25 @@ public Action createCompressAction(
3535
final String compressedName,
3636
final boolean deleteSource,
3737
final int compressionLevel) {
38-
// Legacy method: pass maxCompressionDelaySeconds = 0
3938
return new ZipCompressAction(
4039
new File(renameTo), new File(compressedName), deleteSource, compressionLevel, 0);
4140
}
41+
42+
@Override
43+
public Action createCompressAction(
44+
final String renameTo,
45+
final String compressedName,
46+
final boolean deleteSource,
47+
final int compressionLevel,
48+
final int maxCompressionDelaySeconds) {
49+
// Issue #4012: pass maxCompressionDelaySeconds so random delay is applied before compression
50+
return new ZipCompressAction(
51+
new File(renameTo),
52+
new File(compressedName),
53+
deleteSource,
54+
compressionLevel,
55+
maxCompressionDelaySeconds);
56+
}
4257
},
4358
GZ(".gz") {
4459
@Override
@@ -47,10 +62,25 @@ public Action createCompressAction(
4762
final String compressedName,
4863
final boolean deleteSource,
4964
final int compressionLevel) {
50-
// Legacy method: pass maxCompressionDelaySeconds = 0
5165
return new GzCompressAction(
5266
new File(renameTo), new File(compressedName), deleteSource, compressionLevel, 0);
5367
}
68+
69+
@Override
70+
public Action createCompressAction(
71+
final String renameTo,
72+
final String compressedName,
73+
final boolean deleteSource,
74+
final int compressionLevel,
75+
final int maxCompressionDelaySeconds) {
76+
// Issue #4012: pass maxCompressionDelaySeconds so random delay is applied before compression
77+
return new GzCompressAction(
78+
new File(renameTo),
79+
new File(compressedName),
80+
deleteSource,
81+
compressionLevel,
82+
maxCompressionDelaySeconds);
83+
}
5484
},
5585
BZIP2(".bz2") {
5686
@Override

0 commit comments

Comments
 (0)