[Feat] (P13 W5 S1): the fatal vocabulary moves to MG_Pipe and the record arm gets its own fail / latch seam.

This commit is contained in:
2026-10-06 09:48:58 -04:00
parent ec92ae4d08
commit 534f14e3f2
10 changed files with 294 additions and 38 deletions
@@ -1,4 +1,4 @@
// MobileGL - MobileGL/MG_Remote/FatalFamilies.def
// MobileGL - MobileGL/MG_Pipe/FatalFamilies.def
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
+60
View File
@@ -119,6 +119,66 @@
namespace MobileGL::MG_Pipe {
// ----------------------------------------------------------------------------
// P13 W5: THE RECORD ARM'S FAIL / LATCH SEAM (PipeSessionFail.h). Constant-initialized
// pointers, so a registration made during another translation unit's static init (FatalFunnel.cpp)
// is never overwritten by this one's.
namespace {
constinit MGPipeRecordFailHook g_recordFailHook = nullptr;
constinit MGPipeRecordLatchHook g_recordLatchHook = nullptr;
constinit MGPipeRecordLatchedHook g_recordLatchedHook = nullptr;
void FormatRecordLine(char (&line)[512], const char* fmt, va_list args) {
const int written = std::vsnprintf(line, sizeof(line), fmt, args);
if (written < 0) {
std::snprintf(line, sizeof(line), "MGPipe: unformattable Fatal diagnostic (format=%s)", fmt);
}
}
[[noreturn]] void RecordDeath(MGFatalFamily family, const char* line) {
if (g_recordFailHook != nullptr) g_recordFailHook(family, line);
// NO HOOK (no MG_Remote in this library): the line, its stderr echo - the logger's
// console sink is off and a gtest death matcher reads the child's stderr - and the abort.
MGLOG_F("%s", line);
std::fputs(line, stderr);
std::fputc('\n', stderr);
std::fflush(stderr);
std::abort();
}
} // namespace
void MGPipeInstallRecordFailHooks(MGPipeRecordFailHook fail, MGPipeRecordLatchHook latch,
MGPipeRecordLatchedHook latched) {
g_recordFailHook = fail;
g_recordLatchHook = latch;
g_recordLatchedHook = latched;
}
void MGPipeRecordFail(MGFatalFamily family, const char* fmt, ...) {
char line[512];
va_list args;
va_start(args, fmt);
FormatRecordLine(line, fmt, args);
va_end(args);
RecordDeath(family, line);
}
bool MGPipeRecordLatch(MGFatalFamily family, const char* fmt, ...) {
char line[512];
va_list args;
va_start(args, fmt);
FormatRecordLine(line, fmt, args);
va_end(args);
if (g_recordLatchHook != nullptr) return g_recordLatchHook(family, line);
RecordDeath(family, line);
}
bool MGPipeRecordLatched() { return g_recordLatchedHook != nullptr && g_recordLatchedHook(); }
MGPipeRecordFailHook MGPipeRecordFailHookInstalled() { return g_recordFailHook; }
MGPipeRecordLatchHook MGPipeRecordLatchHookInstalled() { return g_recordLatchHook; }
MGPipeRecordLatchedHook MGPipeRecordLatchedHookInstalled() { return g_recordLatchedHook; }
#if MOBILEGL_BUILD_DISAGGREGATED
// ----------------------------------------------------------------------------
// THE BACKEND-SIDE SESSION-FAIL SEAM (P7 wave 0). PipeSessionFail.h holds the argument;
+55
View File
@@ -0,0 +1,55 @@
// MobileGL - MobileGL/MG_Pipe/PipeFatalFamily.h
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
// P13 W5: THE FATAL VOCABULARY WITHOUT THE WIRE. The record arm runs in a library that has no
// MG_Remote (the FCL shape), and its deaths still need their family words: a staged store that
// cannot answer, a record whose bounds are wrong. So the enum and its names are generated here
// from FatalFamilies.def, and MG_Remote/FatalFamily.h keeps only what needs the protocol - the
// projection of a family onto the seven-value wire FatalCode - and re-exports these names.
#pragma once
#include <cstddef>
#include <cstdint>
#include "FatalFamilies.def"
namespace MobileGL::MG_Pipe {
// One value per row of FatalFamilies.def, in file order. The names ARE the family words that
// appear in every `Fatal{...}` log line, which is what lets FatalFamilyName round-trip
// against the string a site passes.
enum class MGFatalFamily : ::std::uint32_t {
#define X(Family, WireCode, Why) Family,
MGL_FATAL_FAMILY_LIST(X)
#undef X
};
// The family's own word, byte-for-byte what appears inside `Fatal{<name>, ...}`. A death
// site passes both the enum and its full message string; a test asserts this name is present
// in that string, which is what keeps the two from diverging.
inline const char* FatalFamilyName(MGFatalFamily family) {
switch (family) {
#define X(Family, WireCode, Why) \
case MGFatalFamily::Family: return #Family;
MGL_FATAL_FAMILY_LIST(X)
#undef X
}
return "<unknown>";
}
// The count, for a test that walks every family.
inline constexpr ::std::size_t MGFatalFamilyCount() {
::std::size_t n = 0;
#define X(Family, WireCode, Why) ++n;
MGL_FATAL_FAMILY_LIST(X)
#undef X
return n;
}
} // namespace MobileGL::MG_Pipe
+40
View File
@@ -39,6 +39,8 @@
#pragma once
#include <MG_Pipe/PipeFatalFamily.h>
namespace MobileGL::MG_Pipe {
// THE FAMILIES THAT CROSS THIS BOUNDARY, AND ONLY THOSE. Not `MGFatalFamily` cast to an
@@ -85,6 +87,44 @@ namespace MobileGL::MG_Pipe {
#endif
void MGPipeSessionFail(MGPipeFatalFamily family, const char* fmt, ...);
// ---- P13 W5: THE RECORD ARM'S DEATHS, IN EVERY BUILD -------------------------------------
//
// The record arm's own code - the staged stores, the verb sink, the verb port - dies by
// family word (MG_Pipe::MGFatalFamily, the whole FatalFamilies.def vocabulary) and, where a
// session can be ended instead of the process, latches. With MG_Remote linked, FatalFunnel.cpp
// registers SessionFail / SessionLatch / SessionLatched here at static init, so every DISAGG
// process - unit cases with no role init included - counts, publishes and latches exactly as
// a direct call did. Without MG_Remote (the FCL shape) the defaults are the monolith answer:
// log the line, echo it to stderr (a death test reads the child's stderr), abort; latching is
// dying; nothing is ever latched.
//
// `line` is the FULLY FORMATTED message, family word and all, as for MGPipeSessionFail.
using MGPipeRecordFailHook = void (*)(MGFatalFamily family, const char* line);
using MGPipeRecordLatchHook = bool (*)(MGFatalFamily family, const char* line);
using MGPipeRecordLatchedHook = bool (*)();
// Last writer wins; a null leaves that half on its default.
void MGPipeInstallRecordFailHooks(MGPipeRecordFailHook fail, MGPipeRecordLatchHook latch,
MGPipeRecordLatchedHook latched);
// The installed hooks, or nullptr - so a test proves the registration happened (and restores
// what it replaced) without having to die to find out.
MGPipeRecordFailHook MGPipeRecordFailHookInstalled();
MGPipeRecordLatchHook MGPipeRecordLatchHookInstalled();
MGPipeRecordLatchedHook MGPipeRecordLatchedHookInstalled();
[[noreturn]]
#if defined(__GNUC__) || defined(__clang__)
__attribute__((format(printf, 2, 3)))
#endif
void MGPipeRecordFail(MGFatalFamily family, const char* fmt, ...);
// Returns false (a Bool site reads `return MGPipeRecordLatch(...);`) when the hook latched the
// calling session; with no latch to raise it dies as MGPipeRecordFail does.
bool
#if defined(__GNUC__) || defined(__clang__)
__attribute__((format(printf, 2, 3)))
#endif
MGPipeRecordLatch(MGFatalFamily family, const char* fmt, ...);
// Whether the calling session already latched (false with no hook).
bool MGPipeRecordLatched();
#if MOBILEGL_BUILD_DISAGGREGATED
// ---- THE LATCH TWIN: A LOST GPU DEVICE ENDS ITS SESSION, NOT THE PROCESS ----------------
//
+8 -32
View File
@@ -10,23 +10,21 @@
// wire FatalCode, both generated from FatalFamilies.def so a family, its name and its wire code
// cannot drift apart: adding a family is one row, and a row that forgets its code does not
// compile.
//
// P13 W5: the vocabulary itself (the enum, its names, its count) lives in MG_Pipe/PipeFatalFamily.h
// so the record arm can die by name in a library without MG_Remote; this header adds the wire
// projection and re-exports the vocabulary under its old names.
#pragma once
#include <MG_Pipe/PipeFatalFamily.h>
#include <MG_Remote/Protocol/generated/protocol_generated.h>
#include "FatalFamilies.def"
namespace MobileGL::MG_Remote {
// One value per row of FatalFamilies.def, in file order. The names ARE the family words that
// appear in every `Fatal{...}` log line, which is what lets FatalFamilyName round-trip
// against the string a site passes.
enum class MGFatalFamily : ::std::uint32_t {
#define X(Family, WireCode, Why) Family,
MGL_FATAL_FAMILY_LIST(X)
#undef X
};
using ::MobileGL::MG_Pipe::FatalFamilyName;
using ::MobileGL::MG_Pipe::MGFatalFamily;
using ::MobileGL::MG_Pipe::MGFatalFamilyCount;
// THE PROJECTION, AS A TOTAL TABLE. Not a switch with a default arm: this build has no
// -Werror, so a default would silently swallow a family added without a code. Every family
@@ -43,26 +41,4 @@ namespace MobileGL::MG_Remote {
return ::MobileGL::Wire::FatalCode::ProtocolCorruption;
}
// The family's own word, byte-for-byte what appears inside `Fatal{<name>, ...}`. A death
// site passes both the enum and its full message string; a test asserts this name is present
// in that string, which is what keeps the two from diverging.
inline const char* FatalFamilyName(MGFatalFamily family) {
switch (family) {
#define X(Family, WireCode, Why) \
case MGFatalFamily::Family: return #Family;
MGL_FATAL_FAMILY_LIST(X)
#undef X
}
return "<unknown>";
}
// The count, for a test that walks every family.
inline constexpr ::std::size_t MGFatalFamilyCount() {
::std::size_t n = 0;
#define X(Family, WireCode, Why) ++n;
MGL_FATAL_FAMILY_LIST(X)
#undef X
return n;
}
} // namespace MobileGL::MG_Remote
+19
View File
@@ -307,4 +307,23 @@ namespace MobileGL::MG_Remote {
}
} // namespace
// P13 W5: THE RECORD ARM'S DEATHS GO THROUGH THIS FUNNEL WHEREVER MG_Remote IS LINKED.
// Registered at static init rather than at role init: both roles, and unit cases that never
// run one, call into the record arm, and each of its deaths must count, publish and latch
// exactly as the direct SessionFail / SessionLatch call it replaced did. The line arrives
// formatted, so it is passed through "%s" - the same bytes reach the log and the frame.
namespace {
[[noreturn]] void RecordFailThroughFunnel(MGFatalFamily family, const char* line) {
SessionFail(family, "%s", line);
}
bool RecordLatchThroughFunnel(MGFatalFamily family, const char* line) {
return SessionLatch(family, "%s", line);
}
[[maybe_unused]] const bool g_recordFailHooksRegistered = [] {
MG_Pipe::MGPipeInstallRecordFailHooks(&RecordFailThroughFunnel, &RecordLatchThroughFunnel,
&SessionLatched);
return true;
}();
} // namespace
} // namespace MobileGL::MG_Remote
+6
View File
@@ -24,6 +24,12 @@ if (MSVC)
target_compile_options(PipeCatalogueTest PRIVATE /Zc:preprocessor)
endif()
# P13 W5: the record arm's fail / latch seam, in both library shapes (with and without MG_Remote).
add_executable(RecordFailSeamTest RecordFailSeamTest.cpp)
target_include_directories(RecordFailSeamTest PRIVATE ${MGL_ROOT}/include ${MGL_ROOT}/MobileGL)
target_link_libraries(RecordFailSeamTest PRIVATE GTest::gtest_main ${LINK_LIBRARIES})
gtest_discover_tests(RecordFailSeamTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
# P7 B4 aggregate-wait regression control shares the production selector; no device is needed.
add_executable(MagmaAggregateWaitTest MagmaAggregateWaitTest.cpp)
target_include_directories(MagmaAggregateWaitTest PRIVATE ${MGL_ROOT}/include ${MGL_ROOT}/MobileGL)
@@ -0,0 +1,83 @@
// MobileGL - MobileGL/MG_Test/Pipe/RecordFailSeamTest.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
// P13 W5: THE RECORD ARM'S FAIL / LATCH SEAM (MG_Pipe/PipeSessionFail.h), in both library shapes.
// Without MG_Remote the defaults are the monolith answer - the line on stderr, then the abort; with
// it, FatalFunnel.cpp's static registration is in place before any case runs, so the record arm's
// deaths count and publish through SessionFail exactly as a direct call did.
#include <MG_Pipe/PipeFatalFamily.h>
#include <MG_Pipe/PipeSessionFail.h>
#include <gtest/gtest.h>
#include <string>
using namespace MobileGL;
using MG_Pipe::MGFatalFamily;
TEST(RecordFailSeam, TheVocabularyIsTheDefFilesWords) {
EXPECT_STREQ(MG_Pipe::FatalFamilyName(MGFatalFamily::ProtocolCorruption), "ProtocolCorruption");
EXPECT_STREQ(MG_Pipe::FatalFamilyName(MGFatalFamily::BackendDeviceLost), "BackendDeviceLost");
EXPECT_GT(MG_Pipe::MGFatalFamilyCount(), 10u);
}
TEST(RecordFailSeam, TheFunnelIsRegisteredWhereverMgRemoteIsLinked) {
#if MOBILEGL_BUILD_DISAGGREGATED
EXPECT_NE(MG_Pipe::MGPipeRecordFailHookInstalled(), nullptr)
<< "FatalFunnel.cpp's static registration did not run: the record arm's deaths would skip "
"SessionFail's count and SessionFault frame";
EXPECT_NE(MG_Pipe::MGPipeRecordLatchHookInstalled(), nullptr);
EXPECT_NE(MG_Pipe::MGPipeRecordLatchedHookInstalled(), nullptr);
#else
EXPECT_EQ(MG_Pipe::MGPipeRecordFailHookInstalled(), nullptr) << "a library without MG_Remote has no funnel";
EXPECT_EQ(MG_Pipe::MGPipeRecordLatchHookInstalled(), nullptr);
EXPECT_EQ(MG_Pipe::MGPipeRecordLatchedHookInstalled(), nullptr);
#endif
EXPECT_FALSE(MG_Pipe::MGPipeRecordLatched()) << "nothing latched in a process with no session";
}
TEST(RecordFailSeamDeathTest, AFailWritesTheCallersLineAndDies) {
EXPECT_DEATH(MG_Pipe::MGPipeRecordFail(MGFatalFamily::ProtocolCorruption,
"MGPipe: Fatal{ProtocolCorruption, \"record-seam-unit\"} n=%d", 7),
"Fatal\\{ProtocolCorruption, \"record-seam-unit\"\\} n=7");
}
TEST(RecordFailSeamDeathTest, ALatchWithNoSessionToLatchDiesTheSameWay) {
EXPECT_DEATH((void)MG_Pipe::MGPipeRecordLatch(MGFatalFamily::BackendDeviceLost,
"MGPipe: Fatal{BackendDeviceLost, \"record-seam-unit\"} n=%d", 9),
"Fatal\\{BackendDeviceLost, \"record-seam-unit\"\\} n=9");
}
namespace {
MGFatalFamily g_seenFamily{};
std::string g_seenLine;
bool CaptureLatch(MGFatalFamily family, const char* line) {
g_seenFamily = family;
g_seenLine = line;
return false;
}
bool AlwaysLatched() { return true; }
} // namespace
// An installed latch hook gets the family and the FORMATTED line, and its answer is the call's.
TEST(RecordFailSeam, AnInstalledLatchHookGetsTheFormattedLine) {
const auto fail = MG_Pipe::MGPipeRecordFailHookInstalled();
const auto latch = MG_Pipe::MGPipeRecordLatchHookInstalled();
const auto latched = MG_Pipe::MGPipeRecordLatchedHookInstalled();
MG_Pipe::MGPipeInstallRecordFailHooks(fail, &CaptureLatch, &AlwaysLatched);
const bool answer = MG_Pipe::MGPipeRecordLatch(MGFatalFamily::BackendDeviceLost,
"MGPipe: Fatal{BackendDeviceLost, \"captured\"} k=%u", 3u);
const bool nowLatched = MG_Pipe::MGPipeRecordLatched();
MG_Pipe::MGPipeInstallRecordFailHooks(fail, latch, latched);
EXPECT_FALSE(answer);
EXPECT_TRUE(nowLatched);
EXPECT_EQ(g_seenFamily, MGFatalFamily::BackendDeviceLost);
EXPECT_EQ(g_seenLine, "MGPipe: Fatal{BackendDeviceLost, \"captured\"} k=3");
EXPECT_EQ(MG_Pipe::MGPipeRecordLatchHookInstalled(), latch) << "the case did not restore the hooks";
}
+20 -3
View File
@@ -110,7 +110,8 @@ EXCLUDED_PARTS = ("MG_Test", "MG_Benchmark", "MG_IntegrationTest")
SOURCE_SUFFIXES = (".cpp", ".h", ".inc")
BASELINE = Path(__file__).with_name("fatal_census_baseline.json")
FAMILIES_DEF = ROOT / "MobileGL" / "MG_Remote" / "FatalFamilies.def"
# P13 W5: the vocabulary moved to MG_Pipe so the record arm can name its deaths without MG_Remote.
FAMILIES_DEF = ROOT / "MobileGL" / "MG_Pipe" / "FatalFamilies.def"
PROTOCOL_FBS = ROOT / "MobileGL" / "MG_Remote" / "Protocol" / "protocol.fbs"
# The funnel's own files. WireLog.cpp's abort is reached only through WireLogFatal, and rule 2
@@ -125,6 +126,9 @@ FUNNEL_FILES = {
# call, so it is exempt from both rules for the same reason WireLog is.
"MobileGL/MG_Remote/FatalFunnel.cpp",
"MobileGL/MG_Remote/FatalFunnel.h",
# P13 W5: the seam header declares MGPipeRecordFail / MGPipeRecordLatch; it has no abort and no
# call, and its declarations would otherwise read as calls with no family word.
"MobileGL/MG_Pipe/PipeSessionFail.h",
}
# THE ALLOW-LIST FOR FUNNELS THAT DO NOT GET A WHOLE FILE TO THEMSELVES (P7 wave 0). An unmarked
@@ -141,6 +145,10 @@ FUNNEL_SITES = {
"the no-hook default of the backend session-fail seam (MG_Pipe/PipeSessionFail.h): the "
"line it logs is the CALLER's, family word and all, and with MG_Remote's hook installed "
"this abort is never reached at all"),
("[[noreturn]] void RecordDeath(",
"P13 W5: the no-hook default of the record arm's fail seam (MGPipeRecordFail / "
"MGPipeRecordLatch): the line is the CALLER's, family word and all, and wherever MG_Remote "
"is linked FatalFunnel.cpp's static registration sends it through SessionFail instead"),
),
"MobileGL/MG_Pipe/generated/PipeWire.inc": (
("MGPipeWireProtocolFatal",
@@ -180,6 +188,10 @@ SESSION_FAIL_CALL = re.compile(r"(?<![A-Za-z0-9_])SessionFail\s*\(")
# word to FatalFamilies.def. Without this, converting a site from SessionFail to SessionLatch
# would quietly take it out of the census.
SESSION_LATCH_CALL = re.compile(r"(?<![A-Za-z0-9_])SessionLatch\s*\(")
# P13 W5: the record arm's twins (MG_Pipe/PipeSessionFail.h), which reach SessionFail / SessionLatch
# through FatalFunnel.cpp's registration and die alone without it - held to the same rule. A
# definition (`void MGPipeRecordFail(` / `bool MGPipeRecordLatch(`) is not a call.
RECORD_FAIL_CALL = re.compile(r"(?<![A-Za-z0-9_])(?<!void )(?<!bool )MGPipeRecord(?:Fail|Latch)\s*\(")
FAMILY_ROW = re.compile(r"^\s*X\(([A-Za-z][A-Za-z0-9]*)\s*,", re.MULTILINE)
REFUSE_ENUM = re.compile(r"enum\s+RefuseCode\s*:[^{]*\{(.*?)\}", re.DOTALL)
ENUM_VALUE = re.compile(r"^\s*([A-Za-z][A-Za-z0-9]*)\s*=", re.MULTILINE)
@@ -310,7 +322,7 @@ def unnamed_funnel_call_lines(lines):
found = []
for index, line in enumerate(lines):
if not (WIRE_LOG_FATAL_CALL.search(line) or SESSION_FAIL_CALL.search(line) or
SESSION_LATCH_CALL.search(line)):
SESSION_LATCH_CALL.search(line) or RECORD_FAIL_CALL.search(line)):
continue
window = "\n".join(lines[index:index + kMarkerWindow])
if not MARKER.search(window):
@@ -331,6 +343,11 @@ SELF_TEST_CASES = (
('WireLogFatal("MGPipe: no family word");', 1),
('MGPipeSessionLatch(family, format);', 0),
('MGPipeSessionFail(family, format);', 0),
('MGPipeRecordFail(MGFatalFamily::ProtocolCorruption, "MGPipe: no family word");', 1),
('MGPipeRecordLatch(MGFatalFamily::ProtocolCorruption, "MGPipe: no family word %d", x);', 1),
('MGPipeRecordFail(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \\"w\\"}");', 0),
('void MGPipeRecordFail(MGFatalFamily family, const char* fmt, ...) {', 0),
('bool MGPipeRecordLatch(MGFatalFamily family, const char* fmt, ...) {', 0),
)
@@ -485,7 +502,7 @@ def main():
rowless = sorted(set(result["families"]) - declared_families())
if rowless:
failures.append(
"family word(s) with no row in MG_Remote/FatalFamilies.def: " + ", ".join(rowless) +
"family word(s) with no row in MG_Pipe/FatalFamilies.def: " + ", ".join(rowless) +
". A row is the family's projection onto the seven-value wire FatalCode (5.2's D2 "
"ruling); a word with no row tells the peer nothing, and FatalCodeForFamily cannot "
"compile a call that names it.")
+2 -2
View File
@@ -1,5 +1,5 @@
{
"abort_sites": 75,
"abort_sites": 76,
"abort_sites_by_file": {
"MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp": 12,
"MobileGL/MG_Backend/DirectGLES/Managers.cpp": 12,
@@ -13,7 +13,7 @@
"MobileGL/MG_Impl/Pipe/PipeFill.cpp": 5,
"MobileGL/MG_Impl/Pipe/ResourceTracker.h": 5,
"MobileGL/MG_Impl/Pipe/SlotAllocator.cpp": 2,
"MobileGL/MG_Pipe/PipeApply.cpp": 4,
"MobileGL/MG_Pipe/PipeApply.cpp": 5,
"MobileGL/MG_Pipe/PipeRoute.cpp": 7,
"MobileGL/MG_Pipe/generated/PipeFilled.inc": 1,
"MobileGL/MG_Pipe/generated/PipeWire.inc": 1,