blob: 6aa0986bfb36586d88a6dd0cf6db7ce9a15bd377 [file] [log] [blame]
/*
* Copyright (c) 1998, 2021 Oracle and/or its affiliates. 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:
// Oracle - initial API and implementation from Oracle TopLink
package org.eclipse.persistence.testing.tests.feature;
import java.util.Comparator;
import java.util.Arrays;
import org.eclipse.persistence.internal.sessions.AbstractSession;
import org.eclipse.persistence.logging.SessionLog;
import org.eclipse.persistence.sessions.*;
import org.eclipse.persistence.sessions.server.Server;
import org.eclipse.persistence.testing.framework.TestCase;
import org.eclipse.persistence.testing.models.employee.domain.Employee;
/**
* This testcase test the thread-safeness of TopLink's sequencing. The type of sequencing specified in the login
* info is tested (either native or table sequencing is used). To test the other kind of sequencing you must logout,
* change you login info, login, and rerun the test.
* The test start a number of threads. Each thread generates a number of sequence numbers. For this test the
* preallocation size is set to 1. This maximizes the concurrency of the threads and increases the effectiveness
* of this test.
* The choice of number of threads and iterations must be large enough for the test to be effective. I got this test
* to fail (with the 2.5.1.1 sequences bug) with 5 threads and 50 iterations.
*
* Caveat: This test cannot prove that sequencing is thread safe, it can only prove that it is not.
*
* author: Robert Campbell
*/
public class SequencingConcurrencyTest extends TestCase implements Comparator {
public java.util.Vector sequences;
public SequencingConcurrencyTest[] tests;
public int nThreads;
public boolean useServerSession;
public boolean useSeparateConnection;
public Server serverSession;
public DatabaseSession dbSession;
public int previousSequencePreallocationSize;
public int sequencePreallocationSize;
public boolean useSeparateConnectionOriginal;
public boolean shouldLogMessages;
public int originalLogLevel;
public int nIterations;
public Session session;
public int threadNumber;
public boolean handleException;
public Exception exception;
protected SequencingConcurrencyTest(int threadNumber, int nIterations, java.util.Vector sequences, Session session, boolean handleException) {
this.threadNumber = threadNumber;
this.nIterations = nIterations;
this.sequences = sequences;
this.session = session;
this.handleException = handleException;
}
/**
* To be called by testing framework.
*/
public SequencingConcurrencyTest(int nThreads, int nIterations, boolean useServerSession, boolean useSeparateConnection) {
this(nThreads, nIterations, useServerSession, useSeparateConnection, 1);
}
/**
* To be called by testing framework.
*/
public SequencingConcurrencyTest(int nThreads, int nIterations, boolean useServerSession, boolean useSeparateConnection, int sequencePreallocationSize) {
this.nThreads = nThreads;
this.nIterations = nIterations;
this.useServerSession = useServerSession;
this.useSeparateConnection = useSeparateConnection;
String sessionUsed;
if (useServerSession) {
sessionUsed = "ServerSession";
} else {
sessionUsed = "DatabaseSession";
}
shouldLogMessages = false;
this.sequencePreallocationSize = sequencePreallocationSize;
setName(getName() + " " + sessionUsed + " separateConnection=" + useSeparateConnection + " seqPreallocSize=" + sequencePreallocationSize + " threads=" + nThreads + " iterations=" + nIterations);
}
/**
* Compare the two BigDecimal, for using TOPSort.
*/
@Override
public int compare(Object b1, Object b2) {
java.math.BigDecimal big1 = new java.math.BigDecimal(((Number)b1).longValue());
java.math.BigDecimal big2 = new java.math.BigDecimal(((Number)b2).longValue());
return big1.compareTo(big2);
}
/**
* Sets the logging and the sequence preallocation size the way it was.
*/
@Override
public void reset() {
if (useServerSession) {
if (serverSession != null) {
serverSession.logout();
serverSession = null;
}
} else {
if (dbSession != null) {
// dbSession.getSequencingControl().setPreallocationSize(this.previousSequencePreallocationSize);
dbSession.getSequencingControl().initializePreallocated();
if (useSeparateConnection != useSeparateConnectionOriginal) {
dbSession.getSequencingControl().setShouldUseSeparateConnection(useSeparateConnectionOriginal);
dbSession.getSequencingControl().resetSequencing();
}
dbSession.setLogLevel(originalLogLevel);
dbSession = null;
}
}
}
public Runnable runnable() {
return new Runnable() {
/**
* Each thread does this: gets nIterations number of sequence numbers and puts them in a thread specific array.
*/
@Override
public void run() {
// Test
Number[] sequence = (Number[])sequences.elementAt(threadNumber);
try {
if (handleException) {
for (int i = 0; i < nIterations; i++) {
try {
sequence[i] = (Number)((AbstractSession)session).getSequencing().getNextValue(org.eclipse.persistence.testing.models.employee.domain.Employee.class);
} catch (org.eclipse.persistence.exceptions.ConcurrencyException ex) {
if (ex.getErrorCode() == org.eclipse.persistence.exceptions.ConcurrencyException.SEQUENCING_MULTITHREAD_THRU_CONNECTION) {
// that's an acceptable exception, try again.
i--;
} else {
throw ex;
}
}
}
} else {
for (int i = 0; i < nIterations; i++) {
sequence[i] = (Number)((AbstractSession)session).getSequencing().getNextValue(org.eclipse.persistence.testing.models.employee.domain.Employee.class);
}
}
} catch (Exception ex2) {
exception = ex2;
} finally {
if (session.isClientSession()) {
session.release();
}
}
}
};
}
@Override
public void setup() {
if (getAbstractSession().getDescriptor(Employee.class).getSequence().shouldAcquireValueAfterInsert()) {
throw new org.eclipse.persistence.testing.framework.TestWarningException("Not a valid test against databases where the native sequencing is done entirely in the database.");
}
// Setup
dbSession = (DatabaseSession)getSession();
if (useServerSession) {
int numConnections = java.lang.Math.min(nThreads, 5);
serverSession = new Project(getSession().getDatasourceLogin().clone()).createServerSession(numConnections, numConnections);
serverSession.addDescriptors(new org.eclipse.persistence.testing.models.employee.relational.EmployeeProject());
// serverSession.getSequencingControl().setPreallocationSize(sequencePreallocationSize);
serverSession.getSequencingControl().setShouldUseSeparateConnection(useSeparateConnection);
serverSession.setSessionLog(getSession().getSessionLog());
if (shouldLogMessages) {
serverSession.setLogLevel(SessionLog.FINE);
}
serverSession.login();
} else {
originalLogLevel = dbSession.getLogLevel();
if (shouldLogMessages) {
dbSession.setLogLevel(SessionLog.FINE);
}
// this.previousSequencePreallocationSize = dbSession.getSequencingControl().getPreallocationSize();
// dbSession.getSequencingControl().setPreallocationSize(sequencePreallocationSize);
dbSession.getSequencingControl().resetSequencing();
dbSession.getSequencingControl().initializePreallocated();
useSeparateConnectionOriginal = dbSession.getSequencingControl().shouldUseSeparateConnection();
if (useSeparateConnection != useSeparateConnectionOriginal) {
dbSession.getSequencingControl().setShouldUseSeparateConnection(useSeparateConnection);
dbSession.getSequencingControl().resetSequencing();
}
}
// Setup the arrays of BigDecimals to be filled.
sequences = new java.util.Vector(nThreads);
for (int i = 0; i < nThreads; i++) {
sequences.addElement(new Number[nIterations]);
}
}
/**
* Start each thread, then wait until all of them are finished.
*/
@Override
public void test() {
//
Thread[] threads = new Thread[nThreads];
tests = new SequencingConcurrencyTest[nThreads];
Session writeSession = getSession();
boolean handleException = !useServerSession && !useSeparateConnection;
for (int i = 0; i < nThreads; i++) {
if (useServerSession) {
writeSession = serverSession.acquireClientSession();
}
tests[i] = new SequencingConcurrencyTest(i, nIterations, sequences, writeSession, handleException);
threads[i] = new Thread(tests[i].runnable());
threads[i].start();
}
// Join with all the threads so we don't proceed until all the threads have finished.
for (int i = 0; i < nThreads; i++) {
try {
threads[i].join();
} catch (Exception ex) {
throw new org.eclipse.persistence.testing.framework.TestErrorException(ex.getMessage());
}
}
}
/**
* Make sure that the sequence numbers generated contained no gaps or duplicates.
*/
@Override
public void verify() {
// Verify
for (int i = 0; i < nThreads; i++) {
if (tests[i].exception != null) {
throw new org.eclipse.persistence.testing.framework.TestErrorException("exception in thread " + i + ", session(" + System.identityHashCode(session) + ") ", tests[i].exception);
}
}
// Put all the sequences into one big array.
Number[] big = new Number[nThreads * nIterations];
for (int i = 0; i < nThreads; i++) {
System.arraycopy(sequences.elementAt(i), 0, big, i * nIterations, nIterations);
}
try {
// sort the array.
Arrays.sort(big, this);
// Verify that there are no duplicates or gaps in the array.
Number previous = big[0];
for (int i = 1; i < (nIterations * nThreads); i++) {
Number current = big[i];
if ((previous.intValue() + 1) != current.intValue()) {
throw new org.eclipse.persistence.testing.framework.TestErrorException("Gap in sequencing, or incorrect sequences generated.");
}
previous = current;
}
} catch (Exception ex) {
throw new org.eclipse.persistence.testing.framework.TestErrorException(ex.getMessage());
}
}
}