HHH-14444 Test concurrent usage of ID generator optimizers

Signed-off-by: Yoann Rodière <yoann@hibernate.org>
This commit is contained in:
Yoann Rodière 2021-02-05 17:23:48 +01:00 committed by Sanne Grinovero
parent a094e17d2a
commit 04a40f8397
3 changed files with 268 additions and 65 deletions

View File

@ -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 );
}
}
}

View File

@ -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();
}
}
}

View File

@ -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;
}
}