@@ -119,7 +119,7 @@ public void accessorInitialization() {
119119 * from 6 threads. In a fresh JVM, {@code <clinit>} runs for the first time — the only way
120120 * to exercise the real deadlock scenario. A 30-second timeout detects the hang.
121121 */
122- @ Test (groups = { "unit" })
122+ @ Test (groups = { "unit" }, invocationCount = 5 )
123123 public void concurrentAccessorInitializationShouldNotDeadlock () throws Exception {
124124
125125 String javaHome = System .getProperty ("java.home" );
@@ -148,27 +148,39 @@ public void concurrentAccessorInitializationShouldNotDeadlock() throws Exception
148148 int runs = 3 ;
149149
150150 for (int run = 1 ; run <= runs ; run ++) {
151+ final int currentRun = run ;
151152 ProcessBuilder pb = new ProcessBuilder (command );
152153 pb .redirectErrorStream (true );
153154 Process process = pb .start ();
154155
156+ // Drain stdout on a separate thread to prevent blocking if child JVM deadlocks.
157+ // Without this, readLine() would block indefinitely and the timeout below
158+ // would never be reached.
155159 StringBuilder output = new StringBuilder ();
156- try (BufferedReader reader = new BufferedReader (new InputStreamReader (process .getInputStream ()))) {
157- String line ;
158- while ((line = reader .readLine ()) != null ) {
159- output .append (line ).append (System .lineSeparator ());
160- logger .info ("[child-jvm-run-{}] {}" , run , line );
160+ Thread gobbler = new Thread (() -> {
161+ try (BufferedReader reader = new BufferedReader (new InputStreamReader (process .getInputStream ()))) {
162+ String line ;
163+ while ((line = reader .readLine ()) != null ) {
164+ output .append (line ).append (System .lineSeparator ());
165+ logger .info ("[child-jvm-run-{}] {}" , currentRun , line );
166+ }
167+ } catch (Exception e ) {
168+ // Process was destroyed — expected on timeout
161169 }
162- }
170+ });
171+ gobbler .setDaemon (true );
172+ gobbler .start ();
163173
164174 boolean completed = process .waitFor (timeoutSeconds , TimeUnit .SECONDS );
165175
166176 if (!completed ) {
167177 process .destroyForcibly ();
178+ gobbler .join (5000 );
168179 fail ("Run " + run + ": Child JVM did not complete within " + timeoutSeconds
169180 + " seconds — <clinit> deadlock detected" );
170181 }
171182
183+ gobbler .join (5000 );
172184 int exitCode = process .exitValue ();
173185 assertThat (exitCode )
174186 .as ("Run " + run + ": Child JVM exited with non-zero code. Output:\n " + output )
@@ -184,51 +196,244 @@ public void concurrentAccessorInitializationShouldNotDeadlock() throws Exception
184196 public static final class ConcurrentClinitChildProcess {
185197 public static void main (String [] args ) {
186198 int timeoutSeconds = 20 ;
187- int threadCount = 6 ;
188- CyclicBarrier barrier = new CyclicBarrier (threadCount );
189- ExecutorService executor = Executors .newFixedThreadPool (threadCount );
190199
191200 String [] classesToLoad = {
192201 "com.azure.cosmos.CosmosAsyncClient" ,
193202 "com.azure.cosmos.models.SqlParameter" ,
194203 "com.azure.cosmos.models.FeedResponse" ,
195204 "com.azure.cosmos.models.CosmosItemRequestOptions" ,
196205 "com.azure.cosmos.CosmosAsyncContainer" ,
197- "com.azure.cosmos.util.CosmosPagedFluxDefaultImpl"
206+ "com.azure.cosmos.util.CosmosPagedFluxDefaultImpl" ,
207+ "com.azure.cosmos.CosmosClientBuilder" ,
208+ "com.azure.cosmos.CosmosItemSerializer" ,
209+ "com.azure.cosmos.CosmosDiagnostics" ,
210+ "com.azure.cosmos.CosmosDiagnosticsContext" ,
211+ "com.azure.cosmos.models.CosmosQueryRequestOptions" ,
212+ "com.azure.cosmos.models.CosmosChangeFeedRequestOptions"
198213 };
199214
200- List <Future <?>> futures = new ArrayList <>();
201- for (int i = 0 ; i < classesToLoad .length ; i ++) {
202- final String className = classesToLoad [i ];
203- final int idx = i ;
204- futures .add (executor .submit (() -> {
215+ int threadCount = classesToLoad .length ;
216+ CyclicBarrier barrier = new CyclicBarrier (threadCount );
217+ ExecutorService executor = Executors .newFixedThreadPool (threadCount );
218+
219+ try {
220+ List <Future <?>> futures = new ArrayList <>();
221+ for (int i = 0 ; i < classesToLoad .length ; i ++) {
222+ final String className = classesToLoad [i ];
223+ final int idx = i ;
224+ futures .add (executor .submit (() -> {
225+ try {
226+ barrier .await ();
227+ System .out .println ("[Thread-" + idx + "] Loading " + className );
228+ Class .forName (className );
229+ System .out .println ("[Thread-" + idx + "] Done." );
230+ } catch (Exception e ) {
231+ throw new RuntimeException ("Failed to load " + className , e );
232+ }
233+ }));
234+ }
235+
236+ boolean deadlock = false ;
237+ for (int i = 0 ; i < futures .size (); i ++) {
205238 try {
206- barrier . await ( );
207- System . out . println ( "[Thread-" + idx + "] Loading " + className );
208- Class . forName ( className );
209- System . out . println ( "[Thread-" + idx + "] Done." ) ;
239+ futures . get ( i ). get ( timeoutSeconds , TimeUnit . SECONDS );
240+ } catch ( java . util . concurrent . TimeoutException e ) {
241+ System . err . println ( "DEADLOCK: Thread-" + i + " timed out after " + timeoutSeconds + "s" );
242+ deadlock = true ;
210243 } catch (Exception e ) {
211- throw new RuntimeException ("Failed to load " + className , e );
244+ Throwable root = e ;
245+ while (root .getCause () != null ) {
246+ root = root .getCause ();
247+ }
248+ System .err .println ("Thread-" + i + " error: " + root );
212249 }
213- }));
250+ }
251+
252+ if (deadlock ) {
253+ System .exit (1 );
254+ }
255+
256+ // Verify all classes are actually initialized
257+ for (String className : classesToLoad ) {
258+ try {
259+ // Class.forName with initialize=false just checks if already loaded
260+ // If the class was loaded above, this returns immediately
261+ Class <?> cls = Class .forName (className , false ,
262+ ConcurrentClinitChildProcess .class .getClassLoader ());
263+ // Verify the class is initialized by accessing its static state
264+ // (calling a static method would trigger <clinit> if not done,
265+ // but we explicitly check it's already done)
266+ System .out .println ("Verified loaded: " + cls .getName ());
267+ } catch (ClassNotFoundException e ) {
268+ System .err .println ("Class not loaded: " + className );
269+ System .exit (1 );
270+ }
271+ }
272+
273+ System .exit (0 );
274+ } finally {
275+ executor .shutdownNow ();
276+ }
277+ }
278+ }
279+
280+ /**
281+ * Enforces that every class targeted by {@code ensureClassInitialized()} in
282+ * {@link ImplementationBridgeHelpers} registers its accessor during {@code <clinit>}
283+ * (i.e., has a {@code static { initialize(); }} block).
284+ * <p>
285+ * Verification is behavioral, not source-based: a forked child JVM loads each target
286+ * class via {@code Class.forName()}, then uses reflection to check the corresponding
287+ * Helper's accessor field is non-null. If {@code <clinit>} doesn't call
288+ * {@code initialize()}, the accessor remains null and the test fails.
289+ */
290+ @ Test (groups = { "unit" })
291+ public void allAccessorClassesMustHaveStaticInitializerBlock () throws Exception {
292+ String javaHome = System .getProperty ("java.home" );
293+ String javaBin = javaHome + java .io .File .separator + "bin" + java .io .File .separator + "java" ;
294+ String classpath = System .getProperty ("java.class.path" );
295+
296+ List <String > command = new ArrayList <>();
297+ command .add (javaBin );
298+
299+ try {
300+ int majorVersion = Integer .parseInt (System .getProperty ("java.specification.version" ).split ("\\ ." )[0 ]);
301+ if (majorVersion >= 9 ) {
302+ command .add ("--add-opens" );
303+ command .add ("java.base/java.lang=ALL-UNNAMED" );
214304 }
305+ } catch (NumberFormatException e ) {
306+ // JDK 8
307+ }
308+
309+ command .add ("-cp" );
310+ command .add (classpath );
311+ command .add (AccessorRegistrationChildProcess .class .getName ());
312+
313+ ProcessBuilder pb = new ProcessBuilder (command );
314+ pb .redirectErrorStream (true );
315+ Process process = pb .start ();
316+
317+ StringBuilder output = new StringBuilder ();
318+ Thread gobbler = new Thread (() -> {
319+ try (BufferedReader reader = new BufferedReader (new InputStreamReader (process .getInputStream ()))) {
320+ String line ;
321+ while ((line = reader .readLine ()) != null ) {
322+ output .append (line ).append (System .lineSeparator ());
323+ logger .info ("[accessor-check] {}" , line );
324+ }
325+ } catch (Exception e ) {
326+ // Process destroyed
327+ }
328+ });
329+ gobbler .setDaemon (true );
330+ gobbler .start ();
331+
332+ boolean completed = process .waitFor (60 , TimeUnit .SECONDS );
333+ if (!completed ) {
334+ process .destroyForcibly ();
335+ gobbler .join (5000 );
336+ fail ("Accessor registration check timed out after 60s. Output:\n " + output );
337+ }
338+
339+ gobbler .join (5000 );
340+ int exitCode = process .exitValue ();
341+ assertThat (exitCode )
342+ .as ("Some accessor classes don't register their accessor during <clinit>. Output:\n " + output )
343+ .isEqualTo (0 );
344+ }
345+
346+ /**
347+ * Child process that verifies every class referenced by {@code ensureClassInitialized()}
348+ * registers its accessor during {@code <clinit>}. Runs in a fresh JVM where no Cosmos
349+ * classes have been loaded yet.
350+ */
351+ public static final class AccessorRegistrationChildProcess {
352+ public static void main (String [] args ) throws Exception {
353+ // Scan ImplementationBridgeHelpers for all Helper inner classes and their accessor fields.
354+ // For each Helper, find the matching ensureClassInitialized target class, load it via
355+ // Class.forName(), and verify the accessor field is non-null.
356+
357+ Class <?>[] helpers = ImplementationBridgeHelpers .class .getDeclaredClasses ();
358+ List <String > failures = new ArrayList <>();
359+
360+ for (Class <?> helper : helpers ) {
361+ if (!helper .getSimpleName ().endsWith ("Helper" )) {
362+ continue ;
363+ }
364+
365+ // Find the accessor AtomicReference field
366+ Field accessorField = null ;
367+ Field classLoadedField = null ;
368+ for (Field f : helper .getDeclaredFields ()) {
369+ if (f .getName ().contains ("accessor" ) && f .getType () == AtomicReference .class ) {
370+ accessorField = f ;
371+ }
372+ if (f .getName ().contains ("ClassLoaded" ) && f .getType () == AtomicBoolean .class ) {
373+ classLoadedField = f ;
374+ }
375+ }
376+
377+ if (accessorField == null || classLoadedField == null ) {
378+ continue ;
379+ }
380+
381+ // Check if the accessor is already set (from transitive <clinit> of earlier classes)
382+ accessorField .setAccessible (true );
383+ AtomicReference <?> ref = (AtomicReference <?>) accessorField .get (null );
384+ if (ref .get () != null ) {
385+ System .out .println ("OK (already loaded): " + helper .getSimpleName ());
386+ continue ;
387+ }
388+
389+ // Find the target class name by looking for a getXxxAccessor method that calls
390+ // ensureClassInitialized. We can't easily extract the string constant, so instead
391+ // we call the getter and check if the accessor becomes non-null.
392+ // The getter calls ensureClassInitialized() → Class.forName() → <clinit>.
393+ // If <clinit> calls initialize(), the accessor is registered.
394+ java .lang .reflect .Method getterMethod = null ;
395+ for (java .lang .reflect .Method m : helper .getDeclaredMethods ()) {
396+ if (m .getName ().startsWith ("get" ) && m .getName ().endsWith ("Accessor" )
397+ && m .getParameterCount () == 0
398+ && java .lang .reflect .Modifier .isStatic (m .getModifiers ())) {
399+ getterMethod = m ;
400+ break ;
401+ }
402+ }
403+
404+ if (getterMethod == null ) {
405+ continue ;
406+ }
215407
216- boolean deadlock = false ;
217- for (int i = 0 ; i < futures .size (); i ++) {
218408 try {
219- futures .get (i ).get (timeoutSeconds , TimeUnit .SECONDS );
220- } catch (java .util .concurrent .TimeoutException e ) {
221- System .err .println ("DEADLOCK: Thread-" + i + " timed out after " + timeoutSeconds + "s" );
222- deadlock = true ;
409+ Object result = getterMethod .invoke (null );
410+ if (result == null ) {
411+ failures .add (helper .getSimpleName () + ": accessor is null after getter call — "
412+ + "target class <clinit> does not call initialize()" );
413+ } else {
414+ System .out .println ("OK: " + helper .getSimpleName ());
415+ }
223416 } catch (Exception e ) {
224417 Throwable root = e ;
225- while (root .getCause () != null ) root = root .getCause ();
226- System .err .println ("Thread-" + i + " error: " + root );
418+ while (root .getCause () != null ) {
419+ root = root .getCause ();
420+ }
421+ failures .add (helper .getSimpleName () + ": " + root .getClass ().getSimpleName ()
422+ + " — " + root .getMessage ());
227423 }
228424 }
229425
230- executor .shutdownNow ();
231- System .exit (deadlock ? 1 : 0 );
426+ if (failures .isEmpty ()) {
427+ System .out .println ("All accessor classes register their accessor during <clinit>." );
428+ System .exit (0 );
429+ } else {
430+ System .err .println ("FAILURES — the following classes do not register their accessor "
431+ + "during <clinit> (missing 'static { initialize(); }' block):" );
432+ for (String f : failures ) {
433+ System .err .println (" " + f );
434+ }
435+ System .exit (1 );
436+ }
232437 }
233438 }
234439}
0 commit comments