This is an automated email from the ASF dual-hosted git repository. ggregory pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/commons-vfs.git
The following commit(s) were added to refs/heads/master by this push: new 781a9152 [VFS-683] Class loader thread safety (#367) 781a9152 is described below commit 781a9152796637936194a8b553ddb8c082e58a96 Author: Ivan Bella <i...@bella.name> AuthorDate: Wed Jan 18 11:15:39 2023 -0500 [VFS-683] Class loader thread safety (#367) * fixes #VFS-683: class loader thread safety * To ensure that the closing of files does not interfere with * reading the content, the FileObjectUtils read methods must * be synchronized on the underlying filesystem just like the * close AbstractFileObject close and detach methods. * Added a test case that demostrated the issue. * review updates * Remove dependence on google ThreadFactoryBuilder * Added exception stack traces to the error message * Double the threads in the thread safety test from 20 to 40 * Added synchronization around the HeadMethod creation as the detach will clear that out, possibly which the getHeadMethod is being called. --- .../commons/vfs2/provider/http/HttpFileObject.java | 31 ++++--- .../apache/commons/vfs2/util/FileObjectUtils.java | 18 ++-- .../commons/vfs2/impl/VfsClassLoaderTests.java | 99 ++++++++++++++++++++++ 3 files changed, 129 insertions(+), 19 deletions(-) diff --git a/commons-vfs2/src/main/java/org/apache/commons/vfs2/provider/http/HttpFileObject.java b/commons-vfs2/src/main/java/org/apache/commons/vfs2/provider/http/HttpFileObject.java index 45fe6c0a..3464f612 100644 --- a/commons-vfs2/src/main/java/org/apache/commons/vfs2/provider/http/HttpFileObject.java +++ b/commons-vfs2/src/main/java/org/apache/commons/vfs2/provider/http/HttpFileObject.java @@ -98,7 +98,9 @@ public class HttpFileObject<FS extends HttpFileSystem> extends AbstractFileObjec */ @Override protected void doDetach() throws Exception { - method = null; + synchronized (getFileSystem()) { + method = null; + } } /** @@ -106,7 +108,7 @@ public class HttpFileObject<FS extends HttpFileSystem> extends AbstractFileObjec */ @Override protected long doGetContentSize() throws Exception { - final Header header = method.getResponseHeader("content-length"); + final Header header = getHeadMethod().getResponseHeader("content-length"); if (header == null) { // Assume 0 content-length return 0; @@ -147,7 +149,7 @@ public class HttpFileObject<FS extends HttpFileSystem> extends AbstractFileObjec */ @Override protected long doGetLastModifiedTime() throws Exception { - final Header header = method.getResponseHeader("last-modified"); + final Header header = getHeadMethod().getResponseHeader("last-modified"); FileSystemException.requireNonNull(header, "vfs.provider.http/last-modified.error", getName()); return DateUtil.parseDate(header.getValue()).getTime(); } @@ -202,18 +204,21 @@ public class HttpFileObject<FS extends HttpFileSystem> extends AbstractFileObjec } HeadMethod getHeadMethod() throws IOException { - if (method != null) { + // need to synchronize on the filesystem as the detach method will clear out "method" + synchronized (getFileSystem()) { + if (method != null) { + return method; + } + method = new HeadMethod(); + try { + setupMethod(method); + final HttpClient client = getAbstractFileSystem().getClient(); + client.executeMethod(method); + } finally { + method.releaseConnection(); + } return method; } - method = new HeadMethod(); - try { - setupMethod(method); - final HttpClient client = getAbstractFileSystem().getClient(); - client.executeMethod(method); - } finally { - method.releaseConnection(); - } - return method; } protected String getUrlCharset() { diff --git a/commons-vfs2/src/main/java/org/apache/commons/vfs2/util/FileObjectUtils.java b/commons-vfs2/src/main/java/org/apache/commons/vfs2/util/FileObjectUtils.java index d61df19a..dc6e7f10 100644 --- a/commons-vfs2/src/main/java/org/apache/commons/vfs2/util/FileObjectUtils.java +++ b/commons-vfs2/src/main/java/org/apache/commons/vfs2/util/FileObjectUtils.java @@ -82,8 +82,10 @@ public final class FileObjectUtils { * @since 2.6.0 */ public static byte[] getContentAsByteArray(final FileObject file) throws IOException { - try (FileContent content = file.getContent()) { - return content.getByteArray(); + synchronized (file.getFileSystem()) { + try (FileContent content = file.getContent()) { + return content.getByteArray(); + } } } @@ -97,8 +99,10 @@ public final class FileObjectUtils { * @since 2.4 */ public static String getContentAsString(final FileObject file, final Charset charset) throws IOException { - try (FileContent content = file.getContent()) { - return content.getString(charset); + synchronized (file.getFileSystem()) { + try (FileContent content = file.getContent()) { + return content.getString(charset); + } } } @@ -112,8 +116,10 @@ public final class FileObjectUtils { * @since 2.4 */ public static String getContentAsString(final FileObject file, final String charset) throws IOException { - try (FileContent content = file.getContent()) { - return content.getString(charset); + synchronized (file.getFileSystem()) { + try (FileContent content = file.getContent()) { + return content.getString(charset); + } } } diff --git a/commons-vfs2/src/test/java/org/apache/commons/vfs2/impl/VfsClassLoaderTests.java b/commons-vfs2/src/test/java/org/apache/commons/vfs2/impl/VfsClassLoaderTests.java index 5f9bc530..533ba676 100644 --- a/commons-vfs2/src/test/java/org/apache/commons/vfs2/impl/VfsClassLoaderTests.java +++ b/commons-vfs2/src/test/java/org/apache/commons/vfs2/impl/VfsClassLoaderTests.java @@ -19,11 +19,23 @@ package org.apache.commons.vfs2.impl; import static org.apache.commons.vfs2.VfsTestUtils.getTestDirectoryFile; import java.io.File; +import java.io.PrintWriter; import java.net.URL; import java.net.URLConnection; +import java.util.ArrayList; import java.util.Collections; import java.util.Enumeration; +import java.util.LinkedList; +import java.util.List; +import java.util.Queue; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.RejectedExecutionHandler; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import org.apache.commons.io.output.StringBuilderWriter; import org.apache.commons.vfs2.AbstractProviderTestCase; import org.apache.commons.vfs2.Capability; import org.apache.commons.vfs2.FileObject; @@ -203,6 +215,93 @@ public class VfsClassLoaderTests extends AbstractProviderTestCase { verifyPackage(pack, true); } + @Test + public void testThreadSafety() throws Exception { + final int THREADS = 40; + final BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(THREADS * 2); + final List<Throwable> exceptions = new ArrayList<>(); + final Thread.UncaughtExceptionHandler handler = new Thread.UncaughtExceptionHandler() { + @Override + public void uncaughtException(Thread t, Throwable e) { + synchronized (exceptions) { + exceptions.add(e); + } + } + }; + final ThreadFactory factory = new ThreadFactory() { + @Override + public Thread newThread(Runnable r) { + Thread thread = new Thread(r, "VfsClassLoaderTests.testThreadSafety"); + thread.setUncaughtExceptionHandler(handler); + return thread; + } + }; + final Queue<Runnable> rejections = new LinkedList<>(); + final RejectedExecutionHandler rejectionHandler = new RejectedExecutionHandler() { + @Override + public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) { + synchronized (rejections) { + rejections.add(r); + } + } + }; + final ThreadPoolExecutor executor = new ThreadPoolExecutor(THREADS, THREADS, 0, TimeUnit.SECONDS, workQueue, factory, rejectionHandler); + executor.prestartAllCoreThreads(); + for (int i = 0; i < THREADS; i++) { + final VFSClassLoader loader = createClassLoader(); + workQueue.put(new VfsClassLoaderTests.LoadClass(loader)); + } + while (!workQueue.isEmpty()) { + Thread.sleep(10); + } + while (!rejections.isEmpty() && executor.getActiveCount() > 0) { + final List<Runnable> rejected = new ArrayList<>(); + synchronized(rejections) { + rejected.addAll(rejections); + rejections.clear(); + } + workQueue.addAll(rejected); + } + executor.shutdown(); + executor.awaitTermination(30, TimeUnit.SECONDS); + assertEquals(THREADS, executor.getCompletedTaskCount()); + if (!exceptions.isEmpty()) { + StringBuilder exceptionMsg = new StringBuilder(); + StringBuilderWriter writer = new StringBuilderWriter(exceptionMsg); + PrintWriter pWriter = new PrintWriter(writer); + for (Throwable t : exceptions) { + pWriter.write(t.getMessage()); + pWriter.write('\n'); + t.printStackTrace(pWriter); + pWriter.write('\n'); + } + pWriter.flush(); + assertTrue(exceptions.size() + " threads failed: " + exceptionMsg, exceptions.isEmpty()); + } + } + + private class LoadClass implements Runnable { + private final VFSClassLoader loader; + public LoadClass(VFSClassLoader loader) { + this.loader = loader; + } + + @Override + public void run() { + try { + final Class<?> testClass = loader.findClass("code.ClassToLoad"); + final Package pack = testClass.getPackage(); + assertEquals("code", pack.getName()); + verifyPackage(pack, false); + + final Object testObject = testClass.newInstance(); + assertEquals("**PRIVATE**", testObject.toString()); + } catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) { + throw new RuntimeException(e); + } + } + } + /** * Verify the package loaded with class loader. */