001/* 002 * Copyright 2022-2026 Revetware LLC. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package com.soklet; 018 019import com.soklet.Soklet.DefaultSimulator; 020import com.soklet.Soklet.MockHttpServer; 021import com.soklet.Soklet.MockSseServer; 022import org.jspecify.annotations.NonNull; 023import org.jspecify.annotations.Nullable; 024 025import javax.annotation.concurrent.Immutable; 026import javax.annotation.concurrent.NotThreadSafe; 027import javax.annotation.concurrent.ThreadSafe; 028import java.time.Duration; 029import java.util.ArrayList; 030import java.util.Collections; 031import java.util.EnumMap; 032import java.util.EnumSet; 033import java.util.List; 034import java.util.Locale; 035import java.util.Map; 036import java.util.Optional; 037import java.util.Set; 038import java.util.concurrent.TimeoutException; 039import java.util.concurrent.atomic.AtomicBoolean; 040import java.util.concurrent.atomic.AtomicReference; 041 042import static com.soklet.internal.ObjectIdentity.sameInstance; 043import static java.util.Objects.requireNonNull; 044 045/** 046 * Runs single-use, off-network simulation configurations. 047 * <p> 048 * Incomplete teardown throws {@link SokletShutdownIncompleteException} after a 049 * successful simulation body, or suppresses it on the original body failure. 050 * Its message summarizes component dispositions and residual activity; inspect 051 * {@link SokletLifecycleException#getShutdownResult()} for the frozen typed 052 * evidence. Outstanding work alone does not invent a Throwable cause. 053 */ 054@ThreadSafe 055public final class SokletSimulator { 056 /** 057 * Simulation body with checked-exception transparency. One run invokes its 058 * body exactly once and never concurrently; implementations are not required 059 * to be thread-safe. 060 */ 061 @NotThreadSafe 062 @FunctionalInterface 063 public interface Simulation<E extends @NonNull Throwable> { 064 /** 065 * Executes against the transport-isolated simulator. 066 * 067 * @param simulator transport-isolated simulator 068 * @throws E application-selected failure type 069 */ 070 void run(@NonNull Simulator simulator) throws E; 071 } 072 073 private SokletSimulator() { 074 } 075 076 /** 077 * Runs an existing Soklet application configuration against a fresh, 078 * off-network simulator transport graph. 079 * <p> 080 * The source configuration supplies the application's settings and transport 081 * shape. Every source transport type remains present in the derived graph; 082 * use {@link SimulatorConfig#builder()} when a test must omit one. The 083 * simulator call never starts, claims, reuses, or changes the source HTTP, 084 * SSE, or MCP instances, so each invocation is transport-isolated. 085 * <p> 086 * Explicitly configured application and MCP collaborators are reused by 087 * identity. Derivation does not inspect or rebind dependency-injection 088 * providers or other objects that captured a source transport. Supply 089 * test-scoped collaborators through 090 * {@link SimulatorConfig#withSokletConfig(SokletConfig)} when needed. 091 * A caller interrupt status left set by the simulation body is temporarily 092 * cleared while bounded teardown runs and is restored before this method 093 * returns or throws. 094 * 095 * @param sokletConfig source application configuration 096 * @param simulation simulation work 097 * @param <E> checked throwable type selected by the simulation 098 * @return the exact immutable scope-shutdown result 099 * @throws E if the simulation fails; a teardown failure is then suppressed on it 100 * @throws SokletShutdownIncompleteException if successful simulation execution is followed 101 * by shutdown that cannot be proven complete 102 */ 103 @NonNull 104 public static <E extends @NonNull Throwable> ShutdownResult run( 105 @NonNull SokletConfig sokletConfig, 106 @NonNull Simulation<@NonNull E> simulation) throws E { 107 SokletConfig exactConfig = requireNonNull(sokletConfig); 108 Simulation<E> exactSimulation = requireNonNull(simulation); 109 return run(SimulatorConfig.fromSokletConfig(exactConfig), 110 exactSimulation); 111 } 112 113 /** 114 * Runs one prebuilt, single-use off-network simulation configuration. 115 * A caller interrupt status left set by the simulation body is temporarily 116 * cleared while bounded teardown runs and is restored before this method 117 * returns or throws. 118 * 119 * @param simulatorConfig simulator configuration; it is claimed by this call 120 * before lifecycle work begins and cannot be reused 121 * @param simulation simulation work 122 * @param <E> checked throwable type selected by the simulation 123 * @return the exact immutable scope-shutdown result 124 * @throws E if the simulation fails; a teardown failure is then suppressed on it 125 * @throws SokletShutdownIncompleteException if successful simulation execution is followed 126 * by shutdown that cannot be proven complete 127 * @throws IllegalStateException if {@code simulatorConfig} was already claimed 128 */ 129 @NonNull 130 public static <E extends @NonNull Throwable> ShutdownResult run( 131 @NonNull SimulatorConfig simulatorConfig, 132 @NonNull Simulation<@NonNull E> simulation) throws E { 133 return ShutdownResult.fromInternal(run(simulatorConfig, simulation, 134 NanoClock.system(), new LifecycleWorkers())); 135 } 136 137 @NonNull 138 static <E extends @NonNull Throwable> InternalShutdownResult run( 139 @NonNull SimulatorConfig simulatorConfig, 140 @NonNull Simulation<E> simulation, 141 @NonNull NanoClock clock, 142 @NonNull LifecycleWorkers workers) throws E { 143 NanoClock exactClock = requireNonNull(clock); 144 return run(simulatorConfig, simulation, exactClock, 145 new DeadlineWaiter(exactClock), workers); 146 } 147 148 @NonNull 149 static <E extends @NonNull Throwable> InternalShutdownResult run( 150 @NonNull SimulatorConfig simulatorConfig, 151 @NonNull Simulation<E> simulation, 152 @NonNull NanoClock clock, 153 @NonNull DeadlineWaiter waiter, 154 @NonNull LifecycleWorkers workers) throws E { 155 SimulatorConfig exactConfig = requireNonNull(simulatorConfig); 156 Simulation<E> exactSimulation = requireNonNull(simulation); 157 NanoClock exactClock = requireNonNull(clock); 158 DeadlineWaiter exactWaiter = requireNonNull(waiter); 159 LifecycleWorkers exactWorkers = requireNonNull(workers); 160 exactConfig.claimForRun(); 161 return new Scope(exactConfig, exactClock, exactWaiter, exactWorkers) 162 .run(exactConfig, exactSimulation); 163 } 164 165 @SuppressWarnings("unchecked") 166 private static <E extends @NonNull Throwable> void throwBodyFailure( 167 @NonNull Throwable failure) throws E { 168 throw (E) requireNonNull(failure); 169 } 170 171 @NotThreadSafe 172 private static final class Scope 173 implements SimulatorConfigurationScopeIdentity { 174 @NonNull 175 private final NanoClock clock; 176 @NonNull 177 private final LifecycleWorkers workers; 178 @NonNull 179 private final DeadlineWaiter waiter; 180 @NonNull 181 private final TrackedLifecycleCallRunner callRunner; 182 @NonNull 183 private final AtomicReference<@Nullable StartupCallParticipant> 184 activeStartupParticipant; 185 @NonNull 186 private final AtomicBoolean startupCancelationRequired; 187 @NonNull 188 private final Object configurationIdentity; 189 @NonNull 190 private final MockHttpServer httpServer; 191 @NonNull 192 private final MockSseServer sseServer; 193 @NonNull 194 private final InternalLifecycleStateMachine stateMachine; 195 @NonNull 196 private final InternalControllingEventElection controllingEventElection; 197 private final @Nullable DefaultMcpServer mcpServer; 198 private @Nullable DefaultSimulator simulator; 199 200 private Scope(@NonNull SimulatorConfig simulatorConfig, 201 @NonNull NanoClock clock, 202 @NonNull DeadlineWaiter waiter, 203 @NonNull LifecycleWorkers workers) { 204 SimulatorConfig exactConfig = requireNonNull(simulatorConfig); 205 this.clock = requireNonNull(clock); 206 this.workers = requireNonNull(workers); 207 this.waiter = requireNonNull(waiter); 208 this.callRunner = new TrackedLifecycleCallRunner(workers); 209 this.activeStartupParticipant = new AtomicReference<>(); 210 this.startupCancelationRequired = new AtomicBoolean(); 211 this.configurationIdentity = exactConfig.configurationIdentity(); 212 this.httpServer = exactConfig.simulatedHttpServer(); 213 this.sseServer = exactConfig.simulatedSseServer(); 214 this.mcpServer = exactConfig.simulatedMcpServer(); 215 this.stateMachine = new InternalLifecycleStateMachine(); 216 this.controllingEventElection = 217 new InternalControllingEventElection(); 218 } 219 220 @NonNull 221 private <E extends @NonNull Throwable> InternalShutdownResult run( 222 @NonNull SimulatorConfig simulatorConfig, 223 @NonNull Simulation<E> simulation) throws E { 224 if (!simulatorConfig.belongsTo(this.configurationIdentity)) 225 throw new IllegalStateException( 226 "The simulator configuration belongs to a different scope"); 227 228 SokletConfig config = simulatorConfig.getSokletConfig(); 229 ResolvedScopeTransports resolved = validate(config); 230 InternalLifecyclePolicy policy = config.getInternalLifecyclePolicy(); 231 long startupBeganNanos = this.clock.nanoTime(); 232 long startupDeadline = LifecycleDeadlines.after(startupBeganNanos, 233 policy.startupTimeout()); 234 StartupBudget startupBudget = new StartupBudget(startupDeadline, 235 policy.startupCancelationTimeout()); 236 this.stateMachine.claimStart(); 237 // Construction is deliberately lightweight. Setup and installation run 238 // below under this scope's startup context, against only its fresh mocks 239 // and fresh unbound MCP graph. 240 Soklet soklet = Soklet.fromConfig(config); 241 242 Object executionOwnerToken = soklet.getDirectLifecycle() 243 .executionOwnerToken(); 244 List<ScopeParticipant> participants = participants(resolved, 245 executionOwnerToken); 246 ScopeDispatchGate dispatchGate = new ScopeDispatchGate(participants); 247 this.simulator = new DefaultSimulator( 248 resolved.httpServer().orElse(null), 249 resolved.sseServer().orElse(null), 250 simulatorConfig.getSimulatorOptions(), 251 resolved.mcpServer().orElse(null), dispatchGate, executionOwnerToken); 252 Throwable primaryFailure = null; 253 boolean bodyEntered = false; 254 boolean restoreStartupInterrupt = false; 255 boolean startupLifecycleFailure = false; 256 InternalStartupDisposition startupDisposition = 257 InternalStartupDisposition.FAILED; 258 StartupAttempt setupAttempt = new StartupAttempt(this, 259 executionOwnerToken); 260 try { 261 startParticipants(soklet, participants, startupBudget, 262 setupAttempt); 263 ensureStartupDeadline(startupBudget.startupDeadline()); 264 ensureStartupMayContinue(); 265 if (!this.stateMachine.publishReady()) { 266 ensureStartupMayContinue(); 267 throw new IllegalStateException( 268 "The simulator scope could not publish readiness"); 269 } 270 startupDisposition = InternalStartupDisposition.READY; 271 if (!openParticipantAdmission(participants)) 272 requestOwnerShutdown(); 273 bodyEntered = true; 274 simulation.run(this.simulator); 275 } catch (Throwable failure) { 276 primaryFailure = failure; 277 if (!bodyEntered 278 && startupDisposition != InternalStartupDisposition.READY) { 279 startupLifecycleFailure = true; 280 if (failure instanceof TimeoutException) 281 startupDisposition = InternalStartupDisposition.TIMED_OUT; 282 else if (failure instanceof InterruptedException) { 283 startupDisposition = InternalStartupDisposition.CANCELED; 284 restoreStartupInterrupt = true; 285 } else if (this.stateMachine.shutdownRequested() 286 && participants.stream().noneMatch(participant -> 287 participant.terminationGroup() 288 .controllingEvent().isPresent())) 289 startupDisposition = InternalStartupDisposition.CANCELED; 290 else 291 startupDisposition = InternalStartupDisposition.FAILED; 292 } 293 } 294 295 TeardownOutcome teardown = teardown(participants, setupAttempt, 296 policy, startupBudget, startupDisposition); 297 SokletShutdownIncompleteException teardownFailure = teardown.failure(); 298 299 if (startupLifecycleFailure) { 300 SokletStartupException startupFailure = new SokletStartupException( 301 startupDisposition, teardown.result(), 302 requireNonNull(primaryFailure)); 303 if (teardownFailure != null) 304 startupFailure.addSuppressed(teardownFailure); 305 if (restoreStartupInterrupt) 306 Thread.currentThread().interrupt(); 307 throw startupFailure; 308 } 309 if (primaryFailure != null) { 310 if (teardownFailure != null) 311 primaryFailure.addSuppressed(teardownFailure); 312 SokletSimulator.<E>throwBodyFailure(primaryFailure); 313 } 314 if (teardownFailure != null) 315 throw teardownFailure; 316 return teardown.result(); 317 } 318 319 @Override 320 @NonNull 321 public Object simulatorConfigurationIdentity() { 322 return this.configurationIdentity; 323 } 324 325 @NonNull 326 private ResolvedScopeTransports validate(@NonNull SokletConfig config) { 327 HttpServer configuredHttp = config.getHttpServer().orElse(null); 328 if (configuredHttp != null && !sameInstance(configuredHttp, this.httpServer)) 329 throw new IllegalStateException( 330 "The simulator config contains a foreign HTTP transport"); 331 332 SseServer configuredSse = config.getSseServer().orElse(null); 333 if (configuredSse != null && !sameInstance(configuredSse, this.sseServer)) 334 throw new IllegalStateException( 335 "The simulator config contains a foreign SSE transport"); 336 337 McpServer configuredMcp = config.getMcpServer().orElse(null); 338 DefaultMcpServer registeredMcp = this.mcpServer; 339 if (configuredMcp != registeredMcp) 340 throw new IllegalStateException(configuredMcp == null 341 ? "The scope-built MCP server was not installed in the simulator config" 342 : "The simulator config contains a production or foreign MCP transport"); 343 if (registeredMcp != null && registeredMcp.getDiagnostics().getStatus() 344 != McpServerStatus.NOT_STARTED) 345 throw new IllegalStateException( 346 "A simulator MCP transport must not be started"); 347 348 return new ResolvedScopeTransports( 349 Optional.ofNullable((MockHttpServer) configuredHttp), 350 Optional.ofNullable((MockSseServer) configuredSse), 351 Optional.ofNullable(registeredMcp)); 352 } 353 354 @NonNull 355 private List<@NonNull ScopeParticipant> participants( 356 @NonNull ResolvedScopeTransports resolved, 357 @NonNull Object executionOwnerToken) { 358 List<ScopeParticipant> participants = new ArrayList<>(3); 359 resolved.httpServer().ifPresent(ignored -> participants.add( 360 new ScopeParticipant(InternalLifecycleComponentType.HTTP, this, 361 null, executionOwnerToken))); 362 resolved.sseServer().ifPresent(ignored -> participants.add( 363 new ScopeParticipant(InternalLifecycleComponentType.SSE, this, 364 null, executionOwnerToken))); 365 resolved.mcpServer().ifPresent(server -> participants.add( 366 new ScopeParticipant(InternalLifecycleComponentType.MCP, this, 367 server, executionOwnerToken))); 368 return List.copyOf(participants); 369 } 370 371 private void startParticipants(@NonNull Soklet soklet, 372 @NonNull List<@NonNull ScopeParticipant> participants, 373 @NonNull StartupBudget startupBudget, 374 @NonNull StartupAttempt setupAttempt) throws Throwable { 375 StartupBudget exactBudget = requireNonNull(startupBudget); 376 StartupContext context = new StartupContext(this.clock, 377 exactBudget.startupDeadline(), 378 () -> exactBudget.beginCancellation(this.stateMachine.shutdownIntentNanos()), 379 this.stateMachine::shutdownRequested); 380 runStartupCall("simulator-framework-setup", setupAttempt, 381 () -> requireNonNull(soklet).initializeForSimulator(context, 382 this.waiter), exactBudget.startupDeadline()); 383 ensureStartupDeadline(exactBudget.startupDeadline()); 384 ensureStartupMayContinue(); 385 386 for (ScopeParticipant participant : participants) { 387 participant.commit(); 388 ensureStartupMayContinue(); 389 } 390 for (ScopeParticipant participant : participants) { 391 runStartupCall("simulator-start-" 392 + participant.kind().name().toLowerCase(Locale.ROOT), participant, 393 () -> participant.start(context), 394 exactBudget.startupDeadline()); 395 ensureStartupDeadline(exactBudget.startupDeadline()); 396 ensureStartupMayContinue(); 397 } 398 ensureStartupDeadline(exactBudget.startupDeadline()); 399 ensureStartupMayContinue(); 400 } 401 402 private boolean openParticipantAdmission( 403 @NonNull List<@NonNull ScopeParticipant> participants) { 404 boolean everyAdmissionOpened = true; 405 for (ScopeParticipant participant : requireNonNull(participants)) { 406 if (!participant.openAdmission()) 407 everyAdmissionOpened = false; 408 } 409 return everyAdmissionOpened; 410 } 411 412 private void runStartupCall(@NonNull String name, 413 @NonNull StartupCallParticipant participant, 414 @NonNull Runnable action, 415 long startupDeadline) throws Throwable { 416 AtomicReference<Throwable> failure = new AtomicReference<>(); 417 StartupCallParticipant exactParticipant = requireNonNull(participant); 418 exactParticipant.beginStartupCall(); 419 if (!this.activeStartupParticipant.compareAndSet(null, 420 exactParticipant)) { 421 exactParticipant.completeStartupCall(); 422 throw new IllegalStateException( 423 "A simulator startup call is already active"); 424 } 425 TrackedLifecycleCallRunner.Call<Void> call; 426 try { 427 call = this.callRunner.submit(requireNonNull(name), 428 exactParticipant.terminationGroup(), () -> { 429 try { 430 Throwable startupStop = startupStopFailure(); 431 if (startupStop != null) { 432 exactParticipant.terminationGroup() 433 .recordShutdownIntent(); 434 throw startupStop; 435 } 436 requireNonNull(action).run(); 437 return null; 438 } catch (Throwable throwable) { 439 failure.set(throwable); 440 if (throwable instanceof Exception exception) 441 throw exception; 442 throw (Error) throwable; 443 } finally { 444 exactParticipant.completeStartupCall(); 445 this.activeStartupParticipant.compareAndSet( 446 exactParticipant, null); 447 } 448 }); 449 } catch (RuntimeException | Error launchFailure) { 450 exactParticipant.completeStartupCall(); 451 this.activeStartupParticipant.compareAndSet( 452 exactParticipant, null); 453 throw launchFailure; 454 } 455 exactParticipant.installStartupCall(call); 456 call.completion().whenComplete((ignored, ignoredFailure) -> 457 this.waiter.signal()); 458 459 DeadlineWaiter.Outcome outcome; 460 try { 461 outcome = this.waiter.await(startupDeadline, 462 () -> call.isDone() 463 || this.stateMachine.shutdownRequested()); 464 } catch (InterruptedException interrupted) { 465 throw interrupted; 466 } 467 if (outcome == DeadlineWaiter.Outcome.DEADLINE_REACHED) 468 throw new TimeoutException( 469 "Soklet simulator startup deadline was reached"); 470 Throwable exactFailure = failure.get(); 471 if (exactFailure != null) 472 throw exactFailure; 473 ensureStartupMayContinue(); 474 if (!call.isDone()) 475 throw new IllegalStateException( 476 "Soklet simulator startup ended before the call completed"); 477 } 478 479 private void ensureStartupMayContinue() throws Throwable { 480 Throwable failure = startupStopFailure(); 481 if (failure != null) 482 throw failure; 483 } 484 485 @Nullable 486 private Throwable startupStopFailure() { 487 Optional<InternalTerminationEvent> event = 488 this.controllingEventElection.firstEvent(); 489 if (event.isPresent()) 490 return event.orElseThrow().cause().orElseGet(() -> 491 new IllegalStateException( 492 "A simulator participant terminated during startup")); 493 if (this.stateMachine.shutdownRequested()) 494 return new IllegalStateException( 495 "Simulator shutdown was requested during startup"); 496 return null; 497 } 498 499 private void ensureStartupDeadline(long startupDeadline) 500 throws TimeoutException { 501 if (LifecycleDeadlines.remainingNanos(startupDeadline, 502 this.clock.nanoTime()) == 0L) 503 throw new TimeoutException( 504 "Soklet simulator startup deadline was reached"); 505 } 506 507 @NonNull 508 private TeardownOutcome teardown( 509 @NonNull List<@NonNull ScopeParticipant> participants, 510 @NonNull StartupAttempt setupAttempt, 511 @NonNull InternalLifecyclePolicy policy, 512 @NonNull StartupBudget startupBudget, 513 @NonNull InternalStartupDisposition startupDisposition) { 514 InternalShutdownResult result = null; 515 List<Throwable> failures = new ArrayList<>(); 516 boolean restoreTeardownInterrupt = Thread.interrupted(); 517 boolean startupCallActiveAtIntent = setupAttempt.startupCallActive() 518 || participants.stream().anyMatch( 519 ScopeParticipant::startupCallActive); 520 if (startupCallActiveAtIntent) 521 this.startupCancelationRequired.set(true); 522 InternalLifecycleStateMachine.ShutdownRequest shutdownRequest = null; 523 AtomicReference<@Nullable Long> attemptedIntentNanos = 524 new AtomicReference<>(); 525 try { 526 shutdownRequest = this.controllingEventElection 527 .publishShutdownIntent(() -> { 528 long intentNanos = this.clock.nanoTime(); 529 attemptedIntentNanos.set(intentNanos); 530 // Admission closure and the owner cutoff are one election 531 // boundary relative to participant terminal events. 532 try { 533 requireNonNull(this.simulator).sealScope(); 534 } catch (Throwable failure) { 535 failures.add(failure); 536 } 537 return this.stateMachine.requestShutdownDetailed( 538 intentNanos); 539 }); 540 } catch (Throwable failure) { 541 failures.add(failure); 542 } 543 long intentNanos = shutdownRequest == null 544 ? Optional.ofNullable(attemptedIntentNanos.get()) 545 .orElseGet(this.clock::nanoTime) 546 : shutdownRequest.intentNanos(); 547 boolean cancellationRequired = 548 this.startupCancelationRequired.get(); 549 long cancellationDeadline = cancellationRequired 550 ? startupBudget.beginCancellation(intentNanos) : intentNanos; 551 long gracefulDeadline = LifecycleDeadlines.after(cancellationDeadline, 552 policy.gracefulShutdownTimeout()); 553 long forcedDeadline = LifecycleDeadlines.after(gracefulDeadline, 554 policy.forcedShutdownTimeout()); 555 556 List<InternalLifecycleCoordinator.Participant> 557 coordinatedParticipants = new ArrayList<>(); 558 for (ScopeParticipant participant : participants) 559 if (participant.committed()) 560 coordinatedParticipants.add(participant); 561 if (setupAttempt.startupCallActive()) 562 coordinatedParticipants.add(setupAttempt); 563 for (InternalLifecycleCoordinator.Participant participant 564 : coordinatedParticipants) 565 participant.terminationGroup().recordShutdownIntent(); 566 for (ScopeParticipant participant : participants) { 567 try { 568 participant.recordShutdownIntent(); 569 } catch (Throwable failure) { 570 failures.add(failure); 571 } 572 } 573 if (cancellationRequired) { 574 submitStartupCancelationQuiesce(participants, 575 gracefulDeadline); 576 setupAttempt.cancelStartupCall(); 577 for (ScopeParticipant participant : participants) 578 participant.cancelStartupCall(); 579 if (setupAttempt.startupCallActive() 580 || participants.stream().anyMatch( 581 ScopeParticipant::startupCallActive)) { 582 try { 583 this.waiter.await(cancellationDeadline, 584 () -> !setupAttempt.startupCallActive() 585 && participants.stream().noneMatch( 586 ScopeParticipant::startupCallActive)); 587 } catch (InterruptedException failure) { 588 restoreTeardownInterrupt = true; 589 failures.add(failure); 590 } 591 } 592 } 593 try { 594 InternalShutdownResult coordinated = 595 new InternalLifecycleCoordinator(this.clock, this.waiter, 596 this.callRunner).shutdown(coordinatedParticipants, 597 gracefulDeadline, forcedDeadline); 598 result = completeConfiguredParticipantResults(coordinated, 599 participants, startupDisposition); 600 } catch (InterruptedException failure) { 601 restoreTeardownInterrupt = true; 602 failures.add(failure); 603 } catch (Throwable failure) { 604 failures.add(failure); 605 } 606 try { 607 this.stateMachine.publishClosed(); 608 } catch (Throwable failure) { 609 failures.add(failure); 610 } 611 612 if (result == null || !failures.isEmpty()) 613 result = incompleteResult(participants, setupAttempt, 614 startupDisposition, result, failures); 615 616 if (result.isComplete()) { 617 try { 618 requireNonNull(this.simulator).releaseMcpScopeEvidence(); 619 } catch (Throwable failure) { 620 failures.add(failure); 621 result = incompleteResult(participants, setupAttempt, 622 startupDisposition, result, failures); 623 } 624 } 625 626 InternalShutdownResult resultToPublish = result; 627 try { 628 for (ScopeParticipant participant : participants) 629 participant.finalizeAndPublishResult(resultToPublish); 630 } catch (Throwable failure) { 631 failures.add(failure); 632 result = incompleteResult(participants, setupAttempt, 633 startupDisposition, result, failures); 634 for (ScopeParticipant participant : participants) { 635 try { 636 participant.publishResultAfterFailure(result); 637 } catch (Throwable publicationFailure) { 638 if (!sameInstance(publicationFailure, failure)) 639 failure.addSuppressed(publicationFailure); 640 } 641 } 642 } 643 if (result.isComplete()) 644 requireNonNull(this.simulator).releaseHttpAndSseScopeState(); 645 646 Throwable cause = failures.isEmpty() ? null : failures.get(0); 647 for (int index = 1; index < failures.size(); index++) { 648 Throwable additional = failures.get(index); 649 if (!sameInstance(additional, cause)) 650 requireNonNull(cause).addSuppressed(additional); 651 } 652 SokletShutdownIncompleteException failure = result.isComplete() ? null 653 : new SokletShutdownIncompleteException(result, this, cause); 654 if (restoreTeardownInterrupt) 655 Thread.currentThread().interrupt(); 656 return new TeardownOutcome(result, failure); 657 } 658 659 private void submitStartupCancelationQuiesce( 660 @NonNull List<@NonNull ScopeParticipant> participants, 661 long gracefulDeadlineNanos) { 662 ShutdownContext context = new ShutdownContext( 663 ShutdownPhase.GRACEFUL, this.clock, 664 gracefulDeadlineNanos); 665 for (ScopeParticipant participant : requireNonNull(participants)) { 666 if (!participant.committed()) 667 continue; 668 try { 669 this.callRunner.submit("simulator-cancellation-quiesce-" 670 + participant.kind().name().toLowerCase(Locale.ROOT), 671 participant.terminationGroup(), () -> { 672 participant.runtime().shutdownGracefully(context); 673 return null; 674 }); 675 } catch (RuntimeException | Error launchFailure) { 676 participant.terminationGroup().signalFailure( 677 participant.terminationGroup().root(), launchFailure); 678 } 679 } 680 } 681 682 @NonNull 683 private InternalShutdownResult incompleteResult( 684 @NonNull List<@NonNull ScopeParticipant> participants, 685 @NonNull StartupAttempt setupAttempt, 686 @NonNull InternalStartupDisposition startupDisposition, 687 @Nullable InternalShutdownResult coordinated, 688 @NonNull List<? extends @NonNull Throwable> failures) { 689 List<InternalLifecycleComponentShutdownResult> results; 690 if (coordinated == null) { 691 results = new ArrayList<>(); 692 for (ScopeParticipant participant : participants) 693 if (participant.committed()) 694 participant.freezeForClassification(); 695 setupAttempt.freezeForClassification(); 696 EnumMap<InternalLifecycleComponentType, 697 InternalTerminationGroup.EvidenceSnapshot> evidenceByKind = 698 new EnumMap<>(InternalLifecycleComponentType.class); 699 for (ScopeParticipant participant : participants) 700 if (participant.committed()) 701 evidenceByKind.put(participant.kind(), participant 702 .terminationGroup().freezeEvidence()); 703 InternalTerminationGroup.EvidenceSnapshot setupEvidence = 704 setupAttempt.terminationGroup().freezeEvidence(); 705 for (ScopeParticipant participant : participants) { 706 if (!participant.committed()) { 707 results.add(new InternalLifecycleComponentShutdownResult( 708 participant.kind(), 709 InternalLifecycleComponentShutdownDisposition.NOT_STARTED, 710 List.of(), Set.of())); 711 continue; 712 } 713 EnumSet<InternalResidualActivityType> residual = EnumSet 714 .noneOf(InternalResidualActivityType.class); 715 residual.addAll(participant.residualActivity()); 716 InternalTerminationGroup.EvidenceSnapshot evidence = 717 requireNonNull(evidenceByKind.get(participant.kind())); 718 if (evidence.trackedLifecycleCalls() > 0) 719 residual.add(InternalResidualActivityType.LIFECYCLE_CALL); 720 if (evidence.admittedWork() > 0) 721 residual.add(InternalResidualActivityType.CALLBACK); 722 results.add(new InternalLifecycleComponentShutdownResult( 723 participant.kind(), 724 InternalLifecycleComponentShutdownDisposition.TERMINATION_UNKNOWN, 725 mergedFailures(evidence, failures), residual)); 726 } 727 if (setupAttempt.startupCallActive()) { 728 EnumSet<InternalResidualActivityType> residual = EnumSet 729 .noneOf(InternalResidualActivityType.class); 730 if (setupEvidence.trackedLifecycleCalls() > 0) 731 residual.add(InternalResidualActivityType.LIFECYCLE_CALL); 732 if (setupEvidence.admittedWork() > 0) 733 residual.add(InternalResidualActivityType.CALLBACK); 734 results.add(new InternalLifecycleComponentShutdownResult( 735 InternalLifecycleComponentType.FRAMEWORK, 736 InternalLifecycleComponentShutdownDisposition.TERMINATION_UNKNOWN, 737 mergedFailures(setupEvidence, failures), residual)); 738 } 739 if (!failures.isEmpty() && results.stream().noneMatch(result -> 740 result.disposition() 741 == InternalLifecycleComponentShutdownDisposition.RESIDUAL_ACTIVITY 742 || result.disposition() 743 == InternalLifecycleComponentShutdownDisposition 744 .TERMINATION_UNKNOWN)) 745 results.add(new InternalLifecycleComponentShutdownResult( 746 InternalLifecycleComponentType.FRAMEWORK, 747 InternalLifecycleComponentShutdownDisposition.TERMINATION_UNKNOWN, 748 failures, Set.of())); 749 } else { 750 results = new ArrayList<>(coordinated.participantResults()); 751 if (!failures.isEmpty()) { 752 int targetIndex = -1; 753 for (int index = 0; index < results.size(); index++) { 754 InternalLifecycleComponentShutdownDisposition disposition = 755 results.get(index).disposition(); 756 if (disposition 757 == InternalLifecycleComponentShutdownDisposition.RESIDUAL_ACTIVITY 758 || disposition == InternalLifecycleComponentShutdownDisposition 759 .TERMINATION_UNKNOWN) { 760 targetIndex = index; 761 break; 762 } 763 } 764 if (targetIndex < 0) 765 for (int index = 0; index < results.size(); index++) 766 if (results.get(index).kind() 767 == InternalLifecycleComponentType.MCP) { 768 targetIndex = index; 769 break; 770 } 771 if (targetIndex < 0 && !results.isEmpty()) 772 targetIndex = 0; 773 if (targetIndex < 0) { 774 results.add(new InternalLifecycleComponentShutdownResult( 775 InternalLifecycleComponentType.FRAMEWORK, 776 InternalLifecycleComponentShutdownDisposition.TERMINATION_UNKNOWN, 777 failures, Set.of())); 778 } else { 779 InternalLifecycleComponentShutdownResult existing = 780 results.get(targetIndex); 781 List<Throwable> attributedFailures = new ArrayList<>( 782 existing.failures()); 783 for (Throwable failure : failures) 784 if (attributedFailures.stream().noneMatch( 785 candidate -> sameInstance(candidate, failure))) 786 attributedFailures.add(failure); 787 InternalLifecycleComponentShutdownDisposition disposition = 788 existing.disposition() 789 == InternalLifecycleComponentShutdownDisposition 790 .RESIDUAL_ACTIVITY 791 ? existing.disposition() 792 : InternalLifecycleComponentShutdownDisposition 793 .TERMINATION_UNKNOWN; 794 results.set(targetIndex, 795 new InternalLifecycleComponentShutdownResult(existing.kind(), 796 disposition, attributedFailures, 797 existing.residualActivity())); 798 } 799 } 800 } 801 return new InternalShutdownResultAggregator().aggregate( 802 startupDisposition, results); 803 } 804 805 @NonNull 806 private List<@NonNull Throwable> mergedFailures( 807 InternalTerminationGroup.@NonNull EvidenceSnapshot evidence, 808 @NonNull List<? extends @NonNull Throwable> infrastructureFailures) { 809 List<Throwable> merged = new ArrayList<>( 810 requireNonNull(evidence).failures()); 811 for (Throwable failure : requireNonNull(infrastructureFailures)) 812 if (merged.stream().noneMatch(candidate -> sameInstance(candidate, failure))) 813 merged.add(failure); 814 return List.copyOf(merged); 815 } 816 817 @NonNull 818 private InternalShutdownResult completeConfiguredParticipantResults( 819 @NonNull InternalShutdownResult result, 820 @NonNull List<@NonNull ScopeParticipant> participants, 821 @NonNull InternalStartupDisposition startupDisposition) { 822 EnumMap<InternalLifecycleComponentType, InternalLifecycleComponentShutdownResult> 823 resultsByKind = new EnumMap<>(InternalLifecycleComponentType.class); 824 for (InternalLifecycleComponentShutdownResult participantResult 825 : requireNonNull(result).participantResults()) 826 resultsByKind.put(participantResult.kind(), participantResult); 827 for (ScopeParticipant participant : requireNonNull(participants)) { 828 InternalLifecycleComponentShutdownResult participantResult = 829 resultsByKind.get(participant.kind()); 830 if (participantResult == null) { 831 resultsByKind.put(participant.kind(), 832 new InternalLifecycleComponentShutdownResult(participant.kind(), 833 InternalLifecycleComponentShutdownDisposition.NOT_STARTED, 834 List.of(), Set.of())); 835 } else if (!participant.startAttempted() 836 && participantResult.disposition() 837 == InternalLifecycleComponentShutdownDisposition 838 .GRACEFUL_TERMINATION) { 839 resultsByKind.put(participant.kind(), 840 new InternalLifecycleComponentShutdownResult(participant.kind(), 841 InternalLifecycleComponentShutdownDisposition.NOT_STARTED, 842 participantResult.failures(), 843 participantResult.residualActivity())); 844 } 845 } 846 return new InternalShutdownResultAggregator().aggregate( 847 requireNonNull(startupDisposition), 848 List.copyOf(resultsByKind.values())); 849 } 850 851 private void requestOwnerShutdown() { 852 if (this.activeStartupParticipant.get() != null) 853 this.startupCancelationRequired.set(true); 854 DefaultSimulator activeSimulator = this.simulator; 855 try { 856 this.controllingEventElection.publishShutdownIntent(() -> { 857 long intentNanos = this.clock.nanoTime(); 858 try { 859 if (activeSimulator != null) 860 activeSimulator.sealScope(); 861 } catch (Throwable ignored) { 862 // Owner intent still publishes if local sealing fails. 863 } 864 return this.stateMachine.requestShutdownDetailed(intentNanos); 865 }); 866 } finally { 867 this.waiter.signal(); 868 } 869 } 870 871 @NonNull 872 private DefaultSimulator simulator() { 873 return requireNonNull(this.simulator, 874 "The simulator runtime has not been attached"); 875 } 876 } 877 878 private interface StartupCallParticipant 879 extends InternalLifecycleCoordinator.Participant { 880 void beginStartupCall(); 881 882 void installStartupCall( 883 TrackedLifecycleCallRunner.@NonNull Call<@Nullable Void> call); 884 885 void completeStartupCall(); 886 887 void cancelStartupCall(); 888 } 889 890 @ThreadSafe 891 private static final class StartupBudget { 892 private final long startupDeadline; 893 @NonNull 894 private final Duration cancellationDuration; 895 @NonNull 896 private final AtomicReference<@Nullable Long> cancellationDeadline; 897 898 private StartupBudget(long startupDeadline, 899 @NonNull Duration cancellationDuration) { 900 this.startupDeadline = startupDeadline; 901 this.cancellationDuration = requireNonNull(cancellationDuration); 902 this.cancellationDeadline = new AtomicReference<>(); 903 } 904 905 private long startupDeadline() { 906 return this.startupDeadline; 907 } 908 909 private long beginCancellation(long shutdownIntentNanos) { 910 long proposed = LifecycleDeadlines.after(shutdownIntentNanos, 911 this.cancellationDuration); 912 this.cancellationDeadline.compareAndSet(null, proposed); 913 return requireNonNull(this.cancellationDeadline.get()); 914 } 915 916 } 917 918 @ThreadSafe 919 private static final class StartupAttempt implements StartupCallParticipant { 920 @NonNull 921 private final AdmissionFence admissionFence; 922 @NonNull 923 private final InternalTerminationGroup terminationGroup; 924 @NonNull 925 private final InternalTransportTerminationSignal terminationSignal; 926 @NonNull 927 private final DirectParticipantPhaseGate phaseGate; 928 @NonNull 929 private final AtomicReference<TrackedLifecycleCallRunner.@Nullable Call<@Nullable Void>> 930 startupCall; 931 932 private StartupAttempt(@NonNull Scope scope, 933 @NonNull Object executionOwnerToken) { 934 Scope exactScope = requireNonNull(scope); 935 this.admissionFence = new AdmissionFence(false, 936 exactScope.waiter::signal); 937 this.terminationGroup = new InternalTerminationGroup( 938 this.admissionFence, exactScope.waiter::signal, 939 exactScope.workers, requireNonNull(executionOwnerToken)); 940 this.terminationSignal = new InternalTransportTerminationSignal( 941 this.terminationGroup, this.terminationGroup.root()); 942 this.phaseGate = new DirectParticipantPhaseGate(); 943 this.startupCall = new AtomicReference<>(); 944 this.terminationGroup.commit(); 945 } 946 947 @Override 948 public void beginStartupCall() { 949 if (!this.phaseGate.claimStart()) 950 throw new IllegalStateException( 951 "Simulator framework setup was already attempted"); 952 } 953 954 @Override 955 public void installStartupCall( 956 TrackedLifecycleCallRunner.@NonNull Call<@Nullable Void> call) { 957 TrackedLifecycleCallRunner.Call<Void> exactCall = requireNonNull(call); 958 this.startupCall.set(exactCall); 959 if (!this.phaseGate.startupCallActive()) 960 this.startupCall.compareAndSet(exactCall, null); 961 } 962 963 @Override 964 public void completeStartupCall() { 965 this.startupCall.set(null); 966 this.phaseGate.completeStartCall(); 967 this.terminationGroup.recordShutdownIntent(); 968 this.terminationSignal.signalTerminated(); 969 } 970 971 @Override 972 public void cancelStartupCall() { 973 TrackedLifecycleCallRunner.Call<Void> call = this.startupCall.get(); 974 if (call != null) 975 call.cancel(); 976 } 977 978 @Override 979 public boolean startupCallActive() { 980 return this.phaseGate.startupCallActive(); 981 } 982 983 @Override 984 public void freezeForClassification() { 985 this.phaseGate.freezeForClassification(); 986 } 987 988 @NonNull 989 @Override 990 public InternalLifecycleComponentType kind() { 991 return InternalLifecycleComponentType.FRAMEWORK; 992 } 993 994 @NonNull 995 @Override 996 public AdmissionFence admissionFence() { 997 return this.admissionFence; 998 } 999 1000 @NonNull 1001 @Override 1002 public InternalTerminationGroup terminationGroup() { 1003 return this.terminationGroup; 1004 } 1005 1006 @NonNull 1007 @Override 1008 public InternalTransportRuntime runtime() { 1009 return new InternalTransportRuntime() { 1010 @Override 1011 public void start(@NonNull StartupContext context) { 1012 } 1013 1014 @Override 1015 public void shutdownGracefully(@NonNull ShutdownContext context) { 1016 } 1017 1018 @Override 1019 public void shutdownForcibly(@NonNull ShutdownContext context) { 1020 } 1021 }; 1022 } 1023 1024 @NonNull 1025 @Override 1026 public Set<@NonNull InternalResidualActivityType> residualActivity() { 1027 return Set.of(); 1028 } 1029 } 1030 1031 @Immutable 1032 private record TeardownOutcome( 1033 @NonNull InternalShutdownResult result, 1034 @Nullable SokletShutdownIncompleteException failure) { 1035 private TeardownOutcome { 1036 requireNonNull(result); 1037 } 1038 } 1039 1040 static final class ScopeParticipant 1041 implements StartupCallParticipant { 1042 @NonNull 1043 private final InternalLifecycleComponentType kind; 1044 @NonNull 1045 private final Scope scope; 1046 @NonNull 1047 private final AdmissionFence admissionFence; 1048 @NonNull 1049 private final InternalTerminationGroup terminationGroup; 1050 @NonNull 1051 private final InternalTransportTerminationSignal terminationSignal; 1052 @NonNull 1053 private final InternalTransportRuntime runtime; 1054 private final @Nullable McpTransportLifecycleAdapter mcpLifecycleAdapter; 1055 private final McpTransportLifecycleAdapter.@Nullable Generation 1056 mcpGeneration; 1057 @NonNull 1058 private final DirectParticipantPhaseGate phaseGate; 1059 @NonNull 1060 private final AtomicReference<TrackedLifecycleCallRunner.@Nullable Call<@Nullable Void>> 1061 startupCall; 1062 @NonNull 1063 private final AtomicReference<TrackedLifecycleCallRunner.@Nullable Call<@Nullable Void>> 1064 proofObservation; 1065 @NonNull 1066 private final LifecycleResidualActivity residualActivity = new LifecycleResidualActivity(); 1067 @NonNull 1068 private final AtomicBoolean startAttempted; 1069 @NonNull 1070 private final Object phaseDeliveryLock; 1071 private boolean committed; 1072 private boolean mcpGenerationCommitted; 1073 1074 private ScopeParticipant(@NonNull InternalLifecycleComponentType kind, 1075 @NonNull Scope scope, @Nullable DefaultMcpServer mcpServer, 1076 @NonNull Object executionOwnerToken) { 1077 this.kind = requireNonNull(kind); 1078 this.scope = requireNonNull(scope); 1079 if (this.kind == InternalLifecycleComponentType.MCP) { 1080 DefaultMcpServer exactServer = requireNonNull(mcpServer, 1081 "The MCP simulator participant requires a server"); 1082 this.mcpLifecycleAdapter = exactServer.getLifecycleAdapter(); 1083 this.mcpGeneration = this.mcpLifecycleAdapter 1084 .newExternallyCoordinatedGeneration(scope.waiter, 1085 scope.workers, requireNonNull(executionOwnerToken), 1086 scope::requestOwnerShutdown, 1087 scope::requestOwnerShutdown, 1088 scope.controllingEventElection); 1089 this.admissionFence = this.mcpGeneration.admissionFence(); 1090 this.terminationGroup = this.mcpGeneration.terminationGroup(); 1091 } else { 1092 if (mcpServer != null) 1093 throw new IllegalArgumentException( 1094 "Only an MCP simulator participant may receive an MCP server"); 1095 this.mcpLifecycleAdapter = null; 1096 this.mcpGeneration = null; 1097 this.admissionFence = new AdmissionFence(false, 1098 scope.waiter::signal); 1099 this.terminationGroup = new InternalTerminationGroup( 1100 this.admissionFence, scope.waiter::signal, scope.workers); 1101 } 1102 this.terminationSignal = new InternalTransportTerminationSignal( 1103 this.terminationGroup, this.terminationGroup.root()); 1104 this.runtime = new ScopeRuntime(); 1105 this.phaseGate = new DirectParticipantPhaseGate(); 1106 this.startupCall = new AtomicReference<>(); 1107 this.proofObservation = new AtomicReference<>(); 1108 this.startAttempted = new AtomicBoolean(); 1109 this.phaseDeliveryLock = new Object(); 1110 } 1111 1112 private void commit() { 1113 if (this.mcpLifecycleAdapter == null) 1114 this.terminationGroup.commit(); 1115 else { 1116 this.mcpLifecycleAdapter.commitExternallyCoordinatedGeneration( 1117 requireNonNull(this.mcpGeneration)); 1118 this.mcpGenerationCommitted = true; 1119 } 1120 this.committed = true; 1121 } 1122 1123 private void start(@NonNull StartupContext context) { 1124 this.startAttempted.set(true); 1125 this.runtime.start(requireNonNull(context)); 1126 } 1127 1128 private boolean startAttempted() { 1129 return this.startAttempted.get(); 1130 } 1131 1132 private boolean committed() { 1133 return this.committed; 1134 } 1135 1136 private boolean openAdmission() { 1137 return this.mcpLifecycleAdapter == null 1138 ? this.admissionFence.open() 1139 : this.mcpLifecycleAdapter.openExternallyCoordinatedAdmission( 1140 requireNonNull(this.mcpGeneration)); 1141 } 1142 1143 private void recordShutdownIntent() { 1144 if (this.mcpLifecycleAdapter != null && this.mcpGenerationCommitted) 1145 this.mcpLifecycleAdapter.recordExternallyCoordinatedShutdownIntent( 1146 requireNonNull(this.mcpGeneration)); 1147 } 1148 1149 private void finalizeAndPublishResult( 1150 @NonNull InternalShutdownResult result) { 1151 if (this.mcpLifecycleAdapter == null) 1152 return; 1153 if (!this.mcpGenerationCommitted) { 1154 this.mcpLifecycleAdapter.discardExternallyCoordinatedGeneration( 1155 requireNonNull(this.mcpGeneration)); 1156 return; 1157 } 1158 InternalLifecycleComponentShutdownResult participantResult = requireNonNull( 1159 result).participantResult(InternalLifecycleComponentType.MCP) 1160 .orElseThrow(() -> new IllegalArgumentException( 1161 "The simulator result is missing its MCP participant")); 1162 Throwable finalizationFailure = this.mcpLifecycleAdapter 1163 .finalizeExternallyCoordinatedEvidence( 1164 requireNonNull(this.mcpGeneration), participantResult) 1165 .orElse(null); 1166 if (finalizationFailure != null) 1167 throw new IllegalStateException( 1168 "Unable to finalize MCP simulator lifecycle evidence", 1169 finalizationFailure); 1170 this.mcpLifecycleAdapter.publishExternallyCoordinatedResult( 1171 requireNonNull(this.mcpGeneration), result); 1172 } 1173 1174 private void publishResultAfterFailure( 1175 @NonNull InternalShutdownResult result) { 1176 if (this.mcpLifecycleAdapter != null && this.mcpGenerationCommitted) 1177 this.mcpLifecycleAdapter 1178 .publishExternallyCoordinatedOwnerResultAfterFailure( 1179 requireNonNull(this.mcpGeneration), 1180 requireNonNull(result)); 1181 } 1182 1183 @Override 1184 public void beginStartupCall() { 1185 if (!this.phaseGate.claimStart()) 1186 throw new IllegalStateException( 1187 "Simulator participant startup is already active"); 1188 } 1189 1190 @Override 1191 public void installStartupCall( 1192 TrackedLifecycleCallRunner.@NonNull Call<@Nullable Void> call) { 1193 TrackedLifecycleCallRunner.Call<Void> exactCall = requireNonNull(call); 1194 this.startupCall.set(exactCall); 1195 if (!this.phaseGate.startupCallActive()) 1196 this.startupCall.compareAndSet(exactCall, null); 1197 } 1198 1199 @Override 1200 public void completeStartupCall() { 1201 this.startupCall.set(null); 1202 synchronized (this.phaseDeliveryLock) { 1203 ShutdownContext catchUp = this.phaseGate.completeStartCall(); 1204 try { 1205 if (catchUp != null) 1206 applyShutdownPhase(catchUp); 1207 } catch (Throwable failure) { 1208 this.terminationGroup.signalFailure( 1209 this.terminationGroup.root(), failure); 1210 } 1211 } 1212 this.scope.waiter.signal(); 1213 } 1214 1215 @Override 1216 public void cancelStartupCall() { 1217 TrackedLifecycleCallRunner.Call<Void> call = this.startupCall.get(); 1218 if (call != null) 1219 call.cancel(); 1220 } 1221 1222 @Override 1223 public boolean startupCallActive() { 1224 return this.phaseGate.startupCallActive(); 1225 } 1226 1227 @Override 1228 public void freezeForClassification() { 1229 synchronized (this.phaseDeliveryLock) { 1230 this.phaseGate.freezeForClassification(); 1231 TrackedLifecycleCallRunner.Call<Void> observer = 1232 this.proofObservation.getAndSet(null); 1233 if (observer != null && !observer.isDone()) 1234 observer.cancel(); 1235 } 1236 } 1237 1238 private void requestShutdownPhase( 1239 @NonNull ShutdownContext context) { 1240 synchronized (this.phaseDeliveryLock) { 1241 ShutdownContext delivery = this.phaseGate.requestPhase( 1242 requireNonNull(context)); 1243 if (delivery != null) 1244 applyShutdownPhase(delivery); 1245 } 1246 } 1247 1248 private void applyShutdownPhase( 1249 @NonNull ShutdownContext context) { 1250 DefaultSimulator simulator = this.scope.simulator(); 1251 boolean forced = requireNonNull(context).getShutdownPhase() == ShutdownPhase.FORCED; 1252 if (this.kind == InternalLifecycleComponentType.HTTP) { 1253 if (forced) 1254 simulator.forceHttpScope(); 1255 else 1256 simulator.quiesceHttpScope(); 1257 } else if (this.kind == InternalLifecycleComponentType.SSE) { 1258 if (forced) 1259 simulator.forceSseScope(); 1260 else 1261 simulator.quiesceSseScope(); 1262 } else if (forced) 1263 simulator.forceMcpScope(); 1264 else 1265 simulator.quiesceMcpScope(); 1266 beginProofObservation(context); 1267 } 1268 1269 private void beginProofObservation( 1270 @NonNull ShutdownContext context) { 1271 if (this.kind != InternalLifecycleComponentType.MCP 1272 && (this.kind == InternalLifecycleComponentType.HTTP 1273 ? this.scope.simulator().httpScopeTerminationProven() 1274 : this.scope.simulator().sseScopeTerminationProven())) { 1275 this.residualActivity.recordTermination(); 1276 this.terminationSignal.signalTerminated(); 1277 return; 1278 } 1279 ShutdownContext exactContext = requireNonNull(context); 1280 TrackedLifecycleCallRunner.Call<Void> previous = 1281 this.proofObservation.getAndSet(null); 1282 if (previous != null && !previous.isDone()) 1283 previous.cancel(); 1284 TrackedLifecycleCallRunner.Call<Void> observer; 1285 try { 1286 observer = this.scope.callRunner.submit( 1287 "simulator-" + this.kind.name().toLowerCase(Locale.ROOT) + "-termination-observer-" 1288 + exactContext.getShutdownPhase().name().toLowerCase(Locale.ROOT), 1289 this.terminationGroup, () -> { 1290 try { 1291 this.residualActivity.recordObservation(queryResidualActivity()); 1292 if (this.kind == InternalLifecycleComponentType.HTTP 1293 ? this.scope.simulator().awaitHttpScopeTermination( 1294 exactContext.absoluteDeadlineNanos(), this.scope.clock) 1295 : this.kind == InternalLifecycleComponentType.SSE 1296 ? this.scope.simulator().awaitSseScopeTermination( 1297 exactContext.absoluteDeadlineNanos(), this.scope.clock) 1298 : this.scope.simulator().awaitMcpScopeTermination( 1299 exactContext.absoluteDeadlineNanos(), this.scope.clock)) { 1300 this.residualActivity.recordTermination(); 1301 this.terminationSignal.signalTerminated(); 1302 } else { 1303 this.residualActivity.recordObservation(queryResidualActivity()); 1304 } 1305 } catch (InterruptedException phaseAdvance) { 1306 // A phase advance or result freeze ends only this observation. 1307 } 1308 return null; 1309 }); 1310 } catch (RuntimeException | Error launchFailure) { 1311 throw launchFailure; 1312 } 1313 this.proofObservation.set(observer); 1314 observer.completion().whenComplete((ignored, ignoredFailure) -> { 1315 this.proofObservation.compareAndSet(observer, null); 1316 this.scope.waiter.signal(); 1317 }); 1318 if (observer.isDone()) 1319 this.proofObservation.compareAndSet(observer, null); 1320 } 1321 1322 @NonNull 1323 @Override 1324 public InternalLifecycleComponentType kind() { 1325 return this.kind; 1326 } 1327 1328 @NonNull 1329 @Override 1330 public AdmissionFence admissionFence() { 1331 return this.admissionFence; 1332 } 1333 1334 @NonNull 1335 @Override 1336 public InternalTerminationGroup terminationGroup() { 1337 return this.terminationGroup; 1338 } 1339 1340 @NonNull 1341 @Override 1342 public InternalTransportRuntime runtime() { 1343 return this.runtime; 1344 } 1345 1346 @NonNull 1347 @Override 1348 public Set<@NonNull InternalResidualActivityType> residualActivity() { 1349 // The mock projections contain only framework-owned counters and 1350 // conservatively retained references, never application status calls. 1351 if (this.residualActivity.terminationProven()) 1352 return Set.of(); 1353 return this.residualActivity.snapshot(queryResidualActivity()); 1354 } 1355 1356 @NonNull 1357 private Set<@NonNull InternalResidualActivityType> queryResidualActivity() { 1358 return switch (this.kind) { 1359 case HTTP -> this.scope.simulator().httpScopeResidualActivity(); 1360 case MCP -> this.scope.simulator().mcpScopeResidualActivity(); 1361 case SSE -> this.scope.simulator().sseScopeResidualActivity(); 1362 default -> Set.of(); 1363 }; 1364 } 1365 1366 private final class ScopeRuntime implements InternalTransportRuntime { 1367 @Override 1368 public void start(@NonNull StartupContext context) { 1369 requireNonNull(context); 1370 if (kind == InternalLifecycleComponentType.SSE) { 1371 scope.sseServer.start(); 1372 return; 1373 } 1374 // HTTP has no off-network listener to start. MCP owns one fresh 1375 // application/executor/subscription generation before readiness. 1376 if (kind == InternalLifecycleComponentType.MCP) { 1377 McpTransportLifecycleAdapter adapter = requireNonNull( 1378 mcpLifecycleAdapter); 1379 McpTransportLifecycleAdapter.Generation generation = 1380 requireNonNull(mcpGeneration); 1381 adapter.runExternallyCoordinatedStart(generation, () -> { 1382 try { 1383 adapter.beginStart(); 1384 scope.simulator().openMcpScope(); 1385 } catch (RuntimeException | Error failure) { 1386 adapter.failedStart(generation, failure, false); 1387 throw failure; 1388 } 1389 }); 1390 adapter.markReady(generation); 1391 } 1392 } 1393 1394 @Override 1395 public void shutdownGracefully(@NonNull ShutdownContext context) { 1396 requestShutdownPhase(requireNonNull(context)); 1397 } 1398 1399 @Override 1400 public void shutdownForcibly(@NonNull ShutdownContext context) { 1401 requestShutdownPhase(requireNonNull(context)); 1402 } 1403 } 1404 } 1405 1406 @Immutable 1407 private record ResolvedScopeTransports( 1408 @NonNull Optional<@NonNull MockHttpServer> httpServer, 1409 @NonNull Optional<@NonNull MockSseServer> sseServer, 1410 @NonNull Optional<@NonNull DefaultMcpServer> mcpServer) { 1411 private ResolvedScopeTransports { 1412 requireNonNull(httpServer); 1413 requireNonNull(sseServer); 1414 requireNonNull(mcpServer); 1415 } 1416 } 1417} 1418 1419interface SimulatorMcpBuildRegistrar { 1420 void verifyBuildAllowed(); 1421 1422 void register(@NonNull DefaultMcpServer server); 1423} 1424 1425interface SimulatorConfigurationScopeIdentity { 1426 @NonNull 1427 Object simulatorConfigurationIdentity(); 1428} 1429 1430interface SimulatorScopeDispatchGate { 1431 @NonNull 1432 Runnable enter(@NonNull InternalLifecycleComponentType kind); 1433 1434 void seal(); 1435} 1436 1437@ThreadSafe 1438final class ScopeDispatchGate implements SimulatorScopeDispatchGate { 1439 @NonNull 1440 private final Map<@NonNull InternalLifecycleComponentType, @NonNull AdmissionFence> fences; 1441 @NonNull 1442 private final AtomicBoolean sealed; 1443 1444 ScopeDispatchGate(@NonNull List<SokletSimulator.@NonNull ScopeParticipant> participants) { 1445 EnumMap<InternalLifecycleComponentType, AdmissionFence> fences = 1446 new EnumMap<>(InternalLifecycleComponentType.class); 1447 for (SokletSimulator.ScopeParticipant participant 1448 : requireNonNull(participants)) 1449 fences.put(participant.kind(), participant.admissionFence()); 1450 this.fences = Collections.unmodifiableMap(fences); 1451 this.sealed = new AtomicBoolean(); 1452 } 1453 1454 @NonNull 1455 @Override 1456 public Runnable enter(@NonNull InternalLifecycleComponentType kind) { 1457 if (this.sealed.get()) 1458 throw new IllegalStateException("The simulator scope is closed."); 1459 AdmissionFence fence = this.fences.get(requireNonNull(kind)); 1460 if (fence == null) 1461 return () -> { 1462 }; 1463 return fence.tryAdmit() 1464 .<Runnable>map(admission -> admission::close) 1465 .orElseThrow(() -> new IllegalStateException( 1466 "The simulator scope is closed.")); 1467 } 1468 1469 @Override 1470 public void seal() { 1471 if (!this.sealed.compareAndSet(false, true)) 1472 return; 1473 for (AdmissionFence fence : this.fences.values()) 1474 fence.close(); 1475 } 1476}