Skip to content

Commit 3086ccc

Browse files
loiclefevrebeikov
authored andcommitted
HHH-19790 - Fix Gradle Worker ID gaps for parallel testing
1 parent 1622dd3 commit 3086ccc

File tree

1 file changed

+108
-30
lines changed

1 file changed

+108
-30
lines changed

hibernate-testing/src/main/java/org/hibernate/testing/jdbc/GradleParallelTestingResolver.java

Lines changed: 108 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,19 @@
66

77
import org.hibernate.cfg.AvailableSettings;
88

9+
import java.io.File;
10+
import java.io.IOException;
11+
import java.io.RandomAccessFile;
12+
import java.nio.ByteBuffer;
13+
import java.nio.LongBuffer;
14+
import java.nio.channels.FileChannel;
15+
import java.nio.channels.FileLock;
16+
import java.nio.channels.OverlappingFileLockException;
917
import java.util.Map;
1018
import java.util.Properties;
1119

1220
/**
13-
* JDBC config resolver for parallel tests.
21+
* JDBC config resolver for parallel tests (uses a Reentrant File System Based Sequence).
1422
*
1523
* @author Loïc Lefèvre
1624
*/
@@ -31,80 +39,150 @@ public class GradleParallelTestingResolver {
3139
private static final String GRADLE_MAXIMUM_PARALLEL_FORKS = "maxParallelForks";
3240

3341
public static void resolve(final Properties connectionProps) {
34-
if( connectionProps != null ) {
42+
if ( connectionProps != null ) {
3543
// If Gradle parallel testing is enabled (maxParallelForks > 1)
3644
final String user = connectionProps.getProperty( JDBC_USER_CONNECTION_PROPERTY );
37-
if ( user.contains( GRADLE_WORKER_PATTERN ) ) {
45+
if ( user != null && user.contains( GRADLE_WORKER_PATTERN ) ) {
3846
connectionProps.put( JDBC_USER_CONNECTION_PROPERTY,
39-
user.replace( GRADLE_WORKER_PATTERN, String.valueOf( getRunningID() ) ) );
47+
user.replace( GRADLE_WORKER_PATTERN, String.valueOf( getWorkerID() ) ) );
4048
}
4149
final String url = connectionProps.getProperty( AvailableSettings.URL );
4250
if ( url != null && url.contains( GRADLE_WORKER_PATTERN ) ) {
4351
connectionProps.put( AvailableSettings.URL,
44-
url.replace( GRADLE_WORKER_PATTERN, String.valueOf( getRunningID() ) ) );
52+
url.replace( GRADLE_WORKER_PATTERN, String.valueOf( getWorkerID() ) ) );
4553
}
4654
}
4755
}
4856

4957
public static void resolveFromSettings(final Properties settingsProps) {
50-
if( settingsProps != null ) {
58+
if ( settingsProps != null ) {
5159
// If Gradle parallel testing is enabled (maxParallelForks > 1)
5260
final String user = settingsProps.getProperty( AvailableSettings.USER );
53-
if ( user.contains( GRADLE_WORKER_PATTERN ) ) {
61+
if ( user != null && user.contains( GRADLE_WORKER_PATTERN ) ) {
5462
settingsProps.put( AvailableSettings.USER,
55-
user.replace( GRADLE_WORKER_PATTERN, String.valueOf( getRunningID() ) ) );
63+
user.replace( GRADLE_WORKER_PATTERN, String.valueOf( getWorkerID() ) ) );
5664
}
5765
final String url = settingsProps.getProperty( AvailableSettings.URL );
5866
if ( url != null && url.contains( GRADLE_WORKER_PATTERN ) ) {
5967
settingsProps.put( AvailableSettings.URL,
60-
url.replace( GRADLE_WORKER_PATTERN, String.valueOf( getRunningID() ) ) );
68+
url.replace( GRADLE_WORKER_PATTERN, String.valueOf( getWorkerID() ) ) );
6169
}
6270
}
6371
}
6472

6573
public static void resolveFromSettings(final Map<String, Object> settingsProps) {
66-
if( settingsProps != null ) {
74+
if ( settingsProps != null ) {
6775
// If Gradle parallel testing is enabled (maxParallelForks > 1)
6876
final String user = (String) settingsProps.get( AvailableSettings.USER );
69-
if ( user.contains( GRADLE_WORKER_PATTERN ) ) {
77+
if ( user != null && user.contains( GRADLE_WORKER_PATTERN ) ) {
7078
settingsProps.put( AvailableSettings.USER,
71-
user.replace( GRADLE_WORKER_PATTERN, String.valueOf( getRunningID() ) ) );
79+
user.replace( GRADLE_WORKER_PATTERN, String.valueOf( getWorkerID() ) ) );
7280
}
7381
final String url = (String) settingsProps.get( AvailableSettings.URL );
7482
if ( url != null && url.contains( GRADLE_WORKER_PATTERN ) ) {
7583
settingsProps.put( AvailableSettings.URL,
76-
url.replace( GRADLE_WORKER_PATTERN, String.valueOf( getRunningID() ) ) );
84+
url.replace( GRADLE_WORKER_PATTERN, String.valueOf( getWorkerID() ) ) );
7785
}
7886
}
7987
}
8088

81-
public static String resolveUrl(String url) {
82-
return url.replace( GRADLE_WORKER_PATTERN, String.valueOf( getRunningID() ) );
89+
public static String resolveUrl(final String url) {
90+
return url.replace( GRADLE_WORKER_PATTERN, String.valueOf( getWorkerID() ) );
8391
}
8492

85-
public static String resolveUsername(String username) {
86-
return username.replace( GRADLE_WORKER_PATTERN, String.valueOf( getRunningID() ) );
93+
public static String resolveUsername(final String username) {
94+
return username.replace( GRADLE_WORKER_PATTERN, String.valueOf( getWorkerID() ) );
8795
}
8896

8997
/**
90-
* Create a JVM Running ID based on the Gradle properties.
91-
* Whenever a task is running in parallel, Gradle will fork JVMs and assign
92-
* a monotonic sequence number to it (it may not start with 1) which can be
98+
* Retrieves the worker ID based on the Gradle properties.
99+
* Whenever a Gradle task is running in parallel, Gradle will fork JVMs and assign
100+
* a monotonic sequence number to it (it may not start with 1, and it can have "holes") which can be
93101
* retrieved using the system property {@link #GRADLE_WORKER_ID}.
102+
* <p>
103+
* <b>To cope with the Gradle sequence number limitations ("holes"), we use a <i>reentrant file system based sequence</i>.</b>
104+
* </p>
94105
*
95106
* @return an integer between 1 and {@link #GRADLE_MAXIMUM_PARALLEL_FORKS} system property (inclusive)
96107
*/
97-
private static int getRunningID() {
98-
try {
99-
// enable parallelization of up to GRADLE_MAXIMUM_PARALLEL_FORKS
100-
final Integer maxParallelForks = Integer.valueOf(
101-
System.getProperty( GRADLE_MAXIMUM_PARALLEL_FORKS, "1" ) );
102-
// Note that the worker ids are strictly monotonic
103-
final Integer worker = Integer.valueOf( System.getProperty( GRADLE_WORKER_ID, "1" ) );
104-
return (worker % maxParallelForks) + 1;
105-
}
106-
catch(NumberFormatException nfe) {
108+
private static int getWorkerID() {
109+
// maximum degree of parallelization
110+
final int maxParallelForks = Integer.parseInt( System.getProperty( GRADLE_MAXIMUM_PARALLEL_FORKS, "1" ) );
111+
112+
// target JDBC user 1 if no parallel tests enabled
113+
if(maxParallelForks == 1) {
107114
return 1;
108115
}
116+
117+
// current Gradle worker ID (can be for the same task: 157, 158, <hole>, 160, 161
118+
final long id = Long.parseLong( System.getProperty( GRADLE_WORKER_ID, "1" ) );
119+
120+
// sequence file will be stored within the target sub-folder of gradle modules with parallel tests enabled
121+
// we use the parent process handle because Gradle forks JVMs
122+
final File sequenceFile = new File( new File( System.getProperty( "user.dir" ), "target" ),
123+
String.format( "%d.sequence", ProcessHandle.current().parent().get().pid() ) );
124+
125+
// we'll rely on file system locks
126+
try (RandomAccessFile file = new RandomAccessFile( sequenceFile, "rws" )) {
127+
FileChannel fc = file.getChannel();
128+
129+
if ( file.length() > 0 ) {
130+
// read full content and try searching for my own id
131+
final ByteBuffer bb = ByteBuffer.allocate( Long.BYTES * maxParallelForks );
132+
do {
133+
try (FileLock lock = fc.lock( 0L, Long.MAX_VALUE, true )) {
134+
final int bytesRead = fc.read( bb, 0 );
135+
final LongBuffer lb = bb.rewind().asLongBuffer();
136+
137+
for ( int i = 0; i < lb.limit(); i++ ) {
138+
if ( lb.get( i ) == id ) {
139+
return i + 1;
140+
}
141+
}
142+
// could not find our own id inside the file, exit read loop!
143+
break;
144+
}
145+
catch (OverlappingFileLockException e) {
146+
try {
147+
Thread.sleep( 50L );
148+
}
149+
catch (InterruptedException ignored) {
150+
}
151+
}
152+
}
153+
while ( true );
154+
}
155+
156+
// write lock
157+
do {
158+
try {
159+
try (FileLock lock = fc.lock()) {
160+
long length = file.length();
161+
if ( length >= (long) Long.BYTES * maxParallelForks ) {
162+
fc.truncate( 0 );
163+
length = 0;
164+
}
165+
file.seek( length );
166+
final ByteBuffer bb = ByteBuffer.allocate( Long.BYTES );
167+
bb.asLongBuffer().put( new long[] {id} );
168+
final int bytesWritten = fc.write( bb );
169+
fc.force( true );
170+
171+
return (int) ((length / Long.BYTES) + 1);
172+
}
173+
}
174+
catch (OverlappingFileLockException e) {
175+
try {
176+
Thread.sleep( 50L );
177+
}
178+
catch (InterruptedException ignored) {
179+
}
180+
}
181+
}
182+
while ( true );
183+
}
184+
catch (IOException ioe) {
185+
throw new RuntimeException( "An error occurred when computing worker ID", ioe );
186+
}
109187
}
110188
}

0 commit comments

Comments
 (0)