Skip to content

Commit 0a97dde

Browse files
committed
CXF-7396: CachedOutputStream doesn't delete temp files (#2048)
* CXF-7396: CachedOutputStream doesn't delete temp files * Refactor the cleaner implementation and add guardrails for cleaner delay (cherry picked from commit 03a85a5)
1 parent 2cf863e commit 0a97dde

File tree

9 files changed

+561
-4
lines changed

9 files changed

+561
-4
lines changed

core/pom.xml

+5
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,11 @@
180180
<artifactId>saaj-impl</artifactId>
181181
<scope>test</scope>
182182
</dependency>
183+
<dependency>
184+
<groupId>org.awaitility</groupId>
185+
<artifactId>awaitility</artifactId>
186+
<scope>test</scope>
187+
</dependency>
183188
</dependencies>
184189
<build>
185190
<plugins>

core/src/main/java/org/apache/cxf/io/CachedConstants.java

+8
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,14 @@ public final class CachedConstants {
7171
public static final String CIPHER_TRANSFORMATION_BUS_PROP =
7272
"bus.io.CachedOutputStream.CipherTransformation";
7373

74+
/**
75+
* The delay (in ms) for cleaning up unclosed {@code CachedOutputStream} instances. 30 minutes
76+
* is specified by default, the minimum value is 2 seconds. If the value of the delay is set to
77+
* 0 (or is negative), the cleaner will be deactivated.
78+
*/
79+
public static final String CLEANER_DELAY_BUS_PROP =
80+
"bus.io.CachedOutputStreamCleaner.Delay";
81+
7482
private CachedConstants() {
7583
// complete
7684
}

core/src/main/java/org/apache/cxf/io/CachedOutputStream.java

+21-1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import java.io.BufferedOutputStream;
2323
import java.io.ByteArrayInputStream;
2424
import java.io.ByteArrayOutputStream;
25+
import java.io.Closeable;
2526
import java.io.File;
2627
import java.io.FileInputStream;
2728
import java.io.FileNotFoundException;
@@ -93,6 +94,7 @@ public class CachedOutputStream extends OutputStream {
9394
private List<CachedOutputStreamCallback> callbacks;
9495

9596
private List<Object> streamList = new ArrayList<>();
97+
private CachedOutputStreamCleaner cachedOutputStreamCleaner;
9698

9799
public CachedOutputStream() {
98100
this(defaultThreshold);
@@ -127,6 +129,8 @@ private void readBusProperties() {
127129
outputDir = f;
128130
}
129131
}
132+
133+
cachedOutputStreamCleaner = b.getExtension(CachedOutputStreamCleaner.class);
130134
}
131135
}
132136

@@ -279,6 +283,9 @@ public void resetOut(OutputStream out, boolean copyOldContent) throws IOExceptio
279283
}
280284
} finally {
281285
streamList.remove(currentStream);
286+
if (cachedOutputStreamCleaner != null) {
287+
cachedOutputStreamCleaner.unregister(currentStream);
288+
}
282289
deleteTempFile();
283290
inmem = true;
284291
}
@@ -481,6 +488,9 @@ private void createFileOutputStream() throws IOException {
481488
bout.writeTo(currentStream);
482489
inmem = false;
483490
streamList.add(currentStream);
491+
if (cachedOutputStreamCleaner != null) {
492+
cachedOutputStreamCleaner.register(this);
493+
}
484494
} catch (Exception ex) {
485495
//Could be IOException or SecurityException or other issues.
486496
//Don't care what, just keep it in memory.
@@ -512,6 +522,10 @@ public InputStream getInputStream() throws IOException {
512522
try {
513523
InputStream fileInputStream = new TransferableFileInputStream(tempFile);
514524
streamList.add(fileInputStream);
525+
if (cachedOutputStreamCleaner != null) {
526+
cachedOutputStreamCleaner.register(fileInputStream);
527+
}
528+
515529
if (cipherTransformation != null) {
516530
fileInputStream = new CipherInputStream(fileInputStream, ciphers.getDecryptor()) {
517531
boolean closed;
@@ -537,7 +551,7 @@ private synchronized void deleteTempFile() {
537551
FileUtils.delete(file);
538552
}
539553
}
540-
private boolean maybeDeleteTempFile(Object stream) {
554+
private boolean maybeDeleteTempFile(Closeable stream) {
541555
boolean postClosedInvoked = false;
542556
streamList.remove(stream);
543557
if (!inmem && tempFile != null && streamList.isEmpty() && allowDeleteOfFile) {
@@ -549,6 +563,9 @@ private boolean maybeDeleteTempFile(Object stream) {
549563
//ignore
550564
}
551565
postClosedInvoked = true;
566+
if (cachedOutputStreamCleaner != null) {
567+
cachedOutputStreamCleaner.unregister(this);
568+
}
552569
}
553570
deleteTempFile();
554571
currentStream = new LoadingByteArrayOutputStream(1024);
@@ -665,6 +682,9 @@ public void close() throws IOException {
665682
if (!closed) {
666683
super.close();
667684
maybeDeleteTempFile(this);
685+
if (cachedOutputStreamCleaner != null) {
686+
cachedOutputStreamCleaner.unregister(this);
687+
}
668688
}
669689
closed = true;
670690
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/**
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package org.apache.cxf.io;
21+
22+
import java.io.Closeable;
23+
24+
/**
25+
* The {@link Bus} extension to clean up unclosed {@link CachedOutputStream} instances (and alike) backed by
26+
* temporary files (leading to disk fill, see https://issues.apache.org/jira/browse/CXF-7396.
27+
*/
28+
public interface CachedOutputStreamCleaner {
29+
/**
30+
* Run the clean up
31+
*/
32+
void clean();
33+
34+
/**
35+
* Register the stream instance for the clean up
36+
*/
37+
void unregister(Closeable closeable);
38+
39+
/**
40+
* Unregister the stream instance from the clean up (closed properly)
41+
*/
42+
void register(Closeable closeable);
43+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
/**
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package org.apache.cxf.io;
21+
22+
import java.io.Closeable;
23+
import java.io.IOException;
24+
import java.util.ArrayList;
25+
import java.util.Collection;
26+
import java.util.Iterator;
27+
import java.util.Objects;
28+
import java.util.Timer;
29+
import java.util.TimerTask;
30+
import java.util.concurrent.DelayQueue;
31+
import java.util.concurrent.Delayed;
32+
import java.util.concurrent.TimeUnit;
33+
import java.util.logging.Logger;
34+
35+
import jakarta.annotation.Resource;
36+
import org.apache.cxf.Bus;
37+
import org.apache.cxf.buslifecycle.BusLifeCycleListener;
38+
import org.apache.cxf.buslifecycle.BusLifeCycleManager;
39+
import org.apache.cxf.common.logging.LogUtils;
40+
41+
public final class DelayedCachedOutputStreamCleaner implements CachedOutputStreamCleaner, BusLifeCycleListener {
42+
private static final Logger LOG = LogUtils.getL7dLogger(DelayedCachedOutputStreamCleaner.class);
43+
private static final long MIN_DELAY = 2000; /* 2 seconds */
44+
private static final DelayedCleaner NOOP_CLEANER = new DelayedCleaner() {
45+
// NOOP
46+
};
47+
48+
private DelayedCleaner cleaner = NOOP_CLEANER;
49+
50+
private interface DelayedCleaner extends CachedOutputStreamCleaner, Closeable {
51+
@Override
52+
default void register(Closeable closeable) {
53+
}
54+
55+
@Override
56+
default void unregister(Closeable closeable) {
57+
}
58+
59+
@Override
60+
default void close() {
61+
}
62+
63+
@Override
64+
default void clean() {
65+
}
66+
67+
default void forceClean() {
68+
}
69+
}
70+
71+
private static final class DelayedCleanerImpl implements DelayedCleaner {
72+
private final long delay; /* default is 30 minutes, in milliseconds */
73+
private final DelayQueue<DelayedCloseable> queue = new DelayQueue<>();
74+
private final Timer timer;
75+
76+
DelayedCleanerImpl(final long delay) {
77+
this.delay = delay;
78+
this.timer = new Timer("DelayedCachedOutputStreamCleaner", true);
79+
this.timer.scheduleAtFixedRate(new TimerTask() {
80+
@Override
81+
public void run() {
82+
clean();
83+
}
84+
}, 0, Math.max(MIN_DELAY, delay >> 1));
85+
}
86+
87+
@Override
88+
public void register(Closeable closeable) {
89+
queue.put(new DelayedCloseable(closeable, delay));
90+
}
91+
92+
@Override
93+
public void unregister(Closeable closeable) {
94+
queue.remove(new DelayedCloseable(closeable, delay));
95+
}
96+
97+
@Override
98+
public void clean() {
99+
final Collection<DelayedCloseable> closeables = new ArrayList<>();
100+
queue.drainTo(closeables);
101+
clean(closeables);
102+
}
103+
104+
@Override
105+
public void forceClean() {
106+
clean(queue);
107+
}
108+
109+
@Override
110+
public void close() {
111+
timer.cancel();
112+
queue.clear();
113+
}
114+
115+
private void clean(Collection<DelayedCloseable> closeables) {
116+
final Iterator<DelayedCloseable> iterator = closeables.iterator();
117+
while (iterator.hasNext()) {
118+
final DelayedCloseable next = iterator.next();
119+
try {
120+
iterator.remove();
121+
LOG.warning("Unclosed (leaked?) stream detected: " + next.closeable);
122+
next.closeable.close();
123+
} catch (final IOException | RuntimeException ex) {
124+
LOG.warning("Unable to close (leaked?) stream: " + ex.getMessage());
125+
}
126+
}
127+
}
128+
}
129+
130+
private static final class DelayedCloseable implements Delayed {
131+
private final Closeable closeable;
132+
private final long expireAt;
133+
134+
DelayedCloseable(final Closeable closeable, final long delay) {
135+
this.closeable = closeable;
136+
this.expireAt = System.nanoTime() + delay;
137+
}
138+
139+
@Override
140+
public int compareTo(Delayed o) {
141+
return Long.compare(expireAt, ((DelayedCloseable) o).expireAt);
142+
}
143+
144+
@Override
145+
public long getDelay(TimeUnit unit) {
146+
return unit.convert(expireAt - System.nanoTime(), TimeUnit.NANOSECONDS);
147+
}
148+
149+
@Override
150+
public int hashCode() {
151+
return Objects.hash(closeable);
152+
}
153+
154+
@Override
155+
public boolean equals(Object obj) {
156+
if (this == obj) {
157+
return true;
158+
}
159+
160+
if (obj == null) {
161+
return false;
162+
}
163+
164+
if (getClass() != obj.getClass()) {
165+
return false;
166+
}
167+
168+
final DelayedCloseable other = (DelayedCloseable) obj;
169+
return Objects.equals(closeable, other.closeable);
170+
}
171+
}
172+
173+
@Resource
174+
public void setBus(Bus bus) {
175+
Number delayValue = null;
176+
BusLifeCycleManager busLifeCycleManager = null;
177+
178+
if (bus != null) {
179+
delayValue = (Number) bus.getProperty(CachedConstants.CLEANER_DELAY_BUS_PROP);
180+
busLifeCycleManager = bus.getExtension(BusLifeCycleManager.class);
181+
}
182+
183+
if (cleaner != null) {
184+
cleaner.close();
185+
}
186+
187+
if (delayValue == null) {
188+
// Default delay is set to 30 mins
189+
cleaner = new DelayedCleanerImpl(TimeUnit.MILLISECONDS.convert(30, TimeUnit.MINUTES));
190+
} else {
191+
final long value = delayValue.longValue();
192+
if (value > 0 && value >= MIN_DELAY) {
193+
cleaner = new DelayedCleanerImpl(value); /* already in milliseconds */
194+
} else {
195+
cleaner = NOOP_CLEANER;
196+
if (value != 0) {
197+
throw new IllegalArgumentException("The value of " + CachedConstants.CLEANER_DELAY_BUS_PROP
198+
+ " property is invalid: " + value + " (should be >= " + MIN_DELAY + ", 0 to deactivate)");
199+
}
200+
}
201+
}
202+
203+
if (busLifeCycleManager != null) {
204+
busLifeCycleManager.registerLifeCycleListener(this);
205+
}
206+
}
207+
208+
@Override
209+
public void register(Closeable closeable) {
210+
cleaner.register(closeable);
211+
}
212+
213+
@Override
214+
public void unregister(Closeable closeable) {
215+
cleaner.unregister(closeable);
216+
}
217+
218+
@Override
219+
public void clean() {
220+
cleaner.clean();
221+
}
222+
223+
@Override
224+
public void initComplete() {
225+
}
226+
227+
@Override
228+
public void postShutdown() {
229+
}
230+
231+
@Override
232+
public void preShutdown() {
233+
cleaner.close();
234+
}
235+
236+
public void forceClean() {
237+
cleaner.forceClean();
238+
}
239+
}

0 commit comments

Comments
 (0)