Skip to content

Commit 4a7149f

Browse files
Fixes coalesce and case operators in multithreaded environments with different number of arguments (#2259)
The bug is described in the issue #2136 TestArgumentListFunctionExpressionConcurrency.java - adds tests that reproduce the bug. ArgumentListFunctionExpression.java - fixes printSQL to use the operator itself and not the common instance of it ExpressionOperator.java - fixes getArgumentIndices by disabling the caching of the dynamically created argument indexes. --------- Co-authored-by: Igor Mukhin <[email protected]>
1 parent 829f0e0 commit 4a7149f

File tree

3 files changed

+133
-4
lines changed

3 files changed

+133
-4
lines changed

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

+6-2
Original file line numberDiff line numberDiff line change
@@ -2618,7 +2618,7 @@ public void setArgumentIndices(int[] indices) {
26182618
}
26192619

26202620
/**
2621-
* Return the argumentIndices if set, otherwise initialize argumentIndices to the provided size
2621+
* Returns the argumentIndices if set, otherwise returns an array of indexes of the provided size.
26222622
*/
26232623
public int[] getArgumentIndices(int size) {
26242624
int[] indices = this.argumentIndices;
@@ -2630,7 +2630,11 @@ public int[] getArgumentIndices(int size) {
26302630
for (int i = 0; i < indices.length; i++) {
26312631
indices[i] = i;
26322632
}
2633-
this.argumentIndices = indices;
2633+
2634+
// NOTE: Why not cache the newly generated array of indexes like "this.argumentIndices = indices" here?
2635+
// The reason is that some operators have variable number of arguments like COALESCE and CASE WHEN.
2636+
// As instances of this class are shared between threads we cannot cache the array of indexes.
2637+
26342638
return indices;
26352639
}
26362640

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

+1-2
Original file line numberDiff line numberDiff line change
@@ -97,8 +97,7 @@ public void setOperator(ExpressionOperator theOperator) {
9797
*/
9898
@Override
9999
public void printSQL(ExpressionSQLPrinter printer) {
100-
ListExpressionOperator realOperator;
101-
realOperator = (ListExpressionOperator)getPlatformOperator(printer.getPlatform());
100+
ListExpressionOperator realOperator = (ListExpressionOperator) getPlatformOperator(printer.getPlatform());
102101
operator.copyTo(realOperator);
103102
realOperator.setIsComplete(true);
104103
realOperator.printCollection(this.children, printer);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
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 org.eclipse.persistence.jpa.test.framework.DDLGen;
22+
import org.eclipse.persistence.jpa.test.framework.Emf;
23+
import org.eclipse.persistence.jpa.test.framework.EmfRunner;
24+
import org.eclipse.persistence.jpa.test.jpql.model.JPQLEntity;
25+
import org.junit.Test;
26+
import org.junit.runner.RunWith;
27+
28+
import jakarta.persistence.EntityManager;
29+
import jakarta.persistence.EntityManagerFactory;
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;
49+
if (i % 2 == 0) {
50+
jpql = "SELECT p FROM JPQLEntity p WHERE p.string1 = coalesce(p.string2, '" + cacheBuster(i) + "')";
51+
} else {
52+
jpql = "SELECT p FROM JPQLEntity p WHERE p.string1 = coalesce(p.string2, p.string1, '" + cacheBuster(i) + "')";
53+
}
54+
em.createQuery(jpql, JPQLEntity.class).getResultList();
55+
System.out.println(Thread.currentThread().getName() + " - " + i % 2);
56+
});
57+
}
58+
59+
@Test
60+
public void testConcurrentUseOfCaseCondition() throws Exception {
61+
runInParallel((em, i) -> {
62+
String jpql;
63+
if (i % 2 == 0) {
64+
jpql = "SELECT p FROM JPQLEntity p"
65+
+ " WHERE p.string1 = case "
66+
+ " when p.string2 = '" + cacheBuster(i) + "' then p.string1 "
67+
+ " else null "
68+
+ " end";
69+
} else {
70+
jpql = "SELECT p FROM JPQLEntity p"
71+
+ " WHERE p.string1 = case "
72+
+ " when p.string2 = '" + cacheBuster(i) + "' then p.string1"
73+
+ " when p.string2 = 'x' then p.string2"
74+
+ " else null "
75+
+ " end";
76+
77+
}
78+
em.createQuery(jpql, JPQLEntity.class).getResultList();
79+
});
80+
}
81+
82+
private static String cacheBuster(Integer i) {
83+
return "cacheBuster." + Thread.currentThread().getName() + "." + i;
84+
}
85+
86+
private void runInParallel(ObjIntConsumer<EntityManager> runnable) throws Exception {
87+
AtomicReference<Exception> exception = new AtomicReference<>();
88+
89+
// start all threads
90+
List<Thread> threads = new ArrayList<>();
91+
for (int t = 0; t < MAX_THREADS; t++) {
92+
Thread thread = new Thread(() -> {
93+
try {
94+
for (int i = 0; i < ITERATIONS_PER_THREAD; i++) {
95+
if (exception.get() != null) {
96+
return;
97+
}
98+
99+
try (EntityManager em = emf.createEntityManager()) {
100+
runnable.accept(em, i);
101+
}
102+
103+
}
104+
} catch (Exception e) {
105+
exception.set(e);
106+
}
107+
});
108+
threads.add(thread);
109+
thread.start();
110+
}
111+
112+
// wait for all threads to finish
113+
threads.forEach(thread -> {
114+
try {
115+
thread.join();
116+
} catch (InterruptedException e) {
117+
exception.set(e);
118+
}
119+
});
120+
121+
// throw the first exception that occurred
122+
if (exception.get() != null) {
123+
throw exception.get();
124+
}
125+
}
126+
}

0 commit comments

Comments
 (0)