From 04a40f839798886351ce7212c741c9832abd4738 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yoann=20Rodi=C3=A8re?= Date: Fri, 5 Feb 2021 17:23:48 +0100 Subject: [PATCH] HHH-14444 Test concurrent usage of ID generator optimizers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Yoann Rodière --- .../OptimizerConcurrencyUnitTest.java | 188 ++++++++++++++++++ .../id/enhanced/OptimizerUnitTest.java | 66 +----- .../org/hibernate/id/enhanced/SourceMock.java | 79 ++++++++ 3 files changed, 268 insertions(+), 65 deletions(-) create mode 100644 hibernate-core/src/test/java/org/hibernate/id/enhanced/OptimizerConcurrencyUnitTest.java create mode 100644 hibernate-core/src/test/java/org/hibernate/id/enhanced/SourceMock.java diff --git a/hibernate-core/src/test/java/org/hibernate/id/enhanced/OptimizerConcurrencyUnitTest.java b/hibernate-core/src/test/java/org/hibernate/id/enhanced/OptimizerConcurrencyUnitTest.java new file mode 100644 index 0000000000..9757e95048 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/id/enhanced/OptimizerConcurrencyUnitTest.java @@ -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 . + */ +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 params() { + List 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> 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() { + @Override + public Long call() throws Exception { + return ( Long ) optimizer.generate( sequence ); + } + } ); + } + + ExecutorService executor = Executors.newFixedThreadPool( 10 ); + List> 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 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>> 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> tasksForTenant = new ArrayList<>(); + tasksByTenantId.put( tenantId, tasksForTenant ); + for ( int j = 0; j < taskCountPerTenant; j++ ) { + tasksForTenant.add( new Callable() { + @Override + public Long call() throws Exception { + return ( Long ) optimizer.generate( sequenceForTenant ); + } + } ); + } + } + + List> tasks = new ArrayList<>(); + // Make sure to interleave tenants + for ( int i = 0; i < taskCountPerTenant; i++ ) { + for ( List> tasksForTenant : tasksByTenantId.values() ) { + tasks.add( tasksForTenant.get( i ) ); + } + } + + ExecutorService executor = Executors.newFixedThreadPool( 10 ); + List> 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>> futuresByTenantId = new LinkedHashMap<>(); + for ( int i = 0; i < tenantCount; i++ ) { + List> 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>> entry : futuresByTenantId.entrySet() ) { + List 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 getDoneFutureValue(Future future) { + try { + return future.get(0, TimeUnit.SECONDS); + } + catch (InterruptedException | ExecutionException | TimeoutException e) { + throw new AssertionFailure( "Unexpected Future state", e ); + } + } + +} diff --git a/hibernate-core/src/test/java/org/hibernate/id/enhanced/OptimizerUnitTest.java b/hibernate-core/src/test/java/org/hibernate/id/enhanced/OptimizerUnitTest.java index 808b3ed1c7..05579cbd99 100644 --- a/hibernate-core/src/test/java/org/hibernate/id/enhanced/OptimizerUnitTest.java +++ b/hibernate-core/src/test/java/org/hibernate/id/enhanced/OptimizerUnitTest.java @@ -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(); - } - } - } diff --git a/hibernate-core/src/test/java/org/hibernate/id/enhanced/SourceMock.java b/hibernate-core/src/test/java/org/hibernate/id/enhanced/SourceMock.java new file mode 100644 index 0000000000..3015cfb6c7 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/id/enhanced/SourceMock.java @@ -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; + } +}