66
77import 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 ;
917import java .util .Map ;
1018import 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