Skip to content

Commit ab4f4cb

Browse files
committed
fix: retry atomic journal opens and preserve supervisor diagnostics
1 parent fd4e2ce commit ab4f4cb

6 files changed

Lines changed: 117 additions & 9 deletions

File tree

‎.agents/docs/2026-11-06-compiler-attempt-recovery.md‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,20 @@ in-range ticks. A direct exact-toolchain control compiled the failing expression
245245
and the corrected one, including a real file identity. Native Clang, ARM and
246246
bootstrap jobs still require the next full CI run.
247247

248+
The next cold GCC run failed at the owner witness without a branch-specific
249+
diagnostic. Code review found a separate record-publication observation race:
250+
a reader's first open can report absence while an atomic publisher installs
251+
the record before the following existence probe. Treating this pair as a
252+
corrupt record can falsely fail the owner witness. A libc open-boundary control
253+
reproduced the old failure and the corrected retry with identical published
254+
bytes. The reader retries one open; a persistent refusal still fails. Witness
255+
diagnostics now distinguish an invalid record, an unreadable attempt and a
256+
controller without retirement so the next CI can test this attribution. The
257+
native Windows suite also showed that
258+
`file_size` can report a directory's size; output identity now requires a
259+
regular leaf before reading size and timestamp. The Windows negative test and
260+
the cold GCC build remain final-CI gates.
261+
248262
An actual split-compiler control held a previously absent `.rc` observation
249263
until the controller had published status and exited. The earlier engine failed
250264
with `compiler controller ended without a status`; the corrected engine rereads
@@ -265,6 +279,15 @@ OS 0.3.0. The older consumer platform selected by the application's own
265279
target remains independent of the launcher's build platform. The actual
266280
cross-platform packing control and complete CI remain release gates.
267281

282+
The cross-host Windows→Linux job still reached a missing final status after
283+
the controller exited. Because detached supervisor stdio was discarded, the
284+
first CI did not retain whether unit retirement, certificate publication or
285+
status publication failed. The supervisor now appends a bounded reason to its
286+
existing compiler log at each of those exits; a waiter replays that log only
287+
when the controller is gone and status remains absent. This changes the
288+
diagnostic boundary, not the success criteria. The next Windows cross job must
289+
identify the cause before any protocol or OS change is attributed to it.
290+
268291
## Separate Windows build-program defect
269292

270293
The released 2026.11.5.1 engine can try to install `msvc@system` as an indexed

‎CHANGELOG.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@
1414
platform, so their generated dependency resolves alongside openkal-platform 0.24.
1515
- Compiler attempt journals preserve file timestamps on Clang/libc++ when the
1616
file clock uses a wider integer representation.
17+
- Compiler journals retry an open that raced with atomic record publication,
18+
and BMI/object identity checks require a regular output file on every host.
19+
A witness failure identifies its failed observation boundary, and a detached
20+
supervisor's failure is available to the waiter even though its stdio is closed.
1721
- Launch descriptions include the selected target, profile, resolved platform
1822
and available named runners, including the reachable leg of a target set.
1923
Editor debug launches can retain the configuration that actually built the program.

‎src/build/schedule/compiler_transaction.cppm‎

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,12 @@ Record read(const fs::path& path, std::string_view tag) {
4444
if (!input) {
4545
std::error_code ec;
4646
if (!fs::exists(path,ec) && !ec) return std::optional<Fields>{};
47-
return std::unexpected("cannot read compiler transaction record");
47+
if(ec)return std::unexpected("cannot read compiler transaction record");
48+
// An atomic publisher may install the record between the failed open
49+
// and the existence probe. Retry once; a persistent refusal still
50+
// fails rather than being mistaken for an absent record.
51+
input.open(path,std::ios::binary);
52+
if(!input)return std::unexpected("cannot read compiler transaction record");
4853
}
4954
std::string bytes(65537,'\0');
5055
input.read(bytes.data(),bytes.size());bytes.resize(static_cast<std::size_t>(input.gcount()));
@@ -99,7 +104,10 @@ std::optional<long long> journal_time(fs::file_time_type time) {
99104
}
100105
std::optional<Fields> identity(const fs::path& path) {
101106
if(path.empty())return Fields{"", ""};
102-
std::error_code ec;const auto size=fs::file_size(path,ec);if(ec)return {};
107+
std::error_code ec;
108+
const auto status=fs::symlink_status(path,ec);
109+
if(ec||status.type()!=fs::file_type::regular)return {};
110+
const auto size=fs::file_size(path,ec);if(ec)return {};
103111
const auto time=fs::last_write_time(path,ec);if(ec)return {};
104112
const auto ticks=journal_time(time);if(!ticks)return {};
105113
return Fields{std::to_string(size),std::to_string(*ticks)};
@@ -377,21 +385,27 @@ private:
377385
std::atomic<bool> stopping_{false},failed_{false};
378386
std::mutex admission_;
379387
ObserverMap held_;
388+
void fail(std::string_view reason) {
389+
if(!failed_.exchange(true))
390+
std::println(std::cerr,"mcpp: compiler witness: {}",reason);
391+
}
380392
void audit() {
381393
for(auto it=held_.begin();it!=held_.end();) {
382394
const auto directory=named(it->first);
383395
auto retired=read(directory/"retired","mcpp-retired-v1");
384-
if(!retired||(*retired&&(**retired).size()!=3)) {failed_.store(true);++it;continue;}
396+
if(!retired) {fail(retired.error());++it;continue;}
397+
if(*retired&&(**retired).size()!=3) {fail("invalid retirement certificate");++it;continue;}
385398
if(*retired) {it=held_.erase(it);continue;}
386399
auto attempt=Attempt::load(directory);
387-
if(!attempt) {failed_.store(true);++it;continue;}
400+
if(!attempt) {fail(attempt.error());++it;continue;}
388401
if(!controller_alive(*attempt)) {
389402
// Retirement can be published between the first read and the
390403
// controller's exit. Observe it again before reporting loss.
391404
retired=read(directory/"retired","mcpp-retired-v1");
392-
if(!retired||(*retired&&(**retired).size()!=3))failed_.store(true);
405+
if(!retired)fail(retired.error());
406+
else if(*retired&&(**retired).size()!=3)fail("invalid retirement certificate");
393407
else if(*retired) {it=held_.erase(it);continue;}
394-
else failed_.store(true);
408+
else fail("controller ended without retirement certificate");
395409
}
396410
++it;
397411
}

‎src/build/schedule/detach_codegen.cppm‎

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -533,6 +533,18 @@ int run_to_completion(std::string_view command,
533533
return owner.active() ? r.exit_code : 130;
534534
}
535535

536+
void record_supervisor_error(const std::filesystem::path& slot,std::string_view reason) {
537+
// Detached supervisors have null stdio. Keep the failure beside the
538+
// compiler's ordinary log so either waiter can report why status is absent.
539+
std::ofstream log(suffixed(slot,".log"),std::ios::app);
540+
if(log)log << "mcpp: " << reason << '\n';
541+
}
542+
543+
void replay_supervisor_log(const std::filesystem::path& slot) {
544+
std::ifstream log(suffixed(slot,".log"));
545+
if(log)std::cerr << log.rdbuf();
546+
}
547+
536548
std::expected<void,std::string> retire_owned(mcpplibs::os::process::owned_unit& unit,const Owner& owner) {
537549
const auto limit=std::chrono::steady_clock::now()+std::chrono::seconds(5);
538550
for(;;) {
@@ -812,6 +824,7 @@ int compile_release_at_bmi(const CompileRequest& req) {
812824
// The final status can be published between the first read and
813825
// this liveness observation. The next iteration validates it.
814826
if(read_rc(actual.slot))continue;
827+
replay_supervisor_log(actual.slot);
815828
std::println(std::cerr,"mcpp: compiler controller ended without a final status; retaining its outputs until unit recovery");
816829
return 1;
817830
}
@@ -872,10 +885,13 @@ int supervise(const std::filesystem::path& slot,
872885
}
873886
const int rc=run_to_completion(command,suffixed(slot,".log"),actor->owner,unit.get());
874887
if(auto retired=retire_owned(*unit,actor->owner);!retired) {
875-
std::println(std::cerr,"mcpp: {}",retired.error());return 1;
888+
record_supervisor_error(slot,retired.error());return 1;
889+
}
890+
if(!attempt.retired(rc)) {record_supervisor_error(slot,"cannot publish compiler retirement certificate");return 1;}
891+
if(!write_rc(slot,rc)) {record_supervisor_error(slot,"cannot publish compiler final status");return 1;}
892+
if(!transactions::release_reservation(permit->reservation,attempt.directory)) {
893+
record_supervisor_error(slot,"cannot release compiler reservation");return 1;
876894
}
877-
if(!attempt.retired(rc)||!write_rc(slot,rc))return 1;
878-
if(!transactions::release_reservation(permit->reservation,attempt.directory))return 1;
879895
return 0;
880896
}
881897
auto settled = std::make_shared<std::atomic<bool>>(false);
@@ -956,6 +972,7 @@ int await_unit(const std::filesystem::path& slot, const std::filesystem::path& o
956972
// A controller may publish its status and exit after the read
957973
// in the loop condition. Let the normal result path inspect it.
958974
if((rc=read_rc(slot)))break;
975+
replay_supervisor_log(slot);
959976
std::println(std::cerr,"mcpp: compiler controller ended without a status; unit recovery will retain or invalidate its outputs");
960977
return 1;
961978
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
#define _GNU_SOURCE
2+
#include <dlfcn.h>
3+
#include <errno.h>
4+
#include <stdio.h>
5+
#include <string.h>
6+
7+
/* Simulate an atomic publish between the reader's first open and its
8+
* existence probe. The real completed record remains present throughout. */
9+
static FILE *(*native_open)(const char *, const char *);
10+
static int injected;
11+
12+
FILE *fopen64(const char *path, const char *mode) {
13+
if (!native_open) native_open = dlsym(RTLD_NEXT, "fopen64");
14+
if (!injected && strcmp(path, "record") == 0 && mode[0] == 'r') {
15+
injected = 1;
16+
FILE *marker = native_open("record-open-interposed", "wb");
17+
if (marker) fclose(marker);
18+
errno = ENOENT;
19+
return NULL;
20+
}
21+
return native_open(path, mode);
22+
}
23+
24+
FILE *fopen(const char *path, const char *mode) { return fopen64(path, mode); }

‎tests/unit/test_compiler_transaction.cpp‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,32 @@ TEST(CompilerTransaction, RecordCloseFailureDriver) {
227227
EXPECT_EQ(Transaction::contents("record"),"previous");
228228
EXPECT_TRUE(fs::exists("record-close-refused"));
229229
}
230+
TEST(CompilerTransaction, RecordOpenAppearanceDriver) {
231+
if(!mcpp::invocation::get("MCPP_TEST_RECORD_OPEN_DRIVER"))GTEST_SKIP()<<"subprocess driver";
232+
auto record=tx::read("record","schema");
233+
EXPECT_TRUE(fs::exists("record-open-interposed"));
234+
ASSERT_TRUE(record&&*record);
235+
EXPECT_EQ((**record)[0],"published");
236+
}
237+
TEST(CompilerTransaction, RecordAppearanceBetweenOpenAndExistsIsNotCorruption) {
238+
if constexpr (!mcpplibs::os::is_linux)GTEST_SKIP()<<"Linux libc open interposition";
239+
const auto runtime=mcpp::runtime::elf::inspect_elf_runtime(mcpp::invocation::self_exe_path());
240+
if(!runtime || runtime->interp.find("ld-linux")==std::string::npos)
241+
GTEST_SKIP()<<"open interposition requires the dynamic glibc loader";
242+
Transaction t;
243+
const auto library=t.root/"record-open-appearance.so";
244+
const auto source=fs::current_path()/"tests/e2e/helpers/record_open_appearance.c";
245+
auto compiled=mcpp::run::execute({"/usr/bin/cc","-shared","-fPIC","-Wall","-Wextra","-Werror",
246+
source.string(),"-o",library.string(),"-ldl"},{.total=std::chrono::seconds(10)});
247+
ASSERT_EQ(compiled.exit_code,0)<<compiled.output;
248+
ASSERT_TRUE(tx::publish(t.root/"record","schema",{"published"}));
249+
const auto result=mcpp::run::execute({mcpp::invocation::self_exe_path().string(),
250+
"--gtest_filter=CompilerTransaction.RecordOpenAppearanceDriver"},
251+
{.env={{"MCPP_TEST_RECORD_OPEN_DRIVER","1"},{"LD_PRELOAD",library.string()}},
252+
.cwd=t.root.string(),.total=std::chrono::seconds(10)});
253+
EXPECT_EQ(result.exit_code,0)<<result.output;
254+
EXPECT_TRUE(fs::exists(t.root/"record-open-interposed"));
255+
}
230256
TEST(CompilerTransaction, FailedCloseCannotPublishOrAcknowledgeARecord) {
231257
if constexpr (!mcpplibs::os::is_linux)GTEST_SKIP()<<"Linux libc close failure control";
232258
const auto runtime=mcpp::runtime::elf::inspect_elf_runtime(mcpp::invocation::self_exe_path());

0 commit comments

Comments
 (0)