This is an automated email from the ASF dual-hosted git repository. reta pushed a commit to branch 4.0.x-fixes in repository https://gitbox.apache.org/repos/asf/cxf.git
commit 0a97dded6720e83507811900df8f9c4bf2ff6471 Author: Andriy Redko <[email protected]> AuthorDate: Sun Sep 22 10:13:32 2024 -0400 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 03a85a52f78e6eb5826e4b8bfc4992e0f4e0414b) --- core/pom.xml | 5 + .../java/org/apache/cxf/io/CachedConstants.java | 8 + .../java/org/apache/cxf/io/CachedOutputStream.java | 22 +- .../apache/cxf/io/CachedOutputStreamCleaner.java | 43 ++++ .../cxf/io/DelayedCachedOutputStreamCleaner.java | 239 +++++++++++++++++++++ .../main/resources/META-INF/cxf/bus-extensions.txt | 2 +- .../io/DelayedCachedOutputStreamCleanerTest.java | 201 +++++++++++++++++ parent/pom.xml | 1 - .../apache/cxf/systest/jaxws/ClientServerTest.java | 44 +++- 9 files changed, 561 insertions(+), 4 deletions(-) diff --git a/core/pom.xml b/core/pom.xml index 54709eba21..8ac8722025 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -180,6 +180,11 @@ <artifactId>saaj-impl</artifactId> <scope>test</scope> </dependency> + <dependency> + <groupId>org.awaitility</groupId> + <artifactId>awaitility</artifactId> + <scope>test</scope> + </dependency> </dependencies> <build> <plugins> diff --git a/core/src/main/java/org/apache/cxf/io/CachedConstants.java b/core/src/main/java/org/apache/cxf/io/CachedConstants.java index 24ba8d8347..1b23f66e99 100644 --- a/core/src/main/java/org/apache/cxf/io/CachedConstants.java +++ b/core/src/main/java/org/apache/cxf/io/CachedConstants.java @@ -71,6 +71,14 @@ public final class CachedConstants { public static final String CIPHER_TRANSFORMATION_BUS_PROP = "bus.io.CachedOutputStream.CipherTransformation"; + /** + * The delay (in ms) for cleaning up unclosed {@code CachedOutputStream} instances. 30 minutes + * is specified by default, the minimum value is 2 seconds. If the value of the delay is set to + * 0 (or is negative), the cleaner will be deactivated. + */ + public static final String CLEANER_DELAY_BUS_PROP = + "bus.io.CachedOutputStreamCleaner.Delay"; + private CachedConstants() { // complete } diff --git a/core/src/main/java/org/apache/cxf/io/CachedOutputStream.java b/core/src/main/java/org/apache/cxf/io/CachedOutputStream.java index ea8ce0d625..3ba937d04b 100644 --- a/core/src/main/java/org/apache/cxf/io/CachedOutputStream.java +++ b/core/src/main/java/org/apache/cxf/io/CachedOutputStream.java @@ -22,6 +22,7 @@ package org.apache.cxf.io; import java.io.BufferedOutputStream; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; +import java.io.Closeable; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; @@ -93,6 +94,7 @@ public class CachedOutputStream extends OutputStream { private List<CachedOutputStreamCallback> callbacks; private List<Object> streamList = new ArrayList<>(); + private CachedOutputStreamCleaner cachedOutputStreamCleaner; public CachedOutputStream() { this(defaultThreshold); @@ -127,6 +129,8 @@ public class CachedOutputStream extends OutputStream { outputDir = f; } } + + cachedOutputStreamCleaner = b.getExtension(CachedOutputStreamCleaner.class); } } @@ -279,6 +283,9 @@ public class CachedOutputStream extends OutputStream { } } finally { streamList.remove(currentStream); + if (cachedOutputStreamCleaner != null) { + cachedOutputStreamCleaner.unregister(currentStream); + } deleteTempFile(); inmem = true; } @@ -481,6 +488,9 @@ public class CachedOutputStream extends OutputStream { bout.writeTo(currentStream); inmem = false; streamList.add(currentStream); + if (cachedOutputStreamCleaner != null) { + cachedOutputStreamCleaner.register(this); + } } catch (Exception ex) { //Could be IOException or SecurityException or other issues. //Don't care what, just keep it in memory. @@ -512,6 +522,10 @@ public class CachedOutputStream extends OutputStream { try { InputStream fileInputStream = new TransferableFileInputStream(tempFile); streamList.add(fileInputStream); + if (cachedOutputStreamCleaner != null) { + cachedOutputStreamCleaner.register(fileInputStream); + } + if (cipherTransformation != null) { fileInputStream = new CipherInputStream(fileInputStream, ciphers.getDecryptor()) { boolean closed; @@ -537,7 +551,7 @@ public class CachedOutputStream extends OutputStream { FileUtils.delete(file); } } - private boolean maybeDeleteTempFile(Object stream) { + private boolean maybeDeleteTempFile(Closeable stream) { boolean postClosedInvoked = false; streamList.remove(stream); if (!inmem && tempFile != null && streamList.isEmpty() && allowDeleteOfFile) { @@ -549,6 +563,9 @@ public class CachedOutputStream extends OutputStream { //ignore } postClosedInvoked = true; + if (cachedOutputStreamCleaner != null) { + cachedOutputStreamCleaner.unregister(this); + } } deleteTempFile(); currentStream = new LoadingByteArrayOutputStream(1024); @@ -665,6 +682,9 @@ public class CachedOutputStream extends OutputStream { if (!closed) { super.close(); maybeDeleteTempFile(this); + if (cachedOutputStreamCleaner != null) { + cachedOutputStreamCleaner.unregister(this); + } } closed = true; } diff --git a/core/src/main/java/org/apache/cxf/io/CachedOutputStreamCleaner.java b/core/src/main/java/org/apache/cxf/io/CachedOutputStreamCleaner.java new file mode 100644 index 0000000000..3d6361a4c9 --- /dev/null +++ b/core/src/main/java/org/apache/cxf/io/CachedOutputStreamCleaner.java @@ -0,0 +1,43 @@ +/** + * 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.cxf.io; + +import java.io.Closeable; + +/** + * The {@link Bus} extension to clean up unclosed {@link CachedOutputStream} instances (and alike) backed by + * temporary files (leading to disk fill, see https://issues.apache.org/jira/browse/CXF-7396. + */ +public interface CachedOutputStreamCleaner { + /** + * Run the clean up + */ + void clean(); + + /** + * Register the stream instance for the clean up + */ + void unregister(Closeable closeable); + + /** + * Unregister the stream instance from the clean up (closed properly) + */ + void register(Closeable closeable); +} diff --git a/core/src/main/java/org/apache/cxf/io/DelayedCachedOutputStreamCleaner.java b/core/src/main/java/org/apache/cxf/io/DelayedCachedOutputStreamCleaner.java new file mode 100644 index 0000000000..04a258a558 --- /dev/null +++ b/core/src/main/java/org/apache/cxf/io/DelayedCachedOutputStreamCleaner.java @@ -0,0 +1,239 @@ +/** + * 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.cxf.io; + +import java.io.Closeable; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.Objects; +import java.util.Timer; +import java.util.TimerTask; +import java.util.concurrent.DelayQueue; +import java.util.concurrent.Delayed; +import java.util.concurrent.TimeUnit; +import java.util.logging.Logger; + +import jakarta.annotation.Resource; +import org.apache.cxf.Bus; +import org.apache.cxf.buslifecycle.BusLifeCycleListener; +import org.apache.cxf.buslifecycle.BusLifeCycleManager; +import org.apache.cxf.common.logging.LogUtils; + +public final class DelayedCachedOutputStreamCleaner implements CachedOutputStreamCleaner, BusLifeCycleListener { + private static final Logger LOG = LogUtils.getL7dLogger(DelayedCachedOutputStreamCleaner.class); + private static final long MIN_DELAY = 2000; /* 2 seconds */ + private static final DelayedCleaner NOOP_CLEANER = new DelayedCleaner() { + // NOOP + }; + + private DelayedCleaner cleaner = NOOP_CLEANER; + + private interface DelayedCleaner extends CachedOutputStreamCleaner, Closeable { + @Override + default void register(Closeable closeable) { + } + + @Override + default void unregister(Closeable closeable) { + } + + @Override + default void close() { + } + + @Override + default void clean() { + } + + default void forceClean() { + } + } + + private static final class DelayedCleanerImpl implements DelayedCleaner { + private final long delay; /* default is 30 minutes, in milliseconds */ + private final DelayQueue<DelayedCloseable> queue = new DelayQueue<>(); + private final Timer timer; + + DelayedCleanerImpl(final long delay) { + this.delay = delay; + this.timer = new Timer("DelayedCachedOutputStreamCleaner", true); + this.timer.scheduleAtFixedRate(new TimerTask() { + @Override + public void run() { + clean(); + } + }, 0, Math.max(MIN_DELAY, delay >> 1)); + } + + @Override + public void register(Closeable closeable) { + queue.put(new DelayedCloseable(closeable, delay)); + } + + @Override + public void unregister(Closeable closeable) { + queue.remove(new DelayedCloseable(closeable, delay)); + } + + @Override + public void clean() { + final Collection<DelayedCloseable> closeables = new ArrayList<>(); + queue.drainTo(closeables); + clean(closeables); + } + + @Override + public void forceClean() { + clean(queue); + } + + @Override + public void close() { + timer.cancel(); + queue.clear(); + } + + private void clean(Collection<DelayedCloseable> closeables) { + final Iterator<DelayedCloseable> iterator = closeables.iterator(); + while (iterator.hasNext()) { + final DelayedCloseable next = iterator.next(); + try { + iterator.remove(); + LOG.warning("Unclosed (leaked?) stream detected: " + next.closeable); + next.closeable.close(); + } catch (final IOException | RuntimeException ex) { + LOG.warning("Unable to close (leaked?) stream: " + ex.getMessage()); + } + } + } + } + + private static final class DelayedCloseable implements Delayed { + private final Closeable closeable; + private final long expireAt; + + DelayedCloseable(final Closeable closeable, final long delay) { + this.closeable = closeable; + this.expireAt = System.nanoTime() + delay; + } + + @Override + public int compareTo(Delayed o) { + return Long.compare(expireAt, ((DelayedCloseable) o).expireAt); + } + + @Override + public long getDelay(TimeUnit unit) { + return unit.convert(expireAt - System.nanoTime(), TimeUnit.NANOSECONDS); + } + + @Override + public int hashCode() { + return Objects.hash(closeable); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + + if (obj == null) { + return false; + } + + if (getClass() != obj.getClass()) { + return false; + } + + final DelayedCloseable other = (DelayedCloseable) obj; + return Objects.equals(closeable, other.closeable); + } + } + + @Resource + public void setBus(Bus bus) { + Number delayValue = null; + BusLifeCycleManager busLifeCycleManager = null; + + if (bus != null) { + delayValue = (Number) bus.getProperty(CachedConstants.CLEANER_DELAY_BUS_PROP); + busLifeCycleManager = bus.getExtension(BusLifeCycleManager.class); + } + + if (cleaner != null) { + cleaner.close(); + } + + if (delayValue == null) { + // Default delay is set to 30 mins + cleaner = new DelayedCleanerImpl(TimeUnit.MILLISECONDS.convert(30, TimeUnit.MINUTES)); + } else { + final long value = delayValue.longValue(); + if (value > 0 && value >= MIN_DELAY) { + cleaner = new DelayedCleanerImpl(value); /* already in milliseconds */ + } else { + cleaner = NOOP_CLEANER; + if (value != 0) { + throw new IllegalArgumentException("The value of " + CachedConstants.CLEANER_DELAY_BUS_PROP + + " property is invalid: " + value + " (should be >= " + MIN_DELAY + ", 0 to deactivate)"); + } + } + } + + if (busLifeCycleManager != null) { + busLifeCycleManager.registerLifeCycleListener(this); + } + } + + @Override + public void register(Closeable closeable) { + cleaner.register(closeable); + } + + @Override + public void unregister(Closeable closeable) { + cleaner.unregister(closeable); + } + + @Override + public void clean() { + cleaner.clean(); + } + + @Override + public void initComplete() { + } + + @Override + public void postShutdown() { + } + + @Override + public void preShutdown() { + cleaner.close(); + } + + public void forceClean() { + cleaner.forceClean(); + } +} diff --git a/core/src/main/resources/META-INF/cxf/bus-extensions.txt b/core/src/main/resources/META-INF/cxf/bus-extensions.txt index 3677c28b5a..93b8a6b3aa 100644 --- a/core/src/main/resources/META-INF/cxf/bus-extensions.txt +++ b/core/src/main/resources/META-INF/cxf/bus-extensions.txt @@ -11,4 +11,4 @@ org.apache.cxf.bus.resource.ResourceManagerImpl:org.apache.cxf.resource.Resource org.apache.cxf.catalog.OASISCatalogManager:org.apache.cxf.catalog.OASISCatalogManager:true org.apache.cxf.common.util.ASMHelperImpl:org.apache.cxf.common.util.ASMHelper:true org.apache.cxf.common.spi.ClassLoaderProxyService:org.apache.cxf.common.spi.ClassLoaderService:true - +org.apache.cxf.io.DelayedCachedOutputStreamCleaner:org.apache.cxf.io.CachedOutputStreamCleaner:true diff --git a/core/src/test/java/org/apache/cxf/io/DelayedCachedOutputStreamCleanerTest.java b/core/src/test/java/org/apache/cxf/io/DelayedCachedOutputStreamCleanerTest.java new file mode 100644 index 0000000000..3f0603aa7f --- /dev/null +++ b/core/src/test/java/org/apache/cxf/io/DelayedCachedOutputStreamCleanerTest.java @@ -0,0 +1,201 @@ +/** + * 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.cxf.io; + +import java.io.Closeable; +import java.io.IOException; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +import org.apache.cxf.Bus; +import org.apache.cxf.bus.extension.ExtensionManagerBus; + +import org.junit.After; +import org.junit.Test; + +import static org.awaitility.Awaitility.await; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +public class DelayedCachedOutputStreamCleanerTest { + private Bus bus; + + @After + public void tearDown() { + if (bus != null) { + bus.shutdown(true); + bus = null; + } + } + + @Test + public void testNoop() { + final Map<String, Object> properties = Collections.singletonMap(CachedConstants.CLEANER_DELAY_BUS_PROP, 0); + bus = new ExtensionManagerBus(new HashMap<>(), properties); + + final CachedOutputStreamCleaner cleaner = bus.getExtension(CachedOutputStreamCleaner.class); + assertThat(cleaner, instanceOf(DelayedCachedOutputStreamCleaner.class)); /* noop */ + + assertNoopCleaner(cleaner); + } + + @Test + public void testForceClean() throws InterruptedException { + bus = new ExtensionManagerBus(); + + final CachedOutputStreamCleaner cleaner = bus.getExtension(CachedOutputStreamCleaner.class); + assertThat(cleaner, instanceOf(DelayedCachedOutputStreamCleaner.class)); + + final AtomicBoolean latch = new AtomicBoolean(false); + final Closeable closeable = () -> latch.compareAndSet(false, true); + cleaner.register(closeable); + + final DelayedCachedOutputStreamCleaner delayedCleaner = (DelayedCachedOutputStreamCleaner) cleaner; + delayedCleaner.forceClean(); + + // Await for Closeable::close to be called + assertThat(latch.get(), is(true)); + } + + @Test + public void testClean() throws InterruptedException { + final AtomicInteger latch = new AtomicInteger(); + final Closeable closeable1 = () -> latch.incrementAndGet(); + final Closeable closeable2 = () -> latch.incrementAndGet(); + + /* Delay of 2.5 seconds */ + final Map<String, Object> properties = Collections.singletonMap(CachedConstants.CLEANER_DELAY_BUS_PROP, 2500); + bus = new ExtensionManagerBus(new HashMap<>(), properties); + + final CachedOutputStreamCleaner cleaner = bus.getExtension(CachedOutputStreamCleaner.class); + cleaner.register(closeable1); + cleaner.register(closeable2); + + // Await for Closeable::close to be called on schedule + await().atMost(5, TimeUnit.SECONDS).untilAtomic(latch, equalTo(2)); + assertThat(cleaner, instanceOf(DelayedCachedOutputStreamCleaner.class)); + } + + @Test + public void testForceCleanForEmpty() throws InterruptedException { + bus = new ExtensionManagerBus(); + + final CachedOutputStreamCleaner cleaner = bus.getExtension(CachedOutputStreamCleaner.class); + assertThat(cleaner, instanceOf(DelayedCachedOutputStreamCleaner.class)); + + final AtomicBoolean latch = new AtomicBoolean(false); + final Closeable closeable = () -> latch.compareAndSet(false, true); + + cleaner.register(closeable); + cleaner.unregister(closeable); + + final DelayedCachedOutputStreamCleaner delayedCleaner = (DelayedCachedOutputStreamCleaner) cleaner; + delayedCleaner.forceClean(); + + // Closeable::close should not be called + assertThat(latch.get(), is(false)); + } + + @Test + public void testForceCleanException() throws InterruptedException { + bus = new ExtensionManagerBus(); + + final CachedOutputStreamCleaner cleaner = bus.getExtension(CachedOutputStreamCleaner.class); + assertThat(cleaner, instanceOf(DelayedCachedOutputStreamCleaner.class)); + + final AtomicInteger latch = new AtomicInteger(); + final Closeable closeable2 = () -> latch.incrementAndGet(); + final Closeable closeable1 = () -> { + latch.incrementAndGet(); + throw new IOException("Simulated"); + }; + cleaner.register(closeable1); + cleaner.register(closeable2); + + final DelayedCachedOutputStreamCleaner delayedCleaner = (DelayedCachedOutputStreamCleaner) cleaner; + delayedCleaner.forceClean(); + + // Try to call force clean one more time + delayedCleaner.forceClean(); + + // Await for Closeable::close to be called + assertThat(latch.get(), equalTo(2)); + } + + @Test + public void testBusLifecycle() throws InterruptedException { + /* Delay of 2.5 seconds */ + final Map<String, Object> properties = Collections.singletonMap(CachedConstants.CLEANER_DELAY_BUS_PROP, 2500); + bus = new ExtensionManagerBus(new HashMap<>(), properties); + + final AtomicBoolean latch = new AtomicBoolean(); + final Closeable closeable = () -> latch.compareAndSet(false, true); + + bus.setProperty(CachedConstants.CLEANER_DELAY_BUS_PROP, 2500); /* 2.5 seconds */ + final CachedOutputStreamCleaner cleaner = bus.getExtension(CachedOutputStreamCleaner.class); + cleaner.register(closeable); + + // Closes the bus, the cleaner should cancel the internal timer(s) + bus.shutdown(true); + + // The Closeable::close should not be called since timer(s) is cancelled + await().during(3, TimeUnit.SECONDS).untilAtomic(latch, is(false)); + } + + @Test + public void testNegativeDelay() throws InterruptedException { + final Map<String, Object> properties = Collections.singletonMap(CachedConstants.CLEANER_DELAY_BUS_PROP, -1); + bus = new ExtensionManagerBus(new HashMap<>(), properties); + + final CachedOutputStreamCleaner cleaner = bus.getExtension(CachedOutputStreamCleaner.class); + assertThat(cleaner, instanceOf(DelayedCachedOutputStreamCleaner.class)); /* noop */ + + assertNoopCleaner(cleaner); + } + + @Test + public void testTooSmallDelay() throws InterruptedException { + final Map<String, Object> properties = Collections.singletonMap(CachedConstants.CLEANER_DELAY_BUS_PROP, 1500); + bus = new ExtensionManagerBus(new HashMap<>(), properties); + + final CachedOutputStreamCleaner cleaner = bus.getExtension(CachedOutputStreamCleaner.class); + assertThat(cleaner, instanceOf(DelayedCachedOutputStreamCleaner.class)); /* noop */ + + assertNoopCleaner(cleaner); + } + + private void assertNoopCleaner(final CachedOutputStreamCleaner cleaner) { + final AtomicBoolean latch = new AtomicBoolean(false); + final Closeable closeable = () -> latch.compareAndSet(false, true); + cleaner.register(closeable); + + final DelayedCachedOutputStreamCleaner delayedCleaner = (DelayedCachedOutputStreamCleaner) cleaner; + delayedCleaner.forceClean(); + + // Noop, Closeable::close should not be called + assertThat(latch.get(), is(false)); + } +} diff --git a/parent/pom.xml b/parent/pom.xml index 7abb5a478d..d17717c1f6 100644 --- a/parent/pom.xml +++ b/parent/pom.xml @@ -275,7 +275,6 @@ <cxf.wsdl4j.bundle.version>1.6.3_1</cxf.wsdl4j.bundle.version> <cxf.xmlresolver.bundle.version>1.2_5</cxf.xmlresolver.bundle.version> <cxf.xpp3.bundle.version>1.1.4c_6</cxf.xpp3.bundle.version> - <!-- Downgrade to 4.2.0 due to https://github.com/awaitility/awaitility/pull/279 --> <cxf.awaitility.version>4.2.2</cxf.awaitility.version> </properties> <build> diff --git a/systests/jaxws/src/test/java/org/apache/cxf/systest/jaxws/ClientServerTest.java b/systests/jaxws/src/test/java/org/apache/cxf/systest/jaxws/ClientServerTest.java index 47dfd025f4..89e551e21a 100644 --- a/systests/jaxws/src/test/java/org/apache/cxf/systest/jaxws/ClientServerTest.java +++ b/systests/jaxws/src/test/java/org/apache/cxf/systest/jaxws/ClientServerTest.java @@ -68,6 +68,8 @@ import org.apache.cxf.ext.logging.LoggingFeature; import org.apache.cxf.frontend.ClientProxy; import org.apache.cxf.helpers.FileUtils; import org.apache.cxf.io.CachedOutputStream; +import org.apache.cxf.io.CachedOutputStreamCleaner; +import org.apache.cxf.io.DelayedCachedOutputStreamCleaner; import org.apache.cxf.jaxws.DispatchImpl; import org.apache.cxf.message.Message; import org.apache.cxf.staxutils.StaxUtils; @@ -126,9 +128,10 @@ public class ClientServerTest extends AbstractBusClientServerTestBase { public static void startServers() throws Exception { // set up configuration to enable schema validation URL url = ClientServerTest.class.getResource("fault-stack-trace.xml"); + // Create bus first so it will be shared between the server and clients + createStaticBus(url.toString()); assertNotNull("cannot find test resource", url); assertTrue("server did not launch correctly", launchServer(Server.class, true)); - createStaticBus(url.toString()); } @Test @@ -1076,6 +1079,45 @@ public class ClientServerTest extends AbstractBusClientServerTestBase { FileUtils.removeDir(f); } + @Test + public void testEchoProviderThresholdTimeout() throws Exception { + final File f = Files.createTempDir(); + LOG.info("Using temp folder: " + f.getAbsolutePath()); + + System.setProperty("org.apache.cxf.io.CachedOutputStream.OutputDirectory", f.getAbsolutePath()); + CachedOutputStream.setDefaultThreshold(5); + + String requestString = "<echo/>"; + Service service = Service.create(serviceName); + service.addPort(fakePortName, jakarta.xml.ws.soap.SOAPBinding.SOAP11HTTP_BINDING, + "http://localhost:" + PORT + "/SoapContext/AsyncEchoProvider"); + Dispatch<StreamSource> dispatcher = service.createDispatch(fakePortName, + StreamSource.class, + Service.Mode.PAYLOAD); + dispatcher.getRequestContext().put("jakarta.xml.ws.client.receiveTimeout", "1000"); + dispatcher.getRequestContext().put("jakarta.xml.ws.client.connectionTimeout", "1000"); + + StreamSource request = new StreamSource(new ByteArrayInputStream(requestString.getBytes())); + try { + // Expecting java.net.SocketTimeoutException: Read timed out + StreamSource response = dispatcher.invoke(request); + assertEquals(requestString, StaxUtils.toString(response)); + } catch (final WebServiceException ex) { + ((DispatchImpl<StreamSource>)dispatcher).getClient().close(); + } + + //give the server side a little time to process it's part and close the files + if (f.list().length > 0) { + final CachedOutputStreamCleaner cleaner = getBus().getExtension(CachedOutputStreamCleaner.class); + if (cleaner instanceof DelayedCachedOutputStreamCleaner) { + ((DelayedCachedOutputStreamCleaner) cleaner).forceClean(); + } + } + + assertEquals("Expected no files but there is at list one", 0, f.list().length); + FileUtils.removeDir(f); + } + @Test public void testEchoProviderAsyncDecoupledEndpoints() throws Exception { String requestString = "<echo/>";
