6
6
7
7
import org .hibernate .cfg .AvailableSettings ;
8
8
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 ;
9
17
import java .util .Map ;
10
18
import java .util .Properties ;
11
19
12
20
/**
13
- * JDBC config resolver for parallel tests.
21
+ * JDBC config resolver for parallel tests (uses a Reentrant File System Based Sequence) .
14
22
*
15
23
* @author Loïc Lefèvre
16
24
*/
@@ -31,80 +39,150 @@ public class GradleParallelTestingResolver {
31
39
private static final String GRADLE_MAXIMUM_PARALLEL_FORKS = "maxParallelForks" ;
32
40
33
41
public static void resolve (final Properties connectionProps ) {
34
- if ( connectionProps != null ) {
42
+ if ( connectionProps != null ) {
35
43
// If Gradle parallel testing is enabled (maxParallelForks > 1)
36
44
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 ) ) {
38
46
connectionProps .put ( JDBC_USER_CONNECTION_PROPERTY ,
39
- user .replace ( GRADLE_WORKER_PATTERN , String .valueOf ( getRunningID () ) ) );
47
+ user .replace ( GRADLE_WORKER_PATTERN , String .valueOf ( getWorkerID () ) ) );
40
48
}
41
49
final String url = connectionProps .getProperty ( AvailableSettings .URL );
42
50
if ( url != null && url .contains ( GRADLE_WORKER_PATTERN ) ) {
43
51
connectionProps .put ( AvailableSettings .URL ,
44
- url .replace ( GRADLE_WORKER_PATTERN , String .valueOf ( getRunningID () ) ) );
52
+ url .replace ( GRADLE_WORKER_PATTERN , String .valueOf ( getWorkerID () ) ) );
45
53
}
46
54
}
47
55
}
48
56
49
57
public static void resolveFromSettings (final Properties settingsProps ) {
50
- if ( settingsProps != null ) {
58
+ if ( settingsProps != null ) {
51
59
// If Gradle parallel testing is enabled (maxParallelForks > 1)
52
60
final String user = settingsProps .getProperty ( AvailableSettings .USER );
53
- if ( user .contains ( GRADLE_WORKER_PATTERN ) ) {
61
+ if ( user != null && user .contains ( GRADLE_WORKER_PATTERN ) ) {
54
62
settingsProps .put ( AvailableSettings .USER ,
55
- user .replace ( GRADLE_WORKER_PATTERN , String .valueOf ( getRunningID () ) ) );
63
+ user .replace ( GRADLE_WORKER_PATTERN , String .valueOf ( getWorkerID () ) ) );
56
64
}
57
65
final String url = settingsProps .getProperty ( AvailableSettings .URL );
58
66
if ( url != null && url .contains ( GRADLE_WORKER_PATTERN ) ) {
59
67
settingsProps .put ( AvailableSettings .URL ,
60
- url .replace ( GRADLE_WORKER_PATTERN , String .valueOf ( getRunningID () ) ) );
68
+ url .replace ( GRADLE_WORKER_PATTERN , String .valueOf ( getWorkerID () ) ) );
61
69
}
62
70
}
63
71
}
64
72
65
73
public static void resolveFromSettings (final Map <String , Object > settingsProps ) {
66
- if ( settingsProps != null ) {
74
+ if ( settingsProps != null ) {
67
75
// If Gradle parallel testing is enabled (maxParallelForks > 1)
68
76
final String user = (String ) settingsProps .get ( AvailableSettings .USER );
69
- if ( user .contains ( GRADLE_WORKER_PATTERN ) ) {
77
+ if ( user != null && user .contains ( GRADLE_WORKER_PATTERN ) ) {
70
78
settingsProps .put ( AvailableSettings .USER ,
71
- user .replace ( GRADLE_WORKER_PATTERN , String .valueOf ( getRunningID () ) ) );
79
+ user .replace ( GRADLE_WORKER_PATTERN , String .valueOf ( getWorkerID () ) ) );
72
80
}
73
81
final String url = (String ) settingsProps .get ( AvailableSettings .URL );
74
82
if ( url != null && url .contains ( GRADLE_WORKER_PATTERN ) ) {
75
83
settingsProps .put ( AvailableSettings .URL ,
76
- url .replace ( GRADLE_WORKER_PATTERN , String .valueOf ( getRunningID () ) ) );
84
+ url .replace ( GRADLE_WORKER_PATTERN , String .valueOf ( getWorkerID () ) ) );
77
85
}
78
86
}
79
87
}
80
88
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 () ) );
83
91
}
84
92
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 () ) );
87
95
}
88
96
89
97
/**
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
93
101
* 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>
94
105
*
95
106
* @return an integer between 1 and {@link #GRADLE_MAXIMUM_PARALLEL_FORKS} system property (inclusive)
96
107
*/
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 ) {
107
114
return 1 ;
108
115
}
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
+ }
109
187
}
110
188
}
0 commit comments