Skip to content

Commit f0d8782

Browse files
committed
Use a ReentrantLock in HttpConnection
Simplifies how we grab the lock. Also tweaked the IT test so it completes as soon as all exceptions thrown, vs waiting for the SlowRider timeout.
1 parent 01c823e commit f0d8782

File tree

2 files changed

+23
-12
lines changed

2 files changed

+23
-12
lines changed

src/main/java/org/jsoup/helper/HttpConnection.java

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
import java.util.LinkedHashMap;
4141
import java.util.List;
4242
import java.util.Map;
43+
import java.util.concurrent.locks.ReentrantLock;
4344
import java.util.regex.Pattern;
4445
import java.util.zip.GZIPInputStream;
4546
import java.util.zip.Inflater;
@@ -621,7 +622,7 @@ public static class Request extends HttpConnection.Base<Connection.Request> impl
621622
@Nullable RequestAuthenticator authenticator;
622623
private @Nullable Progress<Connection.Response> responseProgress;
623624

624-
private volatile boolean executing = false;
625+
private final ReentrantLock executing = new ReentrantLock(); // detects and warns if same request used concurrently
625626

626627
Request() {
627628
super();
@@ -654,7 +655,6 @@ public static class Request extends HttpConnection.Base<Connection.Request> impl
654655
cookieManager = copy.cookieManager;
655656
authenticator = copy.authenticator;
656657
responseProgress = copy.responseProgress;
657-
executing = false;
658658
}
659659

660660
@Override @Nullable
@@ -847,10 +847,7 @@ static Response execute(HttpConnection.Request req) throws IOException {
847847
}
848848

849849
static Response execute(HttpConnection.Request req, @Nullable Response prevRes) throws IOException {
850-
synchronized (req) {
851-
Validate.isFalse(req.executing, "Multiple threads were detected trying to execute the same request concurrently. Make sure to use Connection#newRequest() and do not share an executing request between threads.");
852-
req.executing = true;
853-
}
850+
Validate.isTrue(req.executing.tryLock(), "Multiple threads were detected trying to execute the same request concurrently. Make sure to use Connection#newRequest() and do not share an executing request between threads.");
854851
Validate.notNullParam(req, "req");
855852
URL url = req.url();
856853
Validate.notNull(url, "URL must be specified to connect");
@@ -890,7 +887,6 @@ else if (supportsBody)
890887
URL redir = StringUtil.resolve(req.url(), location);
891888
req.url(redir);
892889

893-
req.executing = false;
894890
return execute(req, res);
895891
}
896892
if ((res.statusCode < 200 || res.statusCode >= 400) && !req.ignoreHttpErrors())
@@ -932,7 +928,7 @@ else if (res.hasHeaderWithValue(CONTENT_ENCODING, "deflate"))
932928
if (res != null) res.safeClose(); // will be non-null if got to conn
933929
throw e;
934930
} finally {
935-
req.executing = false;
931+
req.executing.unlock();
936932

937933
// detach any thread local auth delegate
938934
if (req.authenticator != null)

src/test/java/org/jsoup/integration/SessionIT.java

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -75,15 +75,28 @@ public void multiThread() throws InterruptedException {
7575
// test that we throw a nice clear exception if you try to multi-thread by forget .newRequest()
7676
@Test
7777
public void multiThreadWithoutNewRequestBlowsUp() throws InterruptedException {
78-
int numThreads = 20;
79-
String url = SlowRider.Url + "?" + SlowRider.MaxTimeParam + "=10000"; // this makes sure that the first req is still executing whilst the others run
78+
int numThreads = 5;
79+
String url = SlowRider.Url + "?" + SlowRider.MaxTimeParam + "=20000"; // this makes sure that the first req is still executing whilst the others run
8080
String title = "Slow Rider";
8181

8282
ThreadCatcher catcher = new ThreadCatcher();
8383
Connection session = Jsoup.newSession();
8484

85-
Thread[] threads = new Thread[numThreads];
85+
// run first slow request
8686
AtomicInteger successful = new AtomicInteger();
87+
Thread slow = new Thread(() -> {
88+
try {
89+
Document doc = session.url(url).get();
90+
assertEquals(title, doc.title());
91+
} catch (IOException e) {
92+
throw new UncheckedIOException(e);
93+
}
94+
});
95+
slow.start();
96+
Thread.sleep(100); // yield so that thread can start before the next do
97+
98+
// spawn others, should fail
99+
Thread[] threads = new Thread[numThreads];
87100
for (int threadNum = 0; threadNum < numThreads; threadNum++) {
88101
Thread thread = new Thread(() -> {
89102
try {
@@ -104,6 +117,8 @@ public void multiThreadWithoutNewRequestBlowsUp() throws InterruptedException {
104117
for (Thread thread : threads) {
105118
thread.join();
106119
}
120+
// cancel the slow runner so we can wrap up the test quicker
121+
slow.interrupt();
107122

108123
// only one should have passed, rest should have blown up (assuming the started whilst other was running)
109124
//assertEquals(numThreads - 1, catcher.multiThreadExceptions.get());
@@ -114,7 +129,7 @@ public void multiThreadWithoutNewRequestBlowsUp() throws InterruptedException {
114129
macOS runners, which appear overtaxed. That makes this test flaky. So we relax the test conditions, and make
115130
sure at least just one passed and one failed. That's OK in prod as well, because we are only concerned about
116131
concurrent execution, which the impl does detect correctly. */
117-
assertTrue(successful.get() > 0);
132+
assertEquals(0, successful.get());
118133
assertTrue(catcher.multiThreadExceptions.get() > 0);
119134
assertEquals(catcher.multiThreadExceptions.get(), catcher.exceptionCount.get()); // all exceptions are multi-threaded
120135
}

0 commit comments

Comments
 (0)