Skip to content

Commit c3123c8

Browse files
committed
fix: diagnose compiler status publication and retry release lookup
1 parent ab4f4cb commit c3123c8

7 files changed

Lines changed: 70 additions & 18 deletions

File tree

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,14 @@ existing compiler log at each of those exits; a waiter replays that log only
287287
when the controller is gone and status remains absent. This changes the
288288
diagnostic boundary, not the success criteria. The next Windows cross job must
289289
identify the cause before any protocol or OS change is attributed to it.
290+
The first diagnostic run narrowed this to final-status publication after unit
291+
retirement. The publisher now reports whether its temporary open, write, close
292+
or atomic rename failed, with the OS wrapper's OpenKal condition code for the
293+
rename and without embedding a path or user name. This wrapper does not expose
294+
the original Windows NTSTATUS. A controlled close failure and a
295+
directory-target rename refusal exercise the stage classification. The next
296+
cross-host run must determine the actual condition before any retry policy is
297+
chosen.
290298

291299
## Separate Windows build-program defect
292300

‎.github/tools/upgrade_lane.sh‎

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -59,17 +59,32 @@ IS_LINUX=0; [[ $PLAT == linux-* ]] && IS_LINUX=1
5959

6060
# ── the previous release ───────────────────────────────────────────────
6161
CURRENT=$(sed -n 's/^version *= *"\([^"]*\)".*/\1/p' mcpp.toml | head -1)
62-
OLD_VER=$(curl -fsSL ${GH_TOKEN:+-H "Authorization: Bearer $GH_TOKEN"} \
63-
"https://api.github.com/repos/mcpp-community/mcpp/releases?per_page=20" \
64-
| "$PY" -c '
62+
release_headers=()
63+
if [ -n "${GH_TOKEN:-}" ]; then release_headers=(-H "Authorization: Bearer $GH_TOKEN"); fi
64+
OLD_VER=''
65+
for attempt in 1 2 3; do
66+
# A hosted Windows run once received an empty release list before it could
67+
# exercise any upgrade. Retry this read-only input, never a product action.
68+
if curl -fsSL --connect-timeout 10 --max-time 30 "${release_headers[@]}" \
69+
"https://api.github.com/repos/mcpp-community/mcpp/releases?per_page=20" \
70+
-o "$W/releases.json"; then
71+
OLD_VER=$("$PY" -c '
6572
import json, sys
6673
current = sys.argv[1]
67-
for r in json.load(sys.stdin):
68-
v = r["tag_name"].lstrip("v")
69-
if not r["draft"] and not r["prerelease"] and v != current:
70-
print(v); break
71-
' "$CURRENT")
72-
case "$OLD_VER" in ''|*[!0-9.]*) echo "::error::no previous release resolved (got '$OLD_VER')"; exit 1 ;; esac
74+
with open(sys.argv[2], encoding="utf-8") as source:
75+
releases = json.load(source)
76+
if isinstance(releases, list):
77+
for release in releases:
78+
version = release["tag_name"].lstrip("v")
79+
if not release["draft"] and not release["prerelease"] and version != current:
80+
print(version)
81+
break
82+
' "$CURRENT" "$W/releases.json") || OLD_VER=''
83+
case "$OLD_VER" in ''|*[!0-9.]*) ;; *) break ;; esac
84+
fi
85+
[ "$attempt" = 3 ] || sleep 2
86+
done
87+
case "$OLD_VER" in ''|*[!0-9.]*) echo "::error::no previous release resolved after three metadata reads (got '$OLD_VER')"; exit 1 ;; esac
7388
echo "previous release: $OLD_VER; this commit: $CURRENT ($PLAT, variant $VARIANT)"
7489

7590
name="mcpp-$OLD_VER-$PLAT.$EXT"

‎CHANGELOG.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
and BMI/object identity checks require a regular output file on every host.
1919
A witness failure identifies its failed observation boundary, and a detached
2020
supervisor's failure is available to the waiter even though its stdio is closed.
21+
Status publication now distinguishes temporary open, write, close and rename
22+
failures, including the OpenKal condition of a failed rename.
2123
- Launch descriptions include the selected target, profile, resolved platform
2224
and available named runners, including the reachable leg of a target set.
2325
Editor debug launches can retain the configuration that actually built the program.

‎src/build/schedule/compiler_transaction.cpp‎

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,35 @@ import std;
33

44
namespace mcpp::build::schedule::transaction {
55

6-
bool replace_record(const fs::path& path,std::string_view bytes) {
6+
bool replace_record(const fs::path& path,std::string_view bytes,std::string* diagnostic) {
77
// No process may observe a partial record. This protects process loss,
88
// not power loss; do not change the OS library's durable write_atomic.
99
static const auto who=os::process::self();
1010
static std::atomic<unsigned long long> sequence{0};
11-
if(!who||!who->id||!who->started_ns)return false;
11+
auto fail=[&](std::string_view stage) {
12+
if(diagnostic)*diagnostic=stage;
13+
return false;
14+
};
15+
if(!who||!who->id||!who->started_ns)return fail("identify writer");
1216
const auto temporary=path.parent_path()/named(std::format(".mcpp-record-{}-{}-{}",
1317
who->id,who->started_ns,sequence++));
1418
std::ofstream output(temporary,std::ios::binary|std::ios::out|std::ios::noreplace);
15-
if(!output)return false; // Never remove a name this invocation did not create.
19+
if(!output)return fail("open temporary"); // Never remove a name this invocation did not create.
1620
output.write(bytes.data(),static_cast<std::streamsize>(bytes.size()));
1721
output.flush();
1822
const bool written=bool(output);
1923
output.close();
2024
const bool closed=bool(output);
21-
if(written&&closed&&os::fs::rename(temporary,path,true))return true;
25+
if(written&&closed) {
26+
auto renamed=os::fs::rename(temporary,path,true);
27+
if(renamed)return true;
28+
if(diagnostic)*diagnostic=std::format("rename temporary (condition {}, native {})",
29+
static_cast<int>(renamed.error().code),renamed.error().native);
30+
} else if(!written) {
31+
fail("write/flush temporary");
32+
} else {
33+
fail("close temporary");
34+
}
2235
std::error_code ec;fs::remove(temporary,ec);return false;
2336
}
2437
bool publish(const fs::path& path,std::string_view tag,const Fields& fields) {

‎src/build/schedule/compiler_transaction.cppm‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ std::string pack(std::string_view tag, const Fields& fields) {
3737
return body + "\n" + mcpp::toolchain::hash_string(body) + "\n";
3838
}
3939
// Atomic visibility across process loss; storage durability is not promised.
40-
bool replace_record(const fs::path& path,std::string_view bytes);
40+
bool replace_record(const fs::path& path,std::string_view bytes,std::string* diagnostic=nullptr);
4141
bool publish(const fs::path& path,std::string_view tag,const Fields& fields);
4242
Record read(const fs::path& path, std::string_view tag) {
4343
std::ifstream input(path,std::ios::binary);

‎src/build/schedule/detach_codegen.cppm‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -312,8 +312,8 @@ std::optional<int> read_rc(const std::filesystem::path& slot) {
312312

313313
// Temp file + rename, so a reader never observes half a number. The same
314314
// guarantee, for the same reason, that makes watching the BMI path sound.
315-
bool write_rc(const std::filesystem::path& slot, int rc) {
316-
return transactions::replace_record(suffixed(slot,".rc"),std::to_string(rc)+"\n");
315+
bool write_rc(const std::filesystem::path& slot, int rc,std::string* diagnostic=nullptr) {
316+
return transactions::replace_record(suffixed(slot,".rc"),std::to_string(rc)+"\n",diagnostic);
317317
}
318318

319319
// Stable kernel lock slots, shared by ordinary and detached compilers. The
@@ -888,7 +888,11 @@ int supervise(const std::filesystem::path& slot,
888888
record_supervisor_error(slot,retired.error());return 1;
889889
}
890890
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;}
891+
std::string status_failure;
892+
if(!write_rc(slot,rc,&status_failure)) {
893+
record_supervisor_error(slot,std::format("cannot publish compiler final status: {}",status_failure));
894+
return 1;
895+
}
892896
if(!transactions::release_reservation(permit->reservation,attempt.directory)) {
893897
record_supervisor_error(slot,"cannot release compiler reservation");return 1;
894898
}

‎tests/unit/test_compiler_transaction.cpp‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -223,10 +223,20 @@ TEST(CompilerTransaction, ChangedBmiParentCannotRedirectCertifiedRecovery) {
223223

224224
TEST(CompilerTransaction, RecordCloseFailureDriver) {
225225
if(!mcpp::invocation::get("MCPP_TEST_RECORD_CLOSE_DRIVER"))GTEST_SKIP()<<"subprocess driver";
226-
EXPECT_FALSE(tx::replace_record("record","replacement"));
226+
std::string diagnostic;
227+
EXPECT_FALSE(tx::replace_record("record","replacement",&diagnostic));
228+
EXPECT_EQ(diagnostic,"close temporary");
227229
EXPECT_EQ(Transaction::contents("record"),"previous");
228230
EXPECT_TRUE(fs::exists("record-close-refused"));
229231
}
232+
TEST(CompilerTransaction, FailedRenameReportsTheStageAndPreservesItsTarget) {
233+
Transaction t;
234+
fs::create_directory(t.root/"record");
235+
std::string diagnostic;
236+
EXPECT_FALSE(tx::replace_record(t.root/"record","replacement",&diagnostic));
237+
EXPECT_TRUE(diagnostic.starts_with("rename temporary (condition "))<<diagnostic;
238+
EXPECT_TRUE(fs::is_directory(t.root/"record"));
239+
}
230240
TEST(CompilerTransaction, RecordOpenAppearanceDriver) {
231241
if(!mcpp::invocation::get("MCPP_TEST_RECORD_OPEN_DRIVER"))GTEST_SKIP()<<"subprocess driver";
232242
auto record=tx::read("record","schema");

0 commit comments

Comments
 (0)