Skip to content

Commit 26eaf39

Browse files
committed
Refactor the cleaner implementation and add guardrails for cleaner delay
1 parent f22a3e7 commit 26eaf39

File tree

3 files changed

+147
-51
lines changed

3 files changed

+147
-51
lines changed

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,8 @@ public final class CachedConstants {
7373

7474
/**
7575
* 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.
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.
7878
*/
7979
public static final String CLEANER_DELAY_BUS_PROP =
8080
"bus.io.CachedOutputStreamCleaner.Delay";

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

+106-47
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,92 @@
4040

4141
public final class DelayedCachedOutputStreamCleaner implements CachedOutputStreamCleaner, BusLifeCycleListener {
4242
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+
};
4347

44-
private long delay; /* default is 30 minutes, in milliseconds */
45-
private final DelayQueue<DelayedCloseable> queue = new DelayQueue<>();
46-
private Timer timer;
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+
}
47129

48130
private static final class DelayedCloseable implements Delayed {
49131
private final Closeable closeable;
@@ -97,52 +179,45 @@ public void setBus(Bus bus) {
97179
delayValue = (Number) bus.getProperty(CachedConstants.CLEANER_DELAY_BUS_PROP);
98180
busLifeCycleManager = bus.getExtension(BusLifeCycleManager.class);
99181
}
100-
182+
183+
if (cleaner != null) {
184+
cleaner.close();
185+
}
186+
101187
if (delayValue == null) {
102-
delay = TimeUnit.MILLISECONDS.convert(30, TimeUnit.MINUTES);
188+
// Default delay is set to 30 mins
189+
cleaner = new DelayedCleanerImpl(TimeUnit.MILLISECONDS.convert(30, TimeUnit.MINUTES));
103190
} else {
104191
final long value = delayValue.longValue();
105-
if (value > 0) {
106-
delay = value; /* already in milliseconds */
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+
LOG.warning("The value of " + CachedConstants.CLEANER_DELAY_BUS_PROP + " property is invalid: "
198+
+ value + " (should be >= " + MIN_DELAY + ", 0 to deactivate)");
199+
}
107200
}
108201
}
109202

110203
if (busLifeCycleManager != null) {
111204
busLifeCycleManager.registerLifeCycleListener(this);
112205
}
113-
114-
if (timer != null) {
115-
timer.cancel();
116-
timer = null;
117-
}
118-
119-
if (delay > 0) {
120-
timer = new Timer("DelayedCachedOutputStreamCleaner", true);
121-
timer.scheduleAtFixedRate(new TimerTask() {
122-
@Override
123-
public void run() {
124-
clean();
125-
}
126-
}, 0, Math.max(1, delay >> 1));
127-
}
128-
129206
}
130-
207+
131208
@Override
132209
public void register(Closeable closeable) {
133-
queue.put(new DelayedCloseable(closeable, delay));
210+
cleaner.register(closeable);
134211
}
135212

136213
@Override
137214
public void unregister(Closeable closeable) {
138-
queue.remove(new DelayedCloseable(closeable, delay));
215+
cleaner.unregister(closeable);
139216
}
140217

141218
@Override
142219
public void clean() {
143-
final Collection<DelayedCloseable> closeables = new ArrayList<>();
144-
queue.drainTo(closeables);
145-
clean(closeables);
220+
cleaner.clean();
146221
}
147222

148223
@Override
@@ -155,26 +230,10 @@ public void postShutdown() {
155230

156231
@Override
157232
public void preShutdown() {
158-
if (timer != null) {
159-
timer.cancel();
160-
}
233+
cleaner.close();
161234
}
162235

163236
public void forceClean() {
164-
clean(queue);
165-
}
166-
167-
private void clean(Collection<DelayedCloseable> closeables) {
168-
final Iterator<DelayedCloseable> iterator = closeables.iterator();
169-
while (iterator.hasNext()) {
170-
final DelayedCloseable next = iterator.next();
171-
try {
172-
iterator.remove();
173-
LOG.warning("Unclosed (leaked?) stream detected: " + next.closeable);
174-
next.closeable.close();
175-
} catch (final IOException | RuntimeException ex) {
176-
LOG.warning("Unable to close (leaked?) stream: " + ex.getMessage());
177-
}
178-
}
237+
cleaner.forceClean();
179238
}
180239
}

core/src/test/java/org/apache/cxf/io/DelayedCachedOutputStreamCleanerTest.java

+39-2
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,10 @@ public class DelayedCachedOutputStreamCleanerTest {
4545

4646
@After
4747
public void tearDown() {
48-
bus.shutdown(true);
49-
bus = null;
48+
if (bus != null) {
49+
bus.shutdown(true);
50+
bus = null;
51+
}
5052
}
5153

5254
@Test
@@ -56,6 +58,8 @@ public void testNoop() {
5658

5759
final CachedOutputStreamCleaner cleaner = bus.getExtension(CachedOutputStreamCleaner.class);
5860
assertThat(cleaner, instanceOf(DelayedCachedOutputStreamCleaner.class)); /* noop */
61+
62+
assertNoopCleaner(cleaner);
5963
}
6064

6165
@Test
@@ -161,4 +165,37 @@ public void testBusLifecycle() throws InterruptedException {
161165
await().during(3, TimeUnit.SECONDS).untilAtomic(latch, is(false));
162166
}
163167

168+
@Test
169+
public void testNegativeDelay() throws InterruptedException {
170+
final Map<String, Object> properties = Collections.singletonMap(CachedConstants.CLEANER_DELAY_BUS_PROP, -1);
171+
bus = new ExtensionManagerBus(new HashMap<>(), properties);
172+
173+
final CachedOutputStreamCleaner cleaner = bus.getExtension(CachedOutputStreamCleaner.class);
174+
assertThat(cleaner, instanceOf(DelayedCachedOutputStreamCleaner.class)); /* noop */
175+
176+
assertNoopCleaner(cleaner);
177+
}
178+
179+
@Test
180+
public void testTooSmallDelay() throws InterruptedException {
181+
final Map<String, Object> properties = Collections.singletonMap(CachedConstants.CLEANER_DELAY_BUS_PROP, 1500);
182+
bus = new ExtensionManagerBus(new HashMap<>(), properties);
183+
184+
final CachedOutputStreamCleaner cleaner = bus.getExtension(CachedOutputStreamCleaner.class);
185+
assertThat(cleaner, instanceOf(DelayedCachedOutputStreamCleaner.class)); /* noop */
186+
187+
assertNoopCleaner(cleaner);
188+
}
189+
190+
private void assertNoopCleaner(final CachedOutputStreamCleaner cleaner) {
191+
final AtomicBoolean latch = new AtomicBoolean(false);
192+
final Closeable closeable = () -> latch.compareAndSet(false, true);
193+
cleaner.register(closeable);
194+
195+
final DelayedCachedOutputStreamCleaner delayedCleaner = (DelayedCachedOutputStreamCleaner) cleaner;
196+
delayedCleaner.forceClean();
197+
198+
// Noop, Closeable::close should not be called
199+
assertThat(latch.get(), is(false));
200+
}
164201
}

0 commit comments

Comments
 (0)