Skip to content

Commit 80ca5ba

Browse files
committed
CXF-7396: CachedOutputStream doesn't delete temp files
1 parent e094bb4 commit 80ca5ba

File tree

9 files changed

+477
-3
lines changed

9 files changed

+477
-3
lines changed

core/pom.xml

+5
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,11 @@
175175
<artifactId>saaj-impl</artifactId>
176176
<scope>test</scope>
177177
</dependency>
178+
<dependency>
179+
<groupId>org.awaitility</groupId>
180+
<artifactId>awaitility</artifactId>
181+
<scope>test</scope>
182+
</dependency>
178183
</dependencies>
179184
<build>
180185
<plugins>

core/src/main/java/org/apache/cxf/bus/extension/ExtensionManagerBus.java

+7
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@
4040
import org.apache.cxf.configuration.NullConfigurer;
4141
import org.apache.cxf.feature.Feature;
4242
import org.apache.cxf.interceptor.AbstractBasicInterceptorProvider;
43+
import org.apache.cxf.io.CachedOutputStreamCleaner;
44+
import org.apache.cxf.io.DelayedCachedOutputStreamCleaner;
4345
import org.apache.cxf.resource.DefaultResourceManager;
4446
import org.apache.cxf.resource.ObjectTypeResolver;
4547
import org.apache.cxf.resource.PropertiesResolver;
@@ -141,6 +143,11 @@ public InputStream getAsStream(String name) {
141143
if (null == this.getExtension(BindingFactoryManager.class)) {
142144
new BindingFactoryManagerImpl(this);
143145
}
146+
147+
if (null == this.getExtension(CachedOutputStreamCleaner.class)) {
148+
this.extensions.put(CachedOutputStreamCleaner.class, DelayedCachedOutputStreamCleaner.create(this));
149+
}
150+
144151
extensionManager.load(new String[] {ExtensionManagerImpl.BUS_EXTENSION_RESOURCE});
145152
extensionManager.activateAllByType(ResourceResolver.class);
146153

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. If the value of the delay is set to 0 (or is negative), the cleaner
77+
* will be disabled.
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,191 @@
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 org.apache.cxf.Bus;
36+
import org.apache.cxf.buslifecycle.BusLifeCycleListener;
37+
import org.apache.cxf.buslifecycle.BusLifeCycleManager;
38+
import org.apache.cxf.common.logging.LogUtils;
39+
40+
public final class DelayedCachedOutputStreamCleaner implements CachedOutputStreamCleaner, BusLifeCycleListener {
41+
private static final Logger LOG = LogUtils.getL7dLogger(DelayedCachedOutputStreamCleaner.class);
42+
43+
private final long delay; /* default is 30 minutes */
44+
private final DelayQueue<DelayedCloseable> queue = new DelayQueue<>();
45+
private final Timer timer;
46+
47+
private static final class DelayedCloseable implements Delayed {
48+
private final Closeable closeable;
49+
private final long expiredAt;
50+
51+
DelayedCloseable(final Closeable closeable, final long delay) {
52+
this.closeable = closeable;
53+
this.expiredAt = System.nanoTime() + delay;
54+
}
55+
56+
@Override
57+
public int compareTo(Delayed o) {
58+
return Long.compare(getDelay(TimeUnit.NANOSECONDS), o.getDelay(TimeUnit.NANOSECONDS));
59+
}
60+
61+
@Override
62+
public long getDelay(TimeUnit unit) {
63+
return unit.convert(expiredAt - System.nanoTime(), TimeUnit.NANOSECONDS);
64+
}
65+
66+
@Override
67+
public int hashCode() {
68+
return Objects.hash(closeable);
69+
}
70+
71+
@Override
72+
public boolean equals(Object obj) {
73+
if (this == obj) {
74+
return true;
75+
}
76+
77+
if (obj == null) {
78+
return false;
79+
}
80+
81+
if (getClass() != obj.getClass()) {
82+
return false;
83+
}
84+
85+
final DelayedCloseable other = (DelayedCloseable) obj;
86+
return Objects.equals(closeable, other.closeable);
87+
}
88+
}
89+
90+
protected DelayedCachedOutputStreamCleaner(long delay, TimeUnit unit) {
91+
this.delay = TimeUnit.NANOSECONDS.convert(delay, unit);
92+
this.timer = new Timer("DelayedCachedOutputStreamCleaner", true);
93+
this.timer.scheduleAtFixedRate(new TimerTask() {
94+
@Override
95+
public void run() {
96+
clean();
97+
}
98+
}, 0, TimeUnit.MILLISECONDS.convert(Math.max(1, delay >> 1), unit));
99+
}
100+
101+
@Override
102+
public void register(Closeable closeable) {
103+
queue.put(new DelayedCloseable(closeable, delay));
104+
}
105+
106+
@Override
107+
public void unregister(Closeable closeable) {
108+
queue.remove(new DelayedCloseable(closeable, delay));
109+
}
110+
111+
@Override
112+
public void clean() {
113+
final Collection<DelayedCloseable> closeables = new ArrayList<>();
114+
queue.drainTo(closeables);
115+
clean(closeables);
116+
}
117+
118+
@Override
119+
public void initComplete() {
120+
}
121+
122+
@Override
123+
public void postShutdown() {
124+
}
125+
126+
@Override
127+
public void preShutdown() {
128+
timer.cancel();
129+
}
130+
131+
public void forceClean() {
132+
clean(queue);
133+
}
134+
135+
private void clean(Collection<DelayedCloseable> closeables) {
136+
final Iterator<DelayedCloseable> iterator = closeables.iterator();
137+
while (iterator.hasNext()) {
138+
final DelayedCloseable next = iterator.next();
139+
try {
140+
iterator.remove();
141+
LOG.warning("Unclosed (leaked?) stream detected: " + next.closeable);
142+
next.closeable.close();
143+
} catch (final IOException | RuntimeException ex) {
144+
LOG.warning("Unable to close (leaked?) stream: " + ex.getMessage());
145+
}
146+
}
147+
}
148+
149+
public static CachedOutputStreamCleaner create(Bus bus) {
150+
Number delayValue = null;
151+
BusLifeCycleManager busLifeCycleManager = null;
152+
153+
if (bus != null) {
154+
delayValue = (Number) bus.getProperty(CachedConstants.CLEANER_DELAY_BUS_PROP);
155+
busLifeCycleManager = bus.getExtension(BusLifeCycleManager.class);
156+
}
157+
158+
if (delayValue == null) {
159+
final DelayedCachedOutputStreamCleaner cleaner =
160+
new DelayedCachedOutputStreamCleaner(30, TimeUnit.MINUTES);
161+
if (busLifeCycleManager != null) {
162+
busLifeCycleManager.registerLifeCycleListener(cleaner);
163+
}
164+
return cleaner;
165+
} else {
166+
final long delay = delayValue.longValue();
167+
if (delay > 0) {
168+
final DelayedCachedOutputStreamCleaner cleaner =
169+
new DelayedCachedOutputStreamCleaner(delay, TimeUnit.MILLISECONDS);
170+
if (busLifeCycleManager != null) {
171+
busLifeCycleManager.registerLifeCycleListener(cleaner);
172+
}
173+
return cleaner;
174+
} else {
175+
return new CachedOutputStreamCleaner() {
176+
@Override
177+
public void unregister(Closeable closeable) {
178+
}
179+
180+
@Override
181+
public void register(Closeable closeable) {
182+
}
183+
184+
@Override
185+
public void clean() {
186+
}
187+
}; /* noop */
188+
}
189+
}
190+
}
191+
}

0 commit comments

Comments
 (0)