Skip to content

Commit 691873e

Browse files
fix race condition in MapMerge duplicate handling under fork-join (#2595)
1 parent 3dd2f6a commit 691873e

File tree

4 files changed

+41
-31
lines changed

4 files changed

+41
-31
lines changed

basex-core/src/main/java/org/basex/query/func/map/MapBuild.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ public XQMap item(final QueryContext qc, final InputInfo ii) throws QueryExcepti
3333
final Iter iter = (keys != null ? invoke(keys, args, qc) : item).atomIter(qc, info);
3434
for(Item key; (key = qc.next(iter)) != null;) {
3535
final Value old = builder.get(key);
36-
final Value val = dups.merge(key, old, value != null ? invoke(value, args, qc) : item);
36+
final Value val = dups.merge(key, old, value != null ? invoke(value, args, qc) : item, qc);
3737
if(val != null) builder.put(key, val);
3838
}
3939
}

basex-core/src/main/java/org/basex/query/func/map/MapDuplicates.java

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,22 +17,25 @@ public abstract class MapDuplicates {
1717
* @param key key
1818
* @param old old (can be {@code null})
1919
* @param value value
20+
* @param qc query context
2021
* @return merged value, or new value if the old one is {@code null}
2122
* @throws QueryException query exception
2223
*/
23-
Value merge(final Item key, final Value old, final Value value) throws QueryException {
24-
return old != null ? get(key, old, value) : value;
24+
Value merge(final Item key, final Value old, final Value value, final QueryContext qc)
25+
throws QueryException {
26+
return old != null ? get(key, old, value, qc) : value;
2527
}
2628

2729
/**
2830
* Merges the old and new value.
2931
* @param key key
3032
* @param old old
3133
* @param value value
34+
* @param qc query context
3235
* @return merged value, or {@code null} if insertion of map entry can be skipped
3336
* @throws QueryException query exception
3437
*/
35-
abstract Value get(Item key, Value old, Value value) throws QueryException;
38+
abstract Value get(Item key, Value old, Value value, QueryContext qc) throws QueryException;
3639

3740
/**
3841
* Returns the result type.

basex-core/src/main/java/org/basex/query/func/map/MapMerge.java

Lines changed: 14 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ public XQMap item(final QueryContext qc, final InputInfo ii) throws QueryExcepti
6969
final XQMap map = toMap(current);
7070
for(final Item key : map.keys()) {
7171
final Value old = mb != null ? mb.get(key) : mp.getOrNull(key);
72-
final Value val = dups.merge(key, old, map.get(key));
72+
final Value val = dups.merge(key, old, map.get(key), qc);
7373
if(val != null) {
7474
if(mb != null) mb.put(key, val);
7575
else mp = mp.put(key, val);
@@ -143,7 +143,7 @@ final MapDuplicates duplicates(final MergeOptions options, final QueryContext qc
143143
if(md != null) return md;
144144
// function
145145
final Value duplicates = options.get(MergeOptions.DUPLICATES);
146-
if(duplicates instanceof FItem) return new Invoke(toFunction(duplicates, 2, qc), qc);
146+
if(duplicates instanceof FItem) return new Invoke(toFunction(duplicates, 2, qc));
147147
// fixed option
148148
final String string = duplicates.isEmpty() ? dflt.toString() : toString(duplicates, qc);
149149
final Duplicates value = Enums.get(Duplicates.class, string);
@@ -152,7 +152,7 @@ final MapDuplicates duplicates(final MergeOptions options, final QueryContext qc
152152

153153
return switch(value) {
154154
case REJECT -> new Reject();
155-
case COMBINE -> new Combine(qc);
155+
case COMBINE -> new Combine();
156156
case USE_FIRST -> new UseFirst();
157157
default -> new UseLast();
158158
};
@@ -178,7 +178,7 @@ public int hofOffsets() {
178178
*/
179179
static final class UseFirst extends MapDuplicates {
180180
@Override
181-
Value get(final Item key, final Value old, final Value value) {
181+
Value get(final Item key, final Value old, final Value value, final QueryContext qc) {
182182
return null;
183183
}
184184
}
@@ -188,7 +188,7 @@ Value get(final Item key, final Value old, final Value value) {
188188
*/
189189
static final class UseLast extends MapDuplicates {
190190
@Override
191-
Value get(final Item key, final Value old, final Value value) {
191+
Value get(final Item key, final Value old, final Value value, final QueryContext qc) {
192192
return value;
193193
}
194194
}
@@ -197,19 +197,8 @@ Value get(final Item key, final Value old, final Value value) {
197197
* Concatenate values.
198198
*/
199199
static final class Combine extends MapDuplicates {
200-
/** Query context. */
201-
private final QueryContext qc;
202-
203-
/**
204-
* Constructor.
205-
* @param qc query context
206-
*/
207-
Combine(final QueryContext qc) {
208-
this.qc = qc;
209-
}
210-
211200
@Override
212-
Value get(final Item key, final Value old, final Value value) {
201+
Value get(final Item key, final Value old, final Value value, final QueryContext qc) {
213202
return old.append(value, qc);
214203
}
215204

@@ -224,7 +213,8 @@ SeqType type(final SeqType st) {
224213
*/
225214
final class Reject extends MapDuplicates {
226215
@Override
227-
Value get(final Item key, final Value old, final Value value) throws QueryException {
216+
Value get(final Item key, final Value old, final Value value, final QueryContext qc)
217+
throws QueryException {
228218
throw MERGE_DUPLICATE_X.get(info, key);
229219
}
230220
}
@@ -235,25 +225,22 @@ Value get(final Item key, final Value old, final Value value) throws QueryExcept
235225
final class Invoke extends MapDuplicates {
236226
/** Combiner function. */
237227
private final FItem function;
238-
/** Query context. */
239-
private final QueryContext qc;
240228
/** HOF arguments. */
241-
private final HofArgs args;
229+
private final ThreadLocal<HofArgs> args;
242230

243231
/**
244232
* Constructor.
245233
* @param function combiner function
246-
* @param qc query context
247234
*/
248-
Invoke(final FItem function, final QueryContext qc) {
235+
Invoke(final FItem function) {
249236
this.function = function;
250-
this.qc = qc;
251-
args = new HofArgs(2);
237+
args = ThreadLocal.withInitial(() -> new HofArgs(2));
252238
}
253239

254240
@Override
255-
Value get(final Item key, final Value old, final Value value) throws QueryException {
256-
return function.invoke(qc, info, args.set(0, old).set(1, value).get());
241+
Value get(final Item key, final Value old, final Value value, final QueryContext qc)
242+
throws QueryException {
243+
return function.invoke(qc, info, args.get().set(0, old).set(1, value).get());
257244
}
258245

259246
@Override

basex-core/src/test/java/org/basex/query/func/XQueryModuleTest.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,26 @@ public final class XQueryModuleTest extends SandboxTest {
152152
query(func.args(" (true#0, false#0)", " { 'parallel': 1000000000 }"), "true\nfalse");
153153
query(func.args(" (true#0, false#0)", " { 'parallel': <_>1</_> }"), "true\nfalse");
154154

155+
// MapMerge mutable instance variables
156+
query("xquery:fork-join(\r\n"
157+
+ " for $t in 1 to 100\r\n"
158+
+ " return function() {\r\n"
159+
+ " for $i in 1 to 10\r\n"
160+
+ " let $v := map:build(\r\n"
161+
+ " ($i, $i),\r\n"
162+
+ " (),\r\n"
163+
+ " (),\r\n"
164+
+ " { 'duplicates': function($x, $y) { $x * $y } }\r\n"
165+
+ " )($i)\r\n"
166+
+ " return\r\n"
167+
+ " if($v eq $i * $i) then\r\n"
168+
+ " $v\r\n"
169+
+ " else\r\n"
170+
+ " error(xs:QName('err:FAIL'), `{$i}: expected {$i * $i}, but got {$v}`)\r\n"
171+
+ " }\r\n"
172+
+ ")\r\n"
173+
+ "=> count()", 1000);
174+
155175
// optimizations
156176
check(func.args(" ()"), "", empty());
157177
check(func.args(" false#0"), false, root(DynFuncCall.class));

0 commit comments

Comments
 (0)