Skip to content

Commit ea091f7

Browse files
Fixes coalesce and case operators in multithreaded environments (#2136) (#2168)
The bug is described in the issue #2136 TestArgumentListFunctionExpressionConcurrency.java - adds tests that reproduce the bug. ExpressionOperator.java - printCollection method is modified so it does not modify the shared argumentIndexes field. ArgumentListFunctionExpression.java - previous "workarounds" are removed Co-authored-by: Igor Mukhin <igor.mukhin@capgemini.com>
1 parent 6de65af commit ea091f7

File tree

3 files changed

+129
-24
lines changed

3 files changed

+129
-24
lines changed

foundation/org.eclipse.persistence.core/src/org/eclipse/persistence/expressions/ExpressionOperator.java

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2380,15 +2380,12 @@ public void printCollection(List<Expression> items, ExpressionSQLPrinter printer
23802380
dbStringIndex = 1;
23812381
}
23822382

2383-
if (this.argumentIndices == null) {
2384-
this.argumentIndices = new int[items.size()];
2385-
for (int i = 0; i < this.argumentIndices.length; i++){
2386-
this.argumentIndices[i] = i;
2387-
}
2388-
}
2383+
// Empty `this.argumentIndices` means the operator expects a list of arguments with a variable length.
2384+
// #2136: As operator's state is shared among all threads, we are not allowed to modify the field `this.argumentIndices`.
2385+
int[] argumentIndexes = (this.argumentIndices != null ? this.argumentIndices : arrayIndexSequence(items.size()));
23892386

23902387
String[] dbStrings = getDatabaseStrings(items.size());
2391-
for (final int index : this.argumentIndices) {
2388+
for (final int index : argumentIndexes) {
23922389
Expression item = items.get(index);
23932390
if ((this.selector == Ref) || ((this.selector == Deref) && (item.isObjectExpression()))) {
23942391
DatabaseTable alias = ((ObjectExpression)item).aliasForTable(((ObjectExpression)item).getDescriptor().getTables().firstElement());
@@ -2404,6 +2401,14 @@ public void printCollection(List<Expression> items, ExpressionSQLPrinter printer
24042401
}
24052402
}
24062403

2404+
private int[] arrayIndexSequence(int size) {
2405+
int[] result = new int[size];
2406+
for (int i = 0; i < size; i++) {
2407+
result[i] = i;
2408+
}
2409+
return result;
2410+
}
2411+
24072412
/**
24082413
* INTERNAL: Print the collection onto the SQL stream.
24092414
*/

foundation/org.eclipse.persistence.core/src/org/eclipse/persistence/internal/expressions/ArgumentListFunctionExpression.java

Lines changed: 5 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 1998, 2022 Oracle and/or its affiliates. All rights reserved.
2+
* Copyright (c) 1998, 2024 Oracle and/or its affiliates. All rights reserved.
33
* Copyright (c) 2021, 2022 IBM Corporation. All rights reserved.
44
*
55
* This program and the accompanying materials are made available under the
@@ -95,26 +95,14 @@ public void setOperator(ExpressionOperator theOperator) {
9595
* Print SQL
9696
*/
9797
public void printSQL(ExpressionSQLPrinter printer) {
98-
ListExpressionOperator realOperator;
99-
realOperator = (ListExpressionOperator)getPlatformOperator(printer.getPlatform());
100-
operator.copyTo(realOperator);
101-
((ListExpressionOperator) realOperator).setIsComplete(true);
102-
realOperator.printCollection(this.children, printer);
98+
ListExpressionOperator operator = (ListExpressionOperator) this.operator;
99+
100+
operator.setIsComplete(true);
101+
operator.printCollection(this.children, printer);
103102
}
104103

105104
@Override
106105
protected void postCopyIn(Map alreadyDone) {
107-
/*
108-
* Bug 463042: All ArgumentListFunctionExpression instances store the same operator reference.
109-
* Unfortunately, ListExpressionOperator.numberOfItems stores state. If multiple ArgumentListFunctionExpression
110-
* are run concurrently, then the ListExpressionOperator.numberOfItems state shared by all instances
111-
* becomes inconsistent. A solution is to make sure each ArgumentListFunctionExpression has a unique operator
112-
* reference.
113-
*/
114-
final ListExpressionOperator originalOperator = ((ListExpressionOperator) this.operator);
115-
this.operator = new ListExpressionOperator();
116-
originalOperator.copyTo(this.operator);
117-
118106
Boolean hasLastChildCopy = hasLastChild;
119107
hasLastChild = null;
120108
super.postCopyIn(alreadyDone);
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
/*
2+
* Copyright (c) 2024 Oracle and/or its affiliates. All rights reserved.
3+
* Copyright (c) 2024 IBM Corporation. All rights reserved.
4+
*
5+
* This program and the accompanying materials are made available under the
6+
* terms of the Eclipse Public License v. 2.0 which is available at
7+
* http://www.eclipse.org/legal/epl-2.0,
8+
* or the Eclipse Distribution License v. 1.0 which is available at
9+
* http://www.eclipse.org/org/documents/edl-v10.php.
10+
*
11+
* SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
12+
*/
13+
package org.eclipse.persistence.jpa.test.jpql;
14+
15+
16+
import java.util.ArrayList;
17+
import java.util.List;
18+
import java.util.concurrent.atomic.AtomicReference;
19+
import java.util.function.ObjIntConsumer;
20+
21+
import javax.persistence.EntityManager;
22+
import javax.persistence.EntityManagerFactory;
23+
24+
import org.eclipse.persistence.jpa.test.framework.DDLGen;
25+
import org.eclipse.persistence.jpa.test.framework.Emf;
26+
import org.eclipse.persistence.jpa.test.framework.EmfRunner;
27+
import org.eclipse.persistence.jpa.test.jpql.model.JPQLEntity;
28+
import org.junit.Test;
29+
import org.junit.runner.RunWith;
30+
31+
/**
32+
* This test reproduces the issues #2136, #1867 and #1717.
33+
*
34+
* @author Igor Mukhin
35+
*/
36+
@RunWith(EmfRunner.class)
37+
public class TestArgumentListFunctionExpressionConcurrency {
38+
39+
private static final int MAX_THREADS = Math.min(Runtime.getRuntime().availableProcessors(), 4);
40+
private static final int ITERATIONS_PER_THREAD = 1000;
41+
42+
@Emf(name = "argumentListFunctionExpressionConcurrencyEMF", createTables = DDLGen.DROP_CREATE, classes = { JPQLEntity.class })
43+
private EntityManagerFactory emf;
44+
45+
@Test
46+
public void testConcurrentUseOfCoalesce() throws Exception {
47+
runInParallel((em, i) -> {
48+
String jpql = "SELECT p FROM JPQLEntity p"
49+
+ " WHERE p.string1 = coalesce(p.string2, '" + cacheBuster(i) + "')";
50+
51+
em.createQuery(jpql, JPQLEntity.class).getResultList();
52+
});
53+
}
54+
55+
@Test
56+
public void testConcurrentUseOfCaseCondition() throws Exception {
57+
runInParallel((em, i) -> {
58+
String jpql = "SELECT p FROM JPQLEntity p"
59+
+ " WHERE p.string1 = case when p.string2 = '" + cacheBuster(i) + "' then null else p.string1 end";
60+
61+
em.createQuery(jpql, JPQLEntity.class).getResultList();
62+
});
63+
}
64+
65+
private static String cacheBuster(Integer i) {
66+
return "cacheBuster." + Thread.currentThread().getName() + "." + i;
67+
}
68+
69+
private void runInParallel(ObjIntConsumer<EntityManager> runnable) throws Exception {
70+
AtomicReference<Exception> exception = new AtomicReference<>();
71+
72+
// start all threads
73+
List<Thread> threads = new ArrayList<>();
74+
for (int t = 0; t < MAX_THREADS; t++) {
75+
Thread thread = new Thread(() -> {
76+
try {
77+
for (int i = 0; i < ITERATIONS_PER_THREAD; i++) {
78+
if (exception.get() != null) {
79+
return;
80+
}
81+
82+
EntityManager em = emf.createEntityManager();
83+
try {
84+
runnable.accept(em, i);
85+
} finally {
86+
em.close();
87+
}
88+
89+
}
90+
} catch (Exception e) {
91+
exception.set(e);
92+
}
93+
});
94+
threads.add(thread);
95+
thread.start();
96+
}
97+
98+
// wait for all threads to finish
99+
threads.forEach(thread -> {
100+
try {
101+
thread.join();
102+
} catch (InterruptedException e) {
103+
exception.set(e);
104+
}
105+
});
106+
107+
// throw the first exception that occurred
108+
if (exception.get() != null) {
109+
throw exception.get();
110+
}
111+
}
112+
}

0 commit comments

Comments
 (0)