Skip to content

Commit d76ffb6

Browse files
committed
fix: own detached compilers through cancellation and rebuilds
1 parent ffcf49d commit d76ffb6

16 files changed

Lines changed: 910 additions & 84 deletions

‎.agents/docs/2026-11-06-bmi-publication-and-compiler-cap-closeout.md‎

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,3 +131,71 @@ follow ordinary installation of the new release, in the same plugin PR.
131131
Public records contain synthetic assertions and aggregate evidence. Raw
132132
provider streams, user logs, usernames and private paths remain outside the
133133
repository and pull-request description.
134+
135+
## Correction, 2026-11-06: ownership across cancellation and generations
136+
137+
The shared-cap change exposed an existing lifetime gap. A real Ninja control
138+
cancelled the top-level build after BMI publication. Its detached compiler
139+
remained alive 200 milliseconds later. Deleting the previous directory tokens
140+
at the next build could therefore admit new compilers over the requested cap.
141+
The correction below supersedes directory-token ownership in the earlier
142+
shared-permit description.
143+
144+
Each detached-schedule backend drive owns a runtime generation. Normal graph
145+
execution and cached replay, including quiet replay, open the same scope. Its
146+
owner has both the operating system's process identity with birth time and an
147+
exclusive kernel lock. A PID alone, a surviving record, or an absent birth time
148+
cannot establish ownership. Closing the lock revokes the drive even while its
149+
process is alive. The generation is passed only in the child environment after
150+
cache decoding; it never enters an action, build fingerprint or saved graph.
151+
152+
Every phase-one helper, supervisor, join helper and ordinary compiler executor
153+
registers a kernel-locked actor before modifying build state and rechecks the
154+
owner after registration. The actor covers status publication, BMI equivalence
155+
settlement and backup restoration, as well as compiler execution. Interrupt
156+
callbacks retain that actor until their final write. A delayed helper from a
157+
closed generation returns cancellation before changing any build state.
158+
159+
A stable gate serializes generations for one canonical output directory. The
160+
next generation waits for all previous actor locks, including actors that have
161+
already released a compiler permit. It retains busy state and refuses admission
162+
if the actors do not drain within the cleanup bound. Runtime locks reside under
163+
the work directory's `.mcpp/compiler-runs`, outside the target output tree and
164+
independently of `MCPP_HOME`. Cleaning output files cannot unlink a live permit.
165+
166+
Actual compiler permits are stable kernel-locked files. A supervisor acquires
167+
its permit itself and holds it until the child has stopped; phase one does not
168+
transfer a lock or consume a second permit. Ordinary compilation uses the same
169+
slots. Files left after process death are reused by locking, never reclaimed by
170+
unlinking. A typed busy result is retried; permission and other failures stop
171+
the invocation. This also replaces the directory-creation contention path that
172+
reported access denied in the Windows-host GCC cross-build.
173+
174+
Child cancellation uses the existing launcher stop predicate. A further causal
175+
control found that inherited or discarded streams skipped that predicate when
176+
no absolute deadline was set. The launcher now polls child completion in that
177+
case, retaining its argv, streams, exit status and existing deadline. No timeout
178+
is increased. Failed or cancelled compiler invocations withdraw partial outputs;
179+
subsequent ordinary builds recover missing objects and BMIs.
180+
181+
The permanent real-Ninja control checks:
182+
183+
- Module and ordinary compilation under top-level SIGINT and SIGKILL, followed
184+
immediately by another generation; the old compiler has stopped before the
185+
new drive is admitted.
186+
- No-op builds across generations, retaining compiler count and object mtime;
187+
normal and quiet cached replay retain their graph, objects and BMIs.
188+
- Changed homes and a symlinked target directory retain the work-directory
189+
runtime barrier, independently of the disposable output location.
190+
- Owner-lock closure while the owner process remains alive.
191+
- Missing and incorrect process birth identities, rejected before state writes.
192+
- Delayed phase-one BMI settlement and backup restoration after compiler exit;
193+
both still block the next generation until their final state write.
194+
- Unregistered old phase-one and supervisor processes delayed until after a
195+
successful new generation; neither can alter its BMI, object or status.
196+
197+
The controls execute the actual helper and launcher implementations with a
198+
small synthetic compiler. The separate GCC mixed workload retains the real
199+
compiler cap, late failure, recovery, argv and no-op assertions. Selected-object
200+
controls establish causality; final qualification remains complete current-source
201+
CI and independent ecosystem review.

‎CHANGELOG.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@
2323
BMI fails with a diagnostic and restores the previous BMI.
2424
- Ordinary compiler invocations share the detached GCC compilers' concurrency
2525
permits, so the additional Ninja scheduling slots do not exceed `--jobs`.
26+
- Cancelled detached builds stop their compilers and finish state cleanup before
27+
another generation starts. Stable kernel permits retain live ownership across
28+
immediate rebuilds; runtime generations leave no-op build identities unchanged.
29+
- Child stop predicates apply with inherited and discarded streams, including
30+
executions without an absolute deadline.
2631
- Queued downloads begin their progress budget when a worker starts them,
2732
rather than inheriting time spent waiting for a slot.
2833
- Provider progress timeouts identify the stalled download and source attempt,

‎docs/04-mcpp-toml.md‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1051,6 +1051,9 @@ with two behaviours has to be.
10511051
With GCC and `bmi_schedule = "on"`, ordinary compilation and detached code
10521052
generation share the same total `jobs` limit (2026.11.5.2+). Additional
10531053
scheduling and join tasks do not increase the number of compiler processes.
1054+
Cancelling the build ends its ordinary and detached compilers. An immediately
1055+
following build waits for their cleanup before starting another compiler;
1056+
partial outputs are rebuilt. An unchanged build still performs no compilation.
10541057

10551058
| value | |
10561059
|---|---|

‎docs/zh/04-mcpp-toml.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -974,6 +974,8 @@ bmi_schedule = "off" # auto (default, = off) | on | off
974974

975975
使用 GCC 且 `bmi_schedule = "on"` 时,普通编译与分离的代码生成共用同一个 `jobs` 总上限
976976
(2026.11.5.2+)。额外的调度和等待任务不增加编译器进程数量。
977+
取消构建会结束本轮普通编译器和分离的编译器。紧接着的构建等待它们完成清理后
978+
才启动新的编译器;未完成的输出会重新构建。输入不变的构建仍不进行编译。
977979

978980
| 取值 | |
979981
|---|---|

‎src/build/execute.cppm‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ import mcpp.runtime.launch_env;
6161
import mcpp.run;
6262
import mcpp.home.files;
6363
import mcpp.build.schedule.policy; // resolve_jobs — one answer to "how many at once"
64+
import mcpp.build.schedule.detach_codegen;
6465
import mcpp.fetcher.progress;
6566
import mcpp.project;
6667
import mcpp.ui;
@@ -2166,6 +2167,14 @@ std::optional<int> run_ninja_fast(const std::string& ninjaProgram,
21662167
childEnv.emplace_back(runtimeEnvKey, runtimeEnvValue);
21672168
}
21682169

2170+
std::optional<mcpp::build::schedule::detach::RunScope> compilerRun;
2171+
if (mcpp::build::read_schedule(ninjaPath) == "detach-codegen") {
2172+
auto opened = mcpp::build::schedule::detach::RunScope::begin(outputDir);
2173+
if (!opened) { mcpp::ui::error(opened.error()); return 1; }
2174+
compilerRun.emplace(std::move(*opened));
2175+
compilerRun->environment(childEnv);
2176+
}
2177+
21692178
auto t0 = std::chrono::steady_clock::now();
21702179
// capture_exec merges stderr into the captured output (replacing `2>&1`),
21712180
// so is_stale_ninja_failure / filter_ninja_output still see ninja errors.
@@ -2213,6 +2222,12 @@ std::optional<int> run_ninja_fast(const std::string& ninjaProgram,
22132222
out = std::move(r.output);
22142223
status = r.exit_code;
22152224
}
2225+
if (compilerRun) {
2226+
if (auto settled = compilerRun->finish(); !settled) {
2227+
mcpp::ui::error(settled.error());
2228+
return 1;
2229+
}
2230+
}
22162231
if (status != 0) {
22172232
if (!reported && is_stale_ninja_failure(out))
22182233
return std::nullopt;

‎src/build/ninja_backend.cppm‎

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ import mcpp.ui;
5858
import mcpp.log;
5959
import mcpp.build.action_progress;
6060
import mcpp.build.progress;
61+
import mcpp.build.schedule.detach_codegen;
6162

6263
export namespace mcpp::build {
6364

@@ -1470,7 +1471,7 @@ std::string emit_ninja_string(const BuildPlan& plan, std::string* placements,
14701471
// compilers must share the supervisors' semaphore rather than use that
14711472
// larger slot count as their own concurrency bound.
14721473
const std::string compilerPrefix = splitBmi
1473-
? std::format("$mcpp __compiler-cap --sem .mcpp-sched --cap {} -- ", plan.scheduleCompilerCap)
1474+
? std::format("$mcpp __compiler-cap --sem .mcpp-sched --cap {} --output $out -- ", plan.scheduleCompilerCap)
14741475
: std::string{};
14751476
// The other split shape. Clang publishes no BMI early — it writes the BMI
14761477
// at the end of the compile — so the GCC trick of releasing the file
@@ -2108,7 +2109,7 @@ std::string emit_ninja_string(const BuildPlan& plan, std::string* placements,
21082109
// wrong in this file before.
21092110
append("rule cxx_module_bmi\n");
21102111
append(" command = $mcpp bmi-compile --bmi $bmi_out --slot $slot"
2111-
" --self $mcpp --sem $sched_sem --cap $sched_cap"
2112+
" --self $mcpp --sem $sched_sem --cap $sched_cap --object $obj_out"
21122113
" --command-file $out.cmd --dep-from $scan_dep --dep-to $out.d\n");
21132114
append(std::format(
21142115
" rspfile = $out.cmd\n"
@@ -4538,6 +4539,12 @@ std::expected<BuildResult, BuildError> NinjaBackend::build(const BuildPlan& plan
45384539
return std::unexpected(BuildError{std::format("cannot create output dir '{}': {}",
45394540
plan.outputDir.string(), ec.message()),
45404541
plan.outputDir});
4542+
std::optional<mcpp::build::schedule::detach::RunScope> compilerRun;
4543+
if (!opts.dryRun && plan.scheduleTag == "detach-codegen") {
4544+
auto opened = mcpp::build::schedule::detach::RunScope::begin(plan.outputDir);
4545+
if (!opened) return std::unexpected(BuildError{opened.error(), plan.outputDir});
4546+
compilerRun.emplace(std::move(*opened));
4547+
}
45414548
if (!opts.dryRun && plan.scheduleTag == "detach-codegen")
45424549
reclaim_stale_schedule_tokens(plan.outputDir);
45434550

@@ -4881,6 +4888,7 @@ std::expected<BuildResult, BuildError> NinjaBackend::build(const BuildPlan& plan
48814888
nenv.emplace_back(runtimeEnv->first, runtimeEnv->second);
48824889
for (auto& ev : plan.toolchain.envOverrides)
48834890
nenv.emplace_back(ev.key, ev.value);
4891+
if (compilerRun) compilerRun->environment(nenv);
48844892

48854893
// THE STAGED CACHE IS PLACED BY A PASS OF ITS OWN, before the graph that
48864894
// reads it. ninja 1.12.1 loads a dyndep file whose producer is up to date
@@ -5008,6 +5016,10 @@ std::expected<BuildResult, BuildError> NinjaBackend::build(const BuildPlan& plan
50085016
ninjaExit = cap.exit_code;
50095017
}
50105018
stage("ninja");
5019+
if (compilerRun) {
5020+
if (auto settled = compilerRun->finish(); !settled)
5021+
return std::unexpected(BuildError{settled.error(), plan.outputDir});
5022+
}
50115023
bool ok = (ninjaExit == 0) && !buildTimedOut;
50125024
// Every package gets its final line before what the build has to say
50135025
// after ninja: validations, advice, `Finished`.

0 commit comments

Comments
 (0)