blob: e22a34e7b636c67492254b73c16a3106cb7a9b34 [file] [log] [blame]
/*
* 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
* http://www.eclipse.org/legal/epl-2.0,
* or the Eclipse Distribution License v. 1.0 which is available at
* http://www.eclipse.org/org/documents/edl-v10.php.
*
* SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
*/
// Contributors:
// 03/14/2018-2.7 Will Dazey
// - 500753: Synchronize initialization of InsertQuery
package org.eclipse.persistence.jpa.test.concurrency;
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 jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import jakarta.persistence.EntityTransaction;
import jakarta.persistence.RollbackException;
import org.eclipse.persistence.jpa.test.concurrency.model.User;
import org.eclipse.persistence.jpa.test.concurrency.model.UserTag;
import org.eclipse.persistence.jpa.test.framework.DDLGen;
import org.eclipse.persistence.jpa.test.framework.Emf;
import org.eclipse.persistence.jpa.test.framework.EmfRunner;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
@RunWith(EmfRunner.class)
public class TestConcurrencyPersistence {
@Emf(createTables = DDLGen.DROP_CREATE, classes = { User.class, UserTag.class })
private EntityManagerFactory emf;
/**
* Persisting multiple entities simultaneously on separate threads has the possibility of
* causing a null descriptor to be set on the InsertQuery.
* This test spins up threads and runs them multiple times to replicate the race condition.
* This test should pass everytime it's run, but there exists the possibility that it
* passes when none of the threads actually test the collision.
*
*/
@Test
public void testInsertConcurrency() throws Exception {
Queue<Exception> errors = new ConcurrentLinkedQueue<>();
int runs = 10;
for(int run = 0; run < runs; run++) {
int threadCount = 3;
CountDownLatch startSignal = new CountDownLatch(1);
CountDownLatch doneSignal = new CountDownLatch(threadCount);
for (int i = 0; i < threadCount; i++) {
User user = new User((1000*run) + i);
user.getTags().add(new UserTag("k1", "v1"));
new PersistWorker(startSignal, doneSignal, errors, user).start();
}
//start all threads running
startSignal.countDown();
try {
//wait until all threads complete
boolean fin = doneSignal.await(10, TimeUnit.SECONDS);
Assert.assertTrue("Timeout occurred while running test", fin);
} catch (InterruptedException e) {
// ignore
}
}
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());
}
/**
* Thread class that persists passed Object.
* Aligns transaction commit with other threads on the 'startSignal' latch.
* Signals the 'doneSignal' latch when complete
* Logs any exceptions in the passed Queue.
*/
private final class PersistWorker extends Thread {
private final Queue<Exception> errors;
private final CountDownLatch startSignal;
private final CountDownLatch doneSignal;
private final Object obj;
PersistWorker(CountDownLatch startSignal, CountDownLatch doneSignal, Queue<Exception> errors, Object obj) {
this.errors = errors;
this.startSignal = startSignal;
this.doneSignal = doneSignal;
this.obj = obj;
}
@Override
public void run() {
EntityManager em = emf.createEntityManager();
try {
try {
emf.getMetamodel().entity(obj.getClass());
} catch (IllegalArgumentException ie) {
this.errors.add(ie);
return;
}
EntityTransaction et = em.getTransaction();
et.begin();
em.persist(this.obj);
//block thread here until all others are ready
this.startSignal.await();
et.commit();
} catch (RollbackException re) {
this.errors.add(re);
} catch (InterruptedException e) {
// ignore
} finally {
em.close();
//notify that this thread has completed
this.doneSignal.countDown();
}
}
}
}