Skip to content

Commit 374ded2

Browse files
committed
Use a threadlocal to hold the NodeIterator
Ensures that an Evaluator query can be multi-threaded Fixes #2088
1 parent 4fb1036 commit 374ded2

File tree

4 files changed

+67
-3
lines changed

4 files changed

+67
-3
lines changed

CHANGES.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@
2727
* When generating XML-syntax output from parsed HTML, script nodes containing (pseudo) CData sections would have an
2828
extraneous CData section added, causing script execution errors. Now, the data content is emitted in a HTML/XML/XHTML
2929
polyglot format, if the data is not already within a CData section. [2078](https://github.com/jhy/jsoup/issues/2078)
30+
* The `:has` evaluator held a non-thread-safe Iterator, and so if an Evaluator object was shared across multiple
31+
concurrent threads, a NoSuchElement exception may be thrown, and the selected results may be incorrect. Now, the
32+
iterator object is a thread-local. [2088](https://github.com/jhy/jsoup/issues/2088)
3033

3134
---
3235
Older changes for versions 0.1.1 (2010-Jan-31) through 1.17.1 (2023-Nov-27) may be found in

src/main/java/org/jsoup/select/StructuralEvaluator.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,16 +58,20 @@ public boolean matches(Element root, Element element) {
5858
}
5959
}
6060

61-
static class Has extends StructuralEvaluator {
62-
final NodeIterator<Element> it = new NodeIterator<>(new Element("html"), Element.class);
61+
final ThreadLocal<NodeIterator<Element>> threadHasIter =
62+
ThreadLocal.withInitial(() -> new NodeIterator<>(new Element("html"), Element.class));
6363
// the element here is just a placeholder so this can be final - gets set in restart()
6464

65+
static class Has extends StructuralEvaluator {
66+
6567
public Has(Evaluator evaluator) {
6668
super(evaluator);
6769
}
6870

6971
@Override public boolean matches(Element root, Element element) {
7072
// for :has, we only want to match children (or below), not the input element. And we want to minimize GCs
73+
NodeIterator<Element> it = threadHasIter.get();
74+
7175
it.restart(element);
7276
while (it.hasNext()) {
7377
Element el = it.next();
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package org.jsoup.select;
2+
3+
import org.jsoup.Jsoup;
4+
import org.jsoup.nodes.Document;
5+
import org.junit.jupiter.api.Test;
6+
7+
import java.util.concurrent.atomic.AtomicInteger;
8+
9+
import static org.junit.jupiter.api.Assertions.assertEquals;
10+
11+
public class SelectorIT {
12+
13+
@Test
14+
public void multiThreadHas() throws InterruptedException {
15+
final String html = "<div id=1></div><div id=2><p>One</p><p>Two</p>";
16+
final Evaluator eval = QueryParser.parse("div:has(p)");
17+
18+
int numThreads = 20;
19+
int numThreadLoops = 5;
20+
21+
SelectorIT.ThreadCatcher catcher = new SelectorIT.ThreadCatcher();
22+
23+
Thread[] threads = new Thread[numThreads];
24+
for (int threadNum = 0; threadNum < numThreads; threadNum++) {
25+
Thread thread = new Thread(() -> {
26+
Document doc = Jsoup.parse(html);
27+
for (int loop = 0; loop < numThreadLoops; loop++) {
28+
Elements els = doc.select(eval);
29+
assertEquals(1, els.size());
30+
assertEquals("2", els.get(0).id());
31+
}
32+
});
33+
thread.setName("Runner-" + threadNum);
34+
thread.start();
35+
thread.setUncaughtExceptionHandler(catcher);
36+
threads[threadNum] = thread;
37+
}
38+
39+
// now join them all
40+
for (Thread thread : threads) {
41+
thread.join();
42+
}
43+
44+
assertEquals(0, catcher.exceptionCount.get());
45+
}
46+
47+
static class ThreadCatcher implements Thread.UncaughtExceptionHandler {
48+
AtomicInteger exceptionCount = new AtomicInteger();
49+
50+
@Override
51+
public void uncaughtException(Thread t, Throwable e) {
52+
53+
e.printStackTrace();
54+
exceptionCount.incrementAndGet();
55+
}
56+
}
57+
58+
}

src/test/java/org/jsoup/select/SelectorTest.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
import org.jsoup.Jsoup;
44
import org.jsoup.MultiLocaleExtension.MultiLocaleTest;
5-
import org.jsoup.TextUtil;
65
import org.jsoup.nodes.Document;
76
import org.jsoup.nodes.Element;
87
import org.jsoup.parser.Parser;

0 commit comments

Comments
 (0)