Skip to content

Reentrant Execute harness + the file-sink leak it found (FT-9) - #454

Draft
ChrisonSimtian wants to merge 6 commits into
Fallout-build:mainfrom
ChrisonSimtian:engine/ft9-reentrancy-harness
Draft

Reentrant Execute harness + the file-sink leak it found (FT-9)#454
ChrisonSimtian wants to merge 6 commits into
Fallout-build:mainfrom
ChrisonSimtian:engine/ft9-reentrancy-harness

Conversation

@ChrisonSimtian

@ChrisonSimtian ChrisonSimtian commented Jun 30, 2026

Copy link
Copy Markdown
Collaborator

Stacked on #453#452 (FT-6 → FT-4) — the diff carries their commits until they merge. FT-1 and FT-2 have landed and are gone from it.

Engine de-statification ([Foundation] epic #315) — FT-9 / #314.

Rewritten. The original FT-9 commit added a BuildContextSpecs.cs whose five assertions have all since landed with the steps they belong to — FT-2 (#451, merged), FT-4 (#452), FT-6 (#453). Replaying it onto today's main would have been a conflict resolving to nothing. What was never delivered is the part its own description deferred: an actual reentrant Execute<T> run. That is what this PR is now.

BuildManagerReentrancySpecs runs a fixture build end-to-end — targets, logging pipeline, build extensions — twice in one process, asserting: both runs exit 0; the second gets its own BuildContext / ParameterService / InMemorySink; it opens on an empty sink; and the static facades inside a run resolve to that run.

It found a bug — fixed in the first commit

Host.WriteErrorsAndWarnings swaps Log.Logger for a console-only logger to re-render a run's warnings. The logger it replaces owns that run's file sinks, and assigning Log.Logger does not dispose it — so it leaked and held build.log open for the life of the process.

Any second in-process run whose predecessor logged a warning then threw in Logging.DeleteOldLogFiles truncating that file, and returned the error exit code silently — the throw lands before Configure reassigns Log.Logger, so Execute's own Host.Error went to an already-disposed logger. Fix: dispose the replaced logger. Nothing is lost; from that point the run logs through the console-only logger regardless.

Verified as a real guard: reverting the fix fails 4 of the 5 new specs.

Harness caveats

Two process-wide inputs have to be pinned for a build to run under a test host, and both are de-statification candidates in their own right:

  • EnvironmentInfo.ArgumentParserExecute resolves invoked targets from the process command line, which under a test host is the runner's own argv. Swapped for a controlled parser, restored after.
  • FalloutBuild.RootDirectory — resolved once in a static constructor, so it cannot be varied per run. It lands on this repo's root; the harness asserts the .fallout marker is there, because UpdateNotificationAttribute prompts for a key press on a local build without one (guard, not hang).

Out of scope: concurrent runs. The harness is sequential, and the process-wide singletons FT-2 called out (value-injection cache, tool-path resolver config) are still shared.

Full build + full spec suite green (14 projects, 0 failures; Build.Specs 164 → 169).

🤖 Generated with Claude Code

@ChrisonSimtian ChrisonSimtian added the target/vCurrent Targets the current version label Jun 30, 2026
@ChrisonSimtian ChrisonSimtian added target/vNext Targets the next calendar-version and removed target/vCurrent Targets the current version labels Jul 20, 2026
The active ParameterService is now the per-run instance held on BuildContext.Parameters; the static
ParameterService.Instance becomes a facade over BuildContext.Current. This is the first service to
ride the FT-2 rail: production and tests now exercise the same instance form (killing the test/prod
divergence — tests already `new ParameterService(funcs)`), and the per-run instance + its mutable
fields (ArgumentsFromFilesService / ArgumentsFromCommitMessageService) no longer leak across runs.

A lazy fallback covers access outside a build run (no cross-run state to leak there); it can retire
once that path is confirmed dead. Non-breaking — the static API is unchanged.
Four cases on the FT-4 seam: the static facade resolves the running context's instance, it falls
back to a stable ambient instance outside a run, each run gets its own service, and a mutated
per-run field does not survive into the next run.
Logging.InMemorySink is now owned per-run by BuildContext.LogSink; the static InMemorySink.Instance
becomes a facade over BuildContext.Current, with a lazy ambient fallback for logging outside a run.
ConfigureInMemory wires the sink at the top of the run and WriteErrorsAndWarnings reads it at the
end — both inside the scope — so a build's warnings and errors are discarded with its context
instead of resurfacing in the next in-process invocation.

The FT-1 InMemorySink.Clear() on dispose is dropped: there is no shared sink left to clear. Clear()
itself stays for explicit resets. The sink constructor becomes internal so a context (and a spec)
can create its own. Non-breaking — the public Logging surface is unchanged.
BuildContextSpecs mirrors the FT-4 parameter-service cases for the sink: the facade resolves the
running context's sink, falls back to a stable ambient sink outside a run, and each run gets its own.

Two existing cases assumed a process-wide sink and had to move with the mechanism:
- Dispose_runs_the_per_run_teardown used the shared sink as its witness and fails once dispose stops
  clearing it — it becomes Log_events_do_not_carry_into_the_next_run, asserting the guarantee
  directly (the next run's sink never saw the previous run's events);
- Disposing_a_superseded_context_leaves_the_shared_state_intact drops its sink witness and keeps the
  resolver-config one, since the sink is no longer shared state.

InMemorySinkSpecs now exercises its own instance rather than the singleton, so it no longer needs
the process-global collection or a normalising reset between cases.
…s over

Host.WriteErrorsAndWarnings replaces Log.Logger with a console-only logger to re-render the run's
warnings. The logger it replaced owns that run's file sinks, and assigning Log.Logger does not
dispose it — so it leaked and kept `build.log` open for the life of the process.

The next in-process run then threw in Logging.DeleteOldLogFiles trying to truncate that file, and
did so silently: the throw lands before Configure has reassigned Log.Logger, so Execute's own
Host.Error call went to an already-disposed logger. The run just returned the error exit code with
no output. Any second run whose predecessor logged a warning hit this.

Dispose the replaced logger. Nothing is lost — from that point the run logs through the console-only
logger regardless, so the file sinks were already out of the picture.
Runs a fixture build end-to-end, twice in one process, and asserts what the de-static work promised:
both runs succeed, the second gets its own context/parameter service/log sink, it opens on an empty
sink, and the static facades inside a run point at that run.

This is what the original FT-9 commit set out to do. Its unit-level assertions have since landed
with the steps they belong to (FT-2 in Fallout-build#451, FT-4 in Fallout-build#452, FT-6 in Fallout-build#453), so what is left here is
the end-to-end case they could not cover — and it immediately found the leaked file-sink logger
fixed in the previous commit. Verified as a real guard: reverting that fix fails 4 of these 5 specs.

Two process-wide inputs are pinned to make a build runnable under a test host — the argument parser
(swapped and restored) and the statically-resolved root directory (asserted to carry the .fallout
marker, since UpdateNotification prompts for input without it). Both are de-statification candidates
in their own right.
@ChrisonSimtian
ChrisonSimtian force-pushed the engine/ft9-reentrancy-harness branch from 030c3fe to 7ac7242 Compare July 26, 2026 10:52
@ChrisonSimtian ChrisonSimtian added target/vCurrent Targets the current version bug Something isn't working and removed target/vNext Targets the next calendar-version labels Jul 26, 2026
@ChrisonSimtian ChrisonSimtian changed the title BuildContext isolation harness (FT-9) Reentrant Execute harness + the file-sink leak it found (FT-9) Jul 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working target/vCurrent Targets the current version

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant