HHH-14444 Test concurrent usage of ID generator optimizers
Signed-off-by: Yoann Rodière <yoann@hibernate.org>
This commit is contained in:
parent
a094e17d2a
commit
04a40f8397
|
@ -0,0 +1,188 @@
|
|||
/*
|
||||
* Hibernate, Relational Persistence for Idiomatic Java
|
||||
*
|
||||
* License: GNU Lesser General Public License (LGPL), version 2.1 or later.
|
||||
* See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
|
||||
*/
|
||||
package org.hibernate.id.enhanced;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.hibernate.AssertionFailure;
|
||||
|
||||
import org.hibernate.testing.junit4.BaseUnitTestCase;
|
||||
import org.hibernate.testing.junit4.CustomParameterized;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.Parameterized;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatCode;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
@RunWith(CustomParameterized.class)
|
||||
public class OptimizerConcurrencyUnitTest extends BaseUnitTestCase {
|
||||
|
||||
@Parameterized.Parameters(name = "{0}")
|
||||
public static List<Object[]> params() {
|
||||
List<Object[]> params = new ArrayList<>();
|
||||
for ( StandardOptimizerDescriptor value : StandardOptimizerDescriptor.values() ) {
|
||||
params.add( new Object[] { value } );
|
||||
}
|
||||
return params;
|
||||
}
|
||||
|
||||
private final StandardOptimizerDescriptor optimizerDescriptor;
|
||||
|
||||
public OptimizerConcurrencyUnitTest(StandardOptimizerDescriptor optimizerDescriptor) {
|
||||
this.optimizerDescriptor = optimizerDescriptor;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConcurrentUsage_singleTenancy() throws InterruptedException {
|
||||
final int increment = 50;
|
||||
final int taskCount = 100 * increment;
|
||||
|
||||
Optimizer optimizer = buildOptimizer( 1, increment );
|
||||
|
||||
List<Callable<Long>> tasks = new ArrayList<>();
|
||||
|
||||
SourceMock sequence = new SourceMock( 1, increment );
|
||||
assertEquals( 0, sequence.getTimesCalled() );
|
||||
assertEquals( -1, sequence.getCurrentValue() );
|
||||
|
||||
for ( int i = 0; i < taskCount; i++ ) {
|
||||
tasks.add( new Callable<Long>() {
|
||||
@Override
|
||||
public Long call() throws Exception {
|
||||
return ( Long ) optimizer.generate( sequence );
|
||||
}
|
||||
} );
|
||||
}
|
||||
|
||||
ExecutorService executor = Executors.newFixedThreadPool( 10 );
|
||||
List<Future<Long>> futures;
|
||||
try {
|
||||
futures = executor.invokeAll( tasks );
|
||||
executor.shutdown();
|
||||
executor.awaitTermination( 10, TimeUnit.SECONDS );
|
||||
}
|
||||
finally {
|
||||
executor.shutdownNow();
|
||||
}
|
||||
|
||||
assertThat( futures )
|
||||
.allSatisfy( future -> {
|
||||
assertThat( future ).isDone();
|
||||
assertThatCode( future::get ).doesNotThrowAnyException();
|
||||
} );
|
||||
List<Long> generated = futures.stream().map( this::getDoneFutureValue ).collect( Collectors.toList());
|
||||
assertThat( generated )
|
||||
.hasSize( taskCount )
|
||||
// Check for uniqueness
|
||||
.containsExactlyInAnyOrderElementsOf( new HashSet<>( generated ) );
|
||||
System.out.println( "Generated IDs: " + generated );
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConcurrentUsage_multiTenancy() throws InterruptedException {
|
||||
final int increment = 50;
|
||||
|
||||
final int tenantCount = 5;
|
||||
final int taskCountPerTenant = 20 * increment;
|
||||
|
||||
Optimizer optimizer = buildOptimizer( 1, increment );
|
||||
|
||||
Map<String, List<Callable<Long>>> tasksByTenantId = new LinkedHashMap<>();
|
||||
|
||||
for ( int i = 0; i < tenantCount; i++ ) {
|
||||
String tenantId = "tenant#" + i;
|
||||
|
||||
SourceMock sequenceForTenant = new SourceMock( tenantId, 1, increment );
|
||||
assertEquals( 0, sequenceForTenant.getTimesCalled() );
|
||||
assertEquals( -1, sequenceForTenant.getCurrentValue() );
|
||||
|
||||
List<Callable<Long>> tasksForTenant = new ArrayList<>();
|
||||
tasksByTenantId.put( tenantId, tasksForTenant );
|
||||
for ( int j = 0; j < taskCountPerTenant; j++ ) {
|
||||
tasksForTenant.add( new Callable<Long>() {
|
||||
@Override
|
||||
public Long call() throws Exception {
|
||||
return ( Long ) optimizer.generate( sequenceForTenant );
|
||||
}
|
||||
} );
|
||||
}
|
||||
}
|
||||
|
||||
List<Callable<Long>> tasks = new ArrayList<>();
|
||||
// Make sure to interleave tenants
|
||||
for ( int i = 0; i < taskCountPerTenant; i++ ) {
|
||||
for ( List<Callable<Long>> tasksForTenant : tasksByTenantId.values() ) {
|
||||
tasks.add( tasksForTenant.get( i ) );
|
||||
}
|
||||
}
|
||||
|
||||
ExecutorService executor = Executors.newFixedThreadPool( 10 );
|
||||
List<Future<Long>> futures;
|
||||
try {
|
||||
futures = executor.invokeAll( tasks );
|
||||
executor.shutdown();
|
||||
executor.awaitTermination( 10, TimeUnit.SECONDS );
|
||||
}
|
||||
finally {
|
||||
executor.shutdownNow();
|
||||
}
|
||||
|
||||
assertThat( futures )
|
||||
.allSatisfy( future -> {
|
||||
assertThat( future ).isDone();
|
||||
assertThatCode( future::get ).doesNotThrowAnyException();
|
||||
} );
|
||||
|
||||
Map<String, List<Future<Long>>> futuresByTenantId = new LinkedHashMap<>();
|
||||
for ( int i = 0; i < tenantCount; i++ ) {
|
||||
List<Future<Long>> futuresForTenant = new ArrayList<>();
|
||||
for ( int j = 0; j < taskCountPerTenant; j++ ) {
|
||||
futuresForTenant.add( futures.get( i + j * tenantCount ) );
|
||||
}
|
||||
String tenantId = "tenant#" + i;
|
||||
futuresByTenantId.put( tenantId, futuresForTenant );
|
||||
}
|
||||
|
||||
for ( Map.Entry<String, List<Future<Long>>> entry : futuresByTenantId.entrySet() ) {
|
||||
List<Long> generated = entry.getValue().stream().map( this::getDoneFutureValue )
|
||||
.collect( Collectors.toList());
|
||||
assertThat( generated )
|
||||
.hasSize( taskCountPerTenant )
|
||||
// Check for uniqueness
|
||||
.containsExactlyInAnyOrderElementsOf( new HashSet<>( generated ) );
|
||||
System.out.println( "Generated IDs for '" + entry.getKey() + "': " + generated );
|
||||
}
|
||||
}
|
||||
|
||||
private Optimizer buildOptimizer(long initial, int increment) {
|
||||
return OptimizerFactory.buildOptimizer( optimizerDescriptor.getExternalName(), Long.class, increment, initial );
|
||||
}
|
||||
|
||||
private <R> R getDoneFutureValue(Future<R> future) {
|
||||
try {
|
||||
return future.get(0, TimeUnit.SECONDS);
|
||||
}
|
||||
catch (InterruptedException | ExecutionException | TimeoutException e) {
|
||||
throw new AssertionFailure( "Unexpected Future state", e );
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -6,12 +6,8 @@
|
|||
*/
|
||||
package org.hibernate.id.enhanced;
|
||||
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Test;
|
||||
|
||||
import org.hibernate.id.IdentifierGeneratorHelper;
|
||||
import org.hibernate.id.IntegralDataTypeHolder;
|
||||
import org.hibernate.testing.junit4.BaseUnitTestCase;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
|
@ -321,64 +317,4 @@ public class OptimizerUnitTest extends BaseUnitTestCase {
|
|||
return OptimizerFactory.buildOptimizer( descriptor.getExternalName(), Long.class, increment, initial );
|
||||
}
|
||||
|
||||
private static class SourceMock implements AccessCallback {
|
||||
private IdentifierGeneratorHelper.BasicHolder value = new IdentifierGeneratorHelper.BasicHolder( Long.class );
|
||||
private long initialValue;
|
||||
private int increment;
|
||||
private int timesCalled = 0;
|
||||
|
||||
public SourceMock(long initialValue) {
|
||||
this( initialValue, 1 );
|
||||
}
|
||||
|
||||
public SourceMock(long initialValue, int increment) {
|
||||
this( initialValue, increment, 0 );
|
||||
}
|
||||
|
||||
public SourceMock(long initialValue, int increment, int timesCalled) {
|
||||
this.increment = increment;
|
||||
this.timesCalled = timesCalled;
|
||||
if ( timesCalled != 0 ) {
|
||||
this.value.initialize( initialValue );
|
||||
this.initialValue = 1;
|
||||
}
|
||||
else {
|
||||
this.value.initialize( -1 );
|
||||
this.initialValue = initialValue;
|
||||
}
|
||||
}
|
||||
|
||||
public IntegralDataTypeHolder getNextValue() {
|
||||
try {
|
||||
if ( timesCalled == 0 ) {
|
||||
initValue();
|
||||
return value.copy();
|
||||
}
|
||||
else {
|
||||
return value.add( increment ).copy();
|
||||
}
|
||||
}
|
||||
finally {
|
||||
timesCalled++;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTenantIdentifier() {
|
||||
return null;
|
||||
}
|
||||
|
||||
private void initValue() {
|
||||
this.value.initialize( initialValue );
|
||||
}
|
||||
|
||||
public int getTimesCalled() {
|
||||
return timesCalled;
|
||||
}
|
||||
|
||||
public long getCurrentValue() {
|
||||
return value == null ? -1 : value.getActualLongValue();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,79 @@
|
|||
/*
|
||||
* Hibernate, Relational Persistence for Idiomatic Java
|
||||
*
|
||||
* License: GNU Lesser General Public License (LGPL), version 2.1 or later
|
||||
* See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html
|
||||
*/
|
||||
package org.hibernate.id.enhanced;
|
||||
|
||||
import org.hibernate.id.IdentifierGeneratorHelper;
|
||||
import org.hibernate.id.IntegralDataTypeHolder;
|
||||
|
||||
class SourceMock implements AccessCallback {
|
||||
private final String tenantId;
|
||||
private final long initialValue;
|
||||
private final int increment;
|
||||
private volatile long currentValue;
|
||||
private volatile int timesCalled;
|
||||
|
||||
public SourceMock(long initialValue) {
|
||||
this( initialValue, 1 );
|
||||
}
|
||||
|
||||
public SourceMock(long initialValue, int increment) {
|
||||
this( null, initialValue, increment );
|
||||
}
|
||||
|
||||
public SourceMock(String tenantId, long initialValue, int increment) {
|
||||
this( tenantId, initialValue, increment, 0 );
|
||||
}
|
||||
|
||||
public SourceMock(long initialValue, int increment, int timesCalled) {
|
||||
this( null, initialValue, increment, timesCalled );
|
||||
}
|
||||
|
||||
public SourceMock(String tenantId, long initialValue, int increment, int timesCalled) {
|
||||
this.tenantId = tenantId;
|
||||
this.increment = increment;
|
||||
this.timesCalled = timesCalled;
|
||||
if ( timesCalled != 0 ) {
|
||||
this.currentValue = initialValue;
|
||||
this.initialValue = 1;
|
||||
}
|
||||
else {
|
||||
this.currentValue = -1;
|
||||
this.initialValue = initialValue;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized IntegralDataTypeHolder getNextValue() {
|
||||
try {
|
||||
if ( timesCalled == 0 ) {
|
||||
currentValue = initialValue;
|
||||
}
|
||||
else {
|
||||
currentValue += increment;
|
||||
}
|
||||
IdentifierGeneratorHelper.BasicHolder result = new IdentifierGeneratorHelper.BasicHolder( Long.class );
|
||||
result.initialize( currentValue );
|
||||
return result;
|
||||
}
|
||||
finally {
|
||||
++timesCalled;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTenantIdentifier() {
|
||||
return tenantId;
|
||||
}
|
||||
|
||||
public int getTimesCalled() {
|
||||
return timesCalled;
|
||||
}
|
||||
|
||||
public long getCurrentValue() {
|
||||
return currentValue;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue