You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@commons.apache.org by gg...@apache.org on 2023/01/18 16:15:44 UTC
[commons-vfs] branch master updated: [VFS-683] Class loader thread safety (#367)
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 <iv...@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.
*/