From e82cd80d77576201b36f64337e2e6fac22db7b5f Mon Sep 17 00:00:00 2001 From: Will Dazey Date: Wed, 10 Nov 2021 15:25:58 -0600 Subject: [PATCH] Bug 463042: Concurrency issue with Case expression operator Signed-off-by: Will Dazey signed-off-by: Patrick Haller --- .../ArgumentListFunctionExpression.java | 16 ++++-- .../TestConcurrencyPersistence.java | 51 ++++++++++++++++++- .../jpa/test/concurrency/model/User.java | 6 ++- 3 files changed, 66 insertions(+), 7 deletions(-) diff --git a/foundation/org.eclipse.persistence.core/src/org/eclipse/persistence/internal/expressions/ArgumentListFunctionExpression.java b/foundation/org.eclipse.persistence.core/src/org/eclipse/persistence/internal/expressions/ArgumentListFunctionExpression.java index 91bbad7d4b9..92b80ab561a 100644 --- a/foundation/org.eclipse.persistence.core/src/org/eclipse/persistence/internal/expressions/ArgumentListFunctionExpression.java +++ b/foundation/org.eclipse.persistence.core/src/org/eclipse/persistence/internal/expressions/ArgumentListFunctionExpression.java @@ -1,5 +1,6 @@ /* - * Copyright (c) 1998, 2018 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1998, 2021 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2021 IBM Corporation. All rights reserved. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v. 2.0 which is available at @@ -102,10 +103,19 @@ public void printSQL(ExpressionSQLPrinter printer) { operator.printCollection(getChildren(), printer); } - @Override protected void postCopyIn(Map alreadyDone) { - ((ListExpressionOperator)operator).setNumberOfItems(0); + /* + * Bug 463042: All ArgumentListFunctionExpression instances store the same operator reference. + * Unfortunately, ListExpressionOperator.numberOfItems stores state. If multiple ArgumentListFunctionExpression + * are run concurrently, then the ListExpressionOperator.numberOfItems state shared by all instances + * becomes inconsistent. A solution is to make sure each ArgumentListFunctionExpression has a unique operator + * reference. + */ + final ListExpressionOperator originalOperator = ((ListExpressionOperator) this.operator); + this.operator = new ListExpressionOperator(); + originalOperator.copyTo(this.operator); + Boolean hasLastChildCopy = hasLastChild; hasLastChild = null; super.postCopyIn(alreadyDone); diff --git a/jpa/eclipselink.jpa.test.jse/src/org/eclipse/persistence/jpa/test/concurrency/TestConcurrencyPersistence.java b/jpa/eclipselink.jpa.test.jse/src/org/eclipse/persistence/jpa/test/concurrency/TestConcurrencyPersistence.java index 1e79eff5684..1612c679d3a 100644 --- a/jpa/eclipselink.jpa.test.jse/src/org/eclipse/persistence/jpa/test/concurrency/TestConcurrencyPersistence.java +++ b/jpa/eclipselink.jpa.test.jse/src/org/eclipse/persistence/jpa/test/concurrency/TestConcurrencyPersistence.java @@ -1,6 +1,6 @@ /* - * Copyright (c) 2018 Oracle and/or its affiliates. All rights reserved. - * Copyright (c) 2018 IBM Corporation. All rights reserved. + * Copyright (c) 2018, 2021 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 2021 IBM Corporation. All rights reserved. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v. 2.0 which is available at @@ -19,7 +19,10 @@ import java.util.Queue; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; import javax.persistence.EntityManager; import javax.persistence.EntityManagerFactory; @@ -76,7 +79,51 @@ public void testInsertConcurrency() throws Exception { } Assert.assertTrue(errors.toString(), errors.isEmpty()); + } + + /** + * Bug 463042: Executing the same query simultaneously on separate threads has the possibility of + * causing an ArrayOutOfBoundsException to be thrown. This test spins up multiple threads, executes + * the same query on each and validates that none of the threads failed. + * + * @throws Exception + */ + @Test + public void testCaseExpressionOperatorConcurrency() throws Exception { + final AtomicInteger count = new AtomicInteger(); + final AtomicInteger error = new AtomicInteger(); + + final int threads = 100; + final ExecutorService taskExecutor = Executors.newFixedThreadPool(threads); + + // Spawn 100 threads + for (int i = 0; i < threads; i++) { + taskExecutor.execute(new Runnable() { + public void run() { + count.incrementAndGet(); + + final EntityManager em = emf.createEntityManager(); + try { + // Executing the Query + em.createNamedQuery("CONCURR_CASE_QUERY", Integer.class).setParameter("id", 1).getSingleResult(); + } catch (Exception e) { + error.incrementAndGet(); + System.out.println(e.getMessage()); + } finally { + if (em != null) { + if (em.getTransaction().isActive()) { + em.getTransaction().rollback(); + } + em.close(); + } + } + } + }); + } + taskExecutor.shutdown(); + taskExecutor.awaitTermination(5, TimeUnit.SECONDS); + Assert.assertEquals("Expected no failures, but " + error.intValue() + "/" + count.intValue() + " threads failed", 0, error.intValue()); } /** diff --git a/jpa/eclipselink.jpa.test.jse/src/org/eclipse/persistence/jpa/test/concurrency/model/User.java b/jpa/eclipselink.jpa.test.jse/src/org/eclipse/persistence/jpa/test/concurrency/model/User.java index a664a69e68b..067d8ef69dd 100644 --- a/jpa/eclipselink.jpa.test.jse/src/org/eclipse/persistence/jpa/test/concurrency/model/User.java +++ b/jpa/eclipselink.jpa.test.jse/src/org/eclipse/persistence/jpa/test/concurrency/model/User.java @@ -1,6 +1,6 @@ /* - * Copyright (c) 2018 Oracle and/or its affiliates. All rights reserved. - * Copyright (c) 2018 IBM Corporation. All rights reserved. + * Copyright (c) 2018, 2021 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 2021 IBM Corporation. All rights reserved. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v. 2.0 which is available at @@ -25,10 +25,12 @@ import javax.persistence.FetchType; import javax.persistence.Id; import javax.persistence.JoinColumn; +import javax.persistence.NamedQuery; import javax.persistence.Table; @Entity @Table(name="CONCURR_USER") +@NamedQuery(name = "CONCURR_CASE_QUERY", query = "SELECT CASE WHEN (COUNT(e) > 0) THEN true ELSE false END FROM User e WHERE e.id = :id") public class User { @Id private int id;