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}