mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
[MG_Remote] (P6/dl): a dying session names itself to the peer
Step three of CONTRACT-P6 5.2, the funnel's whole point: instead of the peer
reading a bare EOF and guessing, the session publishes a SessionFault frame
that names the family before it aborts.
THE WIRE CHANGE IS D2's, exactly: `table Fatal` appends `family: string`,
append-only, so an older peer still reads `code` and `message`. `code` stays
the coarse seven-value projection; `family` carries the full word.
SessionFail publishes it before the abort, on whichever session's control
plane is active - the client has a ClientSession, the server a ServerSession,
so under spawn the frame reaches the far side whichever role is dying. Under
inproc both exist and it goes to an in-process transport the aborting process
will never read, which is harmless and cheaper than a special case that would
then be the untested one. Best-effort by nature: this thread is about to die,
so a send that blocks or fails changes nothing.
seq/op from the prose are NOT threaded through the 90 sites: the message
already names the op where it matters, so the frame carries what it can name
honestly (code, family, message) rather than a field that is always zero.
The client recognizes it: RunRemoteSurfaceControlFrame's reply loop, which
already demuxes CapsSnapshot from SurfaceReply, gains a Fatal arm that logs the
family and detail, latches device-lost from it, and returns transport-closed.
A server that DETECTS a fatal condition and aborts itself now tells the client
why; a kill -9 still bypasses abort entirely and is caught by dl's hangup
witness instead - the two cover the two ways a server can die.
The census learns SessionFail: a SessionFail call, like a WireLogFatal call,
must carry a Fatal{ word in its string, so the enum and the string cannot
disagree. FatalFunnel.{h,cpp} join the funnel exemption. Baseline is 4 aborts.
FatalFamilyTest gains the wire round-trip: every family's frame carries its
word and its projected code across encode/verify/decode.
G1 re-measured after the schema change: 0 symbol diff, .text unchanged at
0xa52203. Unit 2359 (one pre-existing environmental failure), integration-split
186/186, integration-spawn 102/102, retrace 6/6, census 0 unmarked.
This commit is contained in:
@@ -1034,6 +1034,21 @@ namespace MobileGL::MG_Remote::Client {
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (envelope->msg_type() == ::MobileGL::Wire::CtrlMsg::Fatal) {
|
||||
// `dl` step three: the server published a SessionFault before it aborted, so this
|
||||
// is a death that NAMES ITSELF instead of arriving as a bare EOF. Log the family
|
||||
// and detail, latch device-lost from it, and report the op declined - the same
|
||||
// answer a hangup gives, now with a cause the guest's log can read.
|
||||
if (const auto* fatal = envelope->msg_as_Fatal()) {
|
||||
const char* fam = fatal->family() ? fatal->family()->c_str() : "<none>";
|
||||
const char* msg = fatal->message() ? fatal->message()->c_str() : "";
|
||||
MGLOG_E("MG_Remote client: the server published a SessionFault before dying - "
|
||||
"Fatal family %s: %s", fam, msg);
|
||||
}
|
||||
LatchDeviceLost("the server published a SessionFault and then aborted");
|
||||
frame.ok = false;
|
||||
return MOBILEGL_ERR_TRANSPORT_CLOSED;
|
||||
}
|
||||
if (envelope->msg_type() != ::MobileGL::Wire::CtrlMsg::SurfaceReply ||
|
||||
envelope->msg_as_SurfaceReply() == nullptr) {
|
||||
MGLOG_E("MG_Remote client: expected a SurfaceReply, got message type %d",
|
||||
|
||||
@@ -9,11 +9,17 @@
|
||||
#include <MG_Remote/FatalFunnel.h>
|
||||
|
||||
#include "../Includes.h" // MGLOG_F and the umbrella; a .cpp may pull it, a Transport/ header may not
|
||||
#include "Client/ClientSession.h"
|
||||
#include "Protocol/generated/protocol_generated.h"
|
||||
#include "Server/ServerSession.h"
|
||||
#include "Transport/ITransport.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdarg>
|
||||
#include <cstdio>
|
||||
|
||||
#include <flatbuffers/flatbuffers.h>
|
||||
|
||||
namespace MobileGL::MG_Remote {
|
||||
|
||||
namespace {
|
||||
@@ -50,11 +56,39 @@ namespace MobileGL::MG_Remote {
|
||||
std::fputc('\n', stderr);
|
||||
std::fflush(stderr);
|
||||
|
||||
// `dl` step three publishes a SessionFault{family, detail, seq, op} frame to the peer
|
||||
// here, so the guest's log names which record ended the session instead of reading a bare
|
||||
// EOF. The family's wire projection is FatalCodeForFamily(family); until the frame lands
|
||||
// the family is still carried in the line above and counted here.
|
||||
(void)family;
|
||||
// A SessionFault frame to the peer, BEFORE the abort, so the other side's log names the
|
||||
// family instead of reading a bare EOF (5.2). Best-effort by nature: this thread is about
|
||||
// to die, so a send that blocks or fails changes nothing it could have changed anyway.
|
||||
//
|
||||
// WHICHEVER ROLE THIS PROCESS IS. Under spawn only one session is active per process -
|
||||
// the client has ClientSession, the server has ServerSession - so publishing on whatever
|
||||
// control plane exists reaches the far side. Under inproc both exist and the frame goes
|
||||
// to an in-process transport the aborting process will never read; harmless, and not
|
||||
// worth a special case that would then be the untested one.
|
||||
//
|
||||
// `code` is the coarse seven-value projection; `family` carries the full word; `message`
|
||||
// is the same line just logged. seq/op are not threaded through the 90 call sites and the
|
||||
// message already names the op where it matters, so the frame carries what it can name
|
||||
// honestly rather than a field that is always zero.
|
||||
Transport::ITransport* control = nullptr;
|
||||
if (Client::ClientSession* client = Client::ClientSession::Active()) {
|
||||
control = client->Control_Plane();
|
||||
}
|
||||
if (control == nullptr) {
|
||||
if (Server::ServerSession* server = Server::ServerSession::Active()) {
|
||||
control = server->Control_Plane();
|
||||
}
|
||||
}
|
||||
if (control != nullptr) {
|
||||
::flatbuffers::FlatBufferBuilder builder(256);
|
||||
const auto fatal = ::MobileGL::Wire::CreateFatalDirect(
|
||||
builder, FatalCodeForFamily(family), line, FatalFamilyName(family));
|
||||
const auto root = ::MobileGL::Wire::CreateCtrlEnvelope(
|
||||
builder, ::MobileGL::Wire::CtrlMsg::Fatal, fatal.Union());
|
||||
::MobileGL::Wire::FinishCtrlEnvelopeBuffer(builder, root);
|
||||
(void)control->SendFrame(
|
||||
MobileGLByteSpan{builder.GetBufferPointer(), builder.GetSize()});
|
||||
}
|
||||
|
||||
std::abort();
|
||||
}
|
||||
|
||||
@@ -1463,7 +1463,8 @@ struct Fatal FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table {
|
||||
struct Traits;
|
||||
enum FlatBuffersVTableOffset FLATBUFFERS_VTABLE_UNDERLYING_TYPE {
|
||||
VT_CODE = 4,
|
||||
VT_MESSAGE = 6
|
||||
VT_MESSAGE = 6,
|
||||
VT_FAMILY = 8
|
||||
};
|
||||
MobileGL::Wire::FatalCode code() const {
|
||||
return static_cast<MobileGL::Wire::FatalCode>(GetField<uint32_t>(VT_CODE, 0));
|
||||
@@ -1471,12 +1472,17 @@ struct Fatal FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table {
|
||||
const ::flatbuffers::String *message() const {
|
||||
return GetPointer<const ::flatbuffers::String *>(VT_MESSAGE);
|
||||
}
|
||||
const ::flatbuffers::String *family() const {
|
||||
return GetPointer<const ::flatbuffers::String *>(VT_FAMILY);
|
||||
}
|
||||
template <bool B = false>
|
||||
bool Verify(::flatbuffers::VerifierTemplate<B> &verifier) const {
|
||||
return VerifyTableStart(verifier) &&
|
||||
VerifyField<uint32_t>(verifier, VT_CODE, 4) &&
|
||||
VerifyOffset(verifier, VT_MESSAGE) &&
|
||||
verifier.VerifyString(message()) &&
|
||||
VerifyOffset(verifier, VT_FAMILY) &&
|
||||
verifier.VerifyString(family()) &&
|
||||
verifier.EndTable();
|
||||
}
|
||||
};
|
||||
@@ -1491,6 +1497,9 @@ struct FatalBuilder {
|
||||
void add_message(::flatbuffers::Offset<::flatbuffers::String> message) {
|
||||
fbb_.AddOffset(Fatal::VT_MESSAGE, message);
|
||||
}
|
||||
void add_family(::flatbuffers::Offset<::flatbuffers::String> family) {
|
||||
fbb_.AddOffset(Fatal::VT_FAMILY, family);
|
||||
}
|
||||
explicit FatalBuilder(::flatbuffers::FlatBufferBuilder &_fbb)
|
||||
: fbb_(_fbb) {
|
||||
start_ = fbb_.StartTable();
|
||||
@@ -1505,8 +1514,10 @@ struct FatalBuilder {
|
||||
inline ::flatbuffers::Offset<Fatal> CreateFatal(
|
||||
::flatbuffers::FlatBufferBuilder &_fbb,
|
||||
MobileGL::Wire::FatalCode code = MobileGL::Wire::FatalCode::None,
|
||||
::flatbuffers::Offset<::flatbuffers::String> message = 0) {
|
||||
::flatbuffers::Offset<::flatbuffers::String> message = 0,
|
||||
::flatbuffers::Offset<::flatbuffers::String> family = 0) {
|
||||
FatalBuilder builder_(_fbb);
|
||||
builder_.add_family(family);
|
||||
builder_.add_message(message);
|
||||
builder_.add_code(code);
|
||||
return builder_.Finish();
|
||||
@@ -1520,12 +1531,15 @@ struct Fatal::Traits {
|
||||
inline ::flatbuffers::Offset<Fatal> CreateFatalDirect(
|
||||
::flatbuffers::FlatBufferBuilder &_fbb,
|
||||
MobileGL::Wire::FatalCode code = MobileGL::Wire::FatalCode::None,
|
||||
const char *message = nullptr) {
|
||||
const char *message = nullptr,
|
||||
const char *family = nullptr) {
|
||||
auto message__ = message ? _fbb.CreateString(message) : 0;
|
||||
auto family__ = family ? _fbb.CreateString(family) : 0;
|
||||
return MobileGL::Wire::CreateFatal(
|
||||
_fbb,
|
||||
code,
|
||||
message__);
|
||||
message__,
|
||||
family__);
|
||||
}
|
||||
|
||||
struct LogLine FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table {
|
||||
|
||||
@@ -264,6 +264,11 @@ enum FatalCode : uint {
|
||||
table Fatal {
|
||||
code: FatalCode;
|
||||
message: string;
|
||||
// P6 dl (CONTRACT-P6 5.2, D2). The internal MGFatalFamily word, so a peer that receives this
|
||||
// frame before the sender aborts can name WHICH family ended the session instead of reading a
|
||||
// bare EOF. `code` stays the coarse seven-value projection; this carries the full vocabulary.
|
||||
// Append-only: an older peer that does not read it still gets `code` and `message`.
|
||||
family: string;
|
||||
}
|
||||
|
||||
// Severity-graded per plan B section 8.2: <= Warn is lossy, >= Error is
|
||||
|
||||
@@ -9,7 +9,9 @@
|
||||
// P6 `dl` (CONTRACT-P6 5.2). The Fatal-family table and its projection onto the wire FatalCode.
|
||||
|
||||
#include <MG_Remote/FatalFamily.h>
|
||||
#include <MG_Remote/Protocol/generated/protocol_generated.h>
|
||||
|
||||
#include <flatbuffers/flatbuffers.h>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <set>
|
||||
@@ -81,3 +83,28 @@ TEST(FatalFamily, TheAnchorFamiliesProjectWhereTheirMeaningSays) {
|
||||
// An unmigrated verb is a protocol-level "cannot honour this", not a crash.
|
||||
EXPECT_EQ(FatalCodeForFamily(MGFatalFamily::UnmigratedVerb), FatalCode::ProtocolCorruption);
|
||||
}
|
||||
|
||||
TEST(FatalFamily, TheSessionFaultFrameCarriesTheFamilyAndItsCode) {
|
||||
// `dl` step three (5.2): the Fatal frame a dying session publishes carries the full family
|
||||
// WORD beside the coarse code, so a peer names which family ended the session instead of
|
||||
// reading a bare EOF. This is the wire round-trip that proves the field is there and that the
|
||||
// code the frame carries is the family's own projection.
|
||||
for (const MGFatalFamily family : kAllFamilies) {
|
||||
::flatbuffers::FlatBufferBuilder builder(256);
|
||||
const auto fatal = ::MobileGL::Wire::CreateFatalDirect(
|
||||
builder, FatalCodeForFamily(family), "detail line", FatalFamilyName(family));
|
||||
const auto root = ::MobileGL::Wire::CreateCtrlEnvelope(
|
||||
builder, ::MobileGL::Wire::CtrlMsg::Fatal, fatal.Union());
|
||||
::MobileGL::Wire::FinishCtrlEnvelopeBuffer(builder, root);
|
||||
|
||||
::flatbuffers::Verifier verifier(builder.GetBufferPointer(), builder.GetSize());
|
||||
ASSERT_TRUE(::MobileGL::Wire::VerifyCtrlEnvelopeBuffer(verifier));
|
||||
const auto* envelope = ::MobileGL::Wire::GetCtrlEnvelope(builder.GetBufferPointer());
|
||||
ASSERT_EQ(envelope->msg_type(), ::MobileGL::Wire::CtrlMsg::Fatal);
|
||||
const auto* decoded = envelope->msg_as_Fatal();
|
||||
ASSERT_NE(decoded, nullptr);
|
||||
ASSERT_NE(decoded->family(), nullptr) << "the family field did not survive the wire";
|
||||
EXPECT_STREQ(decoded->family()->c_str(), FatalFamilyName(family));
|
||||
EXPECT_EQ(decoded->code(), FatalCodeForFamily(family));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user