@@ -112,7 +112,7 @@ public final class VirtualThreadContextCascadeAntagonist implements Antagonist {
112112 // carriers actually exit before the stale-buffer racers spawned in the burst wake back up.
113113 private static final long ACTIVE_PHASE_MILLIS = 8_000L ;
114114 private static final long QUIET_PHASE_MILLIS = 40_000L ;
115- private static final long STALE_RACER_SLEEP_MILLIS = QUIET_PHASE_MILLIS + 5_000L ;
115+ private static final long STALE_RACER_SLEEP_MILLIS = ACTIVE_PHASE_MILLIS + QUIET_PHASE_MILLIS + 5_000L ;
116116 private static final int STALE_RACER_BATCH = 16 ;
117117 private static final int MAX_STALE_RACERS = 256 ;
118118
@@ -176,36 +176,43 @@ public void start() {
176176 @ Override
177177 public void stopGracefully (Duration timeout ) {
178178 running = false ;
179+ long deadlineNanos = System .nanoTime () + timeout .toNanos ();
179180 try {
180- driver .join (timeout . toMillis ( ));
181+ driver .join (remainingMillis ( deadlineNanos ));
181182 } catch (InterruptedException e ) {
182183 Thread .currentThread ().interrupt ();
183184 }
184185 try {
185- pinningDriver .join (timeout . toMillis ( ));
186+ pinningDriver .join (remainingMillis ( deadlineNanos ));
186187 } catch (InterruptedException e ) {
187188 Thread .currentThread ().interrupt ();
188189 }
189190 // Best-effort drain of in-flight cascades so the JVM doesn't exit mid-fan-out.
190191 try {
191- liveVthreads .tryAcquire (MAX_LIVE_VTHREADS , timeout . toMillis ( ), TimeUnit .MILLISECONDS );
192+ liveVthreads .tryAcquire (MAX_LIVE_VTHREADS , remainingMillis ( deadlineNanos ), TimeUnit .MILLISECONDS );
192193 } catch (InterruptedException e ) {
193194 Thread .currentThread ().interrupt ();
194195 }
195196 try {
196- livePinVthreads .tryAcquire (MAX_PIN_VTHREADS , timeout . toMillis ( ), TimeUnit .MILLISECONDS );
197+ livePinVthreads .tryAcquire (MAX_PIN_VTHREADS , remainingMillis ( deadlineNanos ), TimeUnit .MILLISECONDS );
197198 } catch (InterruptedException e ) {
198199 Thread .currentThread ().interrupt ();
199200 }
200201 // Stale-buffer racers sleep well past stopGracefully's own timeout budget, so this is
201202 // best-effort only — outstanding racers are daemon threads and die with the JVM.
202203 try {
203- liveStaleRacers .tryAcquire (MAX_STALE_RACERS , timeout . toMillis ( ), TimeUnit .MILLISECONDS );
204+ liveStaleRacers .tryAcquire (MAX_STALE_RACERS , remainingMillis ( deadlineNanos ), TimeUnit .MILLISECONDS );
204205 } catch (InterruptedException e ) {
205206 Thread .currentThread ().interrupt ();
206207 }
207208 }
208209
210+ /** Milliseconds remaining until {@code deadlineNanos}, clamped to zero (never negative). */
211+ private static long remainingMillis (long deadlineNanos ) {
212+ long remainingNanos = deadlineNanos - System .nanoTime ();
213+ return remainingNanos <= 0L ? 0L : TimeUnit .NANOSECONDS .toMillis (remainingNanos );
214+ }
215+
209216 /**
210217 * Independently pins short-lived virtual threads to their carrier and releases them again,
211218 * forcing the scheduler to grow and shrink its compensating carrier pool while cascade nodes
0 commit comments