| title | C++ Runtime Correctness Notes | |||
|---|---|---|---|---|
| scope | cpp-lib/tests | |||
| owner_repo | esnacc | |||
| entry_for |
|
|||
| purpose | Record intended C++ runtime semantics and implementation status for ROSE correctness areas exercised by cpp-lib runtime tests. | |||
| read_when |
|
|||
| related_docs |
|
This note records the intended semantics for selected cpp-lib runtime behaviors
and whether the current tree implements them. The goal is to align the runtime
to the public API contract and to common operator expectations rather than
simply preserving whatever behavior exists today.
Primary reference points:
cpp-lib/include/SnaccROSEBase.hcpp-lib/include/SnaccROSEInterfaces.hcpp-lib/include/SnaccTelemetry.hcpp-lib/src/SnaccROSEBase.cpp
| Area | Status | Semantics | Primary tests |
|---|---|---|---|
StopProcessing() shutdown gate |
Implemented | Refuse new outbound work; block inbound handler dispatch; complete pending ops with ROSE_TE_SHUTDOWN |
PublicApiRuntimeTest.StopProcessingBlocks*, LifecycleRuntimeTest.StopProcessing* |
Fire-and-forget (iTimeout == 0) telemetry |
Implemented | Outcome::DISPATCHED + Reason::WAIT_SKIPPED, not UNHANDLED |
TelemetryRuntimeTest.WaitSkippedTelemetry* |
| Response payload decode telemetry | Implemented | Caller-visible ROSE_RE_DECODE_FAILED drives UNHANDLED + DECODE_FAILED, not envelope kind |
TelemetryRuntimeTest.*PayloadDecodeFailureTelemetry* |
| Inbound decode failures and ROSE rejects | Implemented | Garbage wire silent; targeted reject only after envelope decode | InvokeContextRuntimeTest.UnparsableInbound*, section 5 |
OnBinaryDataBlockResult() decode-error hooks |
Implemented | OnRoseDecodeError() and bAlreadyTransportLogged parity with OnBinaryDataBlock() |
PublicApiSmokeTest.OnBinaryDataBlockResultDecodeErrorsInvokeHook* |
Inbound ROSEMessage ownership |
Implemented | unique_ptr at decode sites; std::move through dispatch |
Section 6; InvokeContextRuntimeTest suite |
Outbound encode / Send() ownership |
Implemented | RAII encode helpers detach borrowed arms on scope exit (including encode exceptions) | Section 7; outbound encode-failure tests in InvokeContextRuntimeTest |
SnaccROSEBase documents shutdown as a hard stop:
/*! Shutdown.
Call this function to stop processing any more Invokes.
All pending operations will be completed and new function calls will be blocked.
All Functions return a ROSE_TE_SHUTDOWN */
void StopProcessing(bool bStop = true);Treat StopProcessing(true) as a real runtime shutdown gate:
- New outbound invokes and events must fail fast with
ROSE_TE_SHUTDOWN. - Pending operations must still be completed with
ROSE_TE_SHUTDOWN. - New inbound invokes and inbound events must not be dispatched to application handlers while shutdown is active.
- Late inbound responses that arrive after pending operations were force- completed may be ignored, but they must not resurrect completed work.
StopProcessing(false)re-enables processing; callers must treat that as an explicit restart, not an incidental side effect.
StopProcessing(true) clears m_bProcessingAllowed and completes all pending
operations with ROSE_TE_SHUTDOWN:
void SnaccROSEBase::StopProcessing(bool bStop /*= true*/)
{
{
std::lock_guard<std::mutex> guard(m_InternalProtectMutex);
m_bProcessingAllowed = bStop ? false : true;
}
if (bStop)
CompleteAllPendingOperations();
}
...
for (auto it = m_PendingOperations.begin(); it != m_PendingOperations.end(); it++)
it->second->CompleteOperation(ROSE_TE_SHUTDOWN);Outbound choke points check IsProcessingAllowed() before creating pending
operations or sending:
if (!IsProcessingAllowed())
{
auto telemetry = SnaccTelemetryData::Create(...);
telemetry->finalize(..., SnaccTelemetryData::Reason::SHUTDOWN, ROSE_TE_SHUTDOWN, ...);
OnInvokeProcessed(telemetry);
return ROSE_TE_SHUTDOWN;
}SendEvent() uses the same gate and returns ROSE_TE_SHUTDOWN without sending.
Inbound invoke/event dispatch is blocked in OnInvokeMessage():
if (!IsProcessingAllowed())
lResult = ROSE_TE_SHUTDOWN;Wire data may still be decoded on the receive path; handlers are not reached while shutdown is active.
PublicApiRuntimeTest.StopProcessingBlocksNewOutboundInvokesAndEventsPublicApiRuntimeTest.StopProcessingBlocksInboundDispatchUntilReEnabledLifecycleRuntimeTest.StopProcessingCompletesPendingInvokeWithShutdownBerLifecycleRuntimeTest.StopProcessingCompletesPendingInvokeWithShutdownJsonLifecycleRuntimeTest.PendingInvokeCanRecoverAfterShutdownOnNextFixtureSetupBerLifecycleRuntimeTest.PendingInvokeCanRecoverAfterShutdownOnNextFixtureSetupJson
Fire-and-forget is a successful local dispatch of an invoke whose remote outcome is intentionally unknown to this runtime instance:
Outcome::DISPATCHEDfor "sent, not awaited".Reason::WAIT_SKIPPEDpreserves the explicit cause.Stage::OUTBOUND_WAITis acceptable for now; a finer stage taxonomy is deferred until async invokes that complete via callback reshape outbound lifecycle telemetry anyway.
WAIT_SKIPPED must not be classified under the same top-level failure bucket as
transport errors, timeouts, shutdown, invalid responses, or decode failures.
When iTimeout == 0, SendInvoke() records local success (ROSE_NOERROR) and
does not wait for a response. FinalizeTelemetry() then classifies the
lifecycle as dispatched, not unhandled:
m_pTelemetry->finalize(SnaccTelemetryData::Outcome::DISPATCHED, SnaccTelemetryData::Stage::OUTBOUND_WAIT, SnaccTelemetryData::Reason::WAIT_SKIPPED, m_lRoseResult, std::nullopt, std::move(pctx));SnaccTelemetryData::Outcome::DISPATCHED and its debug text are defined in
cpp-lib/include/SnaccTelemetry.h and cpp-lib/src/SnaccTelemetry.cpp.
TelemetryRuntimeTest.WaitSkippedTelemetryBerTelemetryRuntimeTest.WaitSkippedTelemetryJson
For outbound invoke telemetry, the final caller-visible result is the authoritative classification:
- If the response envelope was received but payload decode fails, telemetry
finalizes as
Outcome::UNHANDLED. - The reason is
DECODE_FAILED. - The result code is
ROSE_RE_DECODE_FAILED. - Envelope kind (
resultvserror) must not override the primary outcome.
HandleInvokeResult() can return ROSE_RE_DECODE_FAILED after a valid envelope
when the embedded result or error payload cannot be decoded. FinalizeTelemetry()
compares the stored pending-op result with the final caller-visible result and
prefers the final outcome when they differ:
if (m_pAnswerMessage && lFinalRoseResult != m_lRoseResult)
{
m_pTelemetry->finalize(SnaccTelemetryData::Outcome::UNHANDLED, GetOutboundUnhandledStageFromResult(lFinalRoseResult), GetUnhandledReasonFromResult(lFinalRoseResult), lFinalRoseResult, m_stResponseData, std::move(pctx));
return;
}When payload decode succeeds, envelope kind still drives RESULT, ERR, or
REJECT telemetry as before.
TelemetryRuntimeTest.ResultPayloadDecodeFailureTelemetryBerTelemetryRuntimeTest.ResultPayloadDecodeFailureTelemetryJsonTelemetryRuntimeTest.ErrorPayloadDecodeFailureTelemetryBerTelemetryRuntimeTest.ErrorPayloadDecodeFailureTelemetryJson
Both inbound entry points call OnRoseDecodeError() for comparable decode
failure classes (BER envelope decode, JSON envelope decode, JSON parse failure,
unknown encoding). Both pass the real bAlreadyTransportLogged value derived
from LogTransportData() return value before invoking the hook.
Shared private methods on SnaccROSEBase centralize logging, hook invocation,
optional reject, and telemetry:
| Method | Role |
|---|---|
HandleInboundEnvelopeSnaccDecodeFailure |
SnaccException after BER BDec or JSON JDec |
HandleInboundJsonParseDecodeFailure |
SJson::Reader::parse failure |
HandleInboundUnknownEncodingDecodeFailure |
Unknown m_eTransportEncoding |
HandleInboundOuterDecodeFailure |
Outer catch around the encoding switch |
EmitInboundDecodeFailureTelemetry |
OnInvokeProcessed for decode failures |
OnBinaryDataBlock() passes bSendReject=true into the envelope helper;
OnBinaryDataBlockResult() passes bSendReject=false.
PublicApiSmokeTest.OnBinaryDataBlockResultDecodeErrorsInvokeHookBerPublicApiSmokeTest.OnBinaryDataBlockResultDecodeErrorsInvokeHookJson
BER is decoded incrementally as nested TLVs. The runtime can fail at different depths on the same buffer:
- Wire garbage — not even a decodable
ROSEMessage(for example truncated tag/length). - Envelope incomplete — some bytes consumed, but
ROSEMessage::BDecdid not finish; generated CHOICEchoiceIdmust not be trusted (codegen deferschoiceIduntil the selected arm decodes successfully). - Envelope OK, payload bad — invoke envelope is valid; operation argument
decode fails later in
OnInvokeMessage.
JSON does not offer an envelope-only parse for invalid wire text.
SJson::Reader::parse is all-or-nothing on the payload after the J length
prefix:
- If parse fails, there is no
SJson::Valuetree and no partial ROSE structure to inspect. - Wire failure and “not a ROSE JSON object” collapse into one step for malformed syntax.
- Only after parse succeeds does
ROSEMessage::JDecrun field-by-field (layer 2 above).
So BER admits layered failure classification at runtime; JSON only admits layers after syntactically valid JSON exists.
Outbound ROSE rejects must be correlatable and semantically honest. Do not
claim mistypedArgument when no invoke was successfully decoded.
| Layer | What failed | bRoseEnvelopeDecoded |
Outbound ROSE reject? |
|---|---|---|---|
| Wire / syntax | BER garbage, JSON parse fail, unknown encoding |
n/a (no envelope) | No — log, OnRoseDecodeError, telemetry only |
| Envelope | BDec / JDec on ROSEMessage did not complete |
false |
No |
| Envelope OK, invoke path | Decode or dispatch failed after envelope succeeded | true and invoke present |
Yes on OnBinaryDataBlock() — mistypedArgument with real invokeID |
| Argument | Operation argument decode in handler path | n/a (handler stage) | Yes — OnInvokeMessage / handler reject path |
Garbage wire therefore gets no response on the application ROSE layer (common
RPC practice: the caller times out; an uncorrelated invokednull reject does not
help a pending client invoke).
| Entry point | Role | Reject on decode failure? |
|---|---|---|
OnBinaryDataBlock() |
Inbound invokes/events (server receive path) | May send targeted mistypedArgument when envelope decode succeeded and invoke is known |
OnBinaryDataBlockResult() |
Inbound results/errors/rejects (client response path) | Must not send rejects for decode failures; log + hook + telemetry only |
Hook and logging parity between the two paths is required. Reject parity is not — the response path must not fabricate server-side rejects when a reply cannot be decoded.
Legacy reject branches were removed from OnBinaryDataBlockResult() decode
catches; that path is telemetry-only on decode failure.
InvokeContextRuntimeTest.UnparsableInboundDoesNotReachHandlerBerInvokeContextRuntimeTest.UnparsableInboundDoesNotReachHandlerJson- CHOICE
choiceIddeferral:compiler/back-ends/c++-gen/gen-code.c(regeneratedSNACCROSE.cpp)
SnaccROSEBase::OnBinaryDataBlock()SnaccROSEBase::OnBinaryDataBlockResult()SnaccROSEBase::OnInvokeMessage()(argument-layer rejects)compiler/back-ends/c++-gen/gen-code.c(CHOICE decode /choiceId)
Inbound decode paths allocate with std::make_unique<ROSEMessage>(). After a
successful envelope decode (BDec / JDec), reject-relevant invoke fields are
snapshotted into InboundInvokeRejectContext (invoke ID, operation ID,
operation name). Ownership then moves into OnROSEMessage() via std::move.
If dispatch throws, the outer catch uses the snapshot for targeted
mistypedArgument rejects — not the moved-away message.
OnROSEMessage() takes std::unique_ptr<ROSEMessage>:
| Stage | Owner |
|---|---|
| Before envelope decode | Local unique_ptr in the decode try block |
After envelope decode, before OnROSEMessage |
Local unique_ptr + optional InboundInvokeRejectContext snapshot |
| Invoke/event dispatch | OnInvokeMessage(std::unique_ptr) — destroyed after dispatch |
| Matched result/error/reject | CompletePendingOperation(std::move) → m_pAnswerMessage |
| Orphan result/error/reject | CompletePendingOperation() when lookup fails |
SnaccException before envelope decode |
Local unique_ptr destroyed on scope exit |
SnaccException after envelope decode |
rejectCtx snapshot drives reject/telemetry; unique_ptr destroyed on scope exit |
Reject policy in decode catch blocks:
| Entry point | Send mistypedArgument reject? |
|---|---|
OnBinaryDataBlock() |
Yes, when rejectCtx is present and invoke ID ≠ 99999 |
OnBinaryDataBlockResult() |
No — telemetry only |
SnaccROSEBase::OnBinaryDataBlock()SnaccROSEBase::OnBinaryDataBlockResult()SnaccROSEBase::OnROSEMessage()SnaccROSEBase::CompletePendingOperation()SnaccROSEPendingOperation::CompleteOperation()
Outbound encoding builds temporary ROSEMessage trees that borrow caller-owned
invoke, result, error, or reject objects. Without explicit detach before
destruction, ~ROSEMessage can delete borrowed values. The previous pattern
used manual // prevent delete nulling on the happy path only, which was fragile
when encode threw.
- Caller retains ownership of invoke arguments, result/error payloads, and reject
objects passed into
Send(),EncodeResult(),EncodeError(), andEncodeReject(). - Stack
ROSEMessageenvelopes used for encoding must detach borrowed arms in all exit paths, including encode exceptions. ScopedInvokeOperationNamemay allocate a temporaryoperationNameon the caller invoke for JSON encoding only; it removes that allocation on scope exit.
File-local RAII helpers in SnaccROSEBase.cpp (anonymous namespace):
| Helper | Role |
|---|---|
RoseEncodeRejectBorrow |
Binds stack ROSEReject into ROSEMessage for EncodeReject |
RoseEncodeResultEnvelope |
Owns stack ROSEResult + encode allocations; borrows result payload |
RoseEncodeErrorEnvelope |
Owns stack ROSEError + AsnAny wrapper; borrows error payload |
ScopedEncodeInvokeBorrow |
Binds caller ROSEInvoke into outbound ROSEMessage for Send |
ScopedInvokeOperationName |
Adds/removes temporary JSON operationName on caller invoke |
Each helper detaches borrowed pointers in its destructor.
- Happy-path outbound invoke/response flows across the runtime test suite
InvokeContextRuntimeTest.OutboundEncodeFailureKeepsCallerContextJson(encode failure must not corrupt caller-owned invoke context)
- Outbound invokes and events: the generated stub literal passed to
SendInvoke/SendEventis the authoritative operation name for telemetry, logging, and transport encoding. Normal outbound contexts useCreateOutboundInvokeContext()(no name on the context). Passstd::optional<unsigned int>to set invoke timeout in the same call; omit it for the connection default (-1). Generated deprecated outbound stubs default toCreateInvokeContext(SnaccInvokeContextInit(OUTBOUND, invoke, operationName))soSNACCDeprecated::DeprecatedASN1Methodcan readOperationName()on the context. - Lookup map:
SnaccRoseOperationLookup::LookUpName()is a parachute when the stub name is absent on outbound paths. Registration in the lookup map is optional but required for inbound context naming when onlyoperationIDis known. - Inbound invokes:
operationIDis authoritative for dispatch. When the client sendsoperationID: 0withoperationName,PrepareInboundInvokeOperationIdresolves the ID viaLookUpIDbefore stub dispatch and before the invoke context is built. The context name is then set viaLookUpName(operationID)— never from the wire string. Synthetic inbound paths without a decoded invoke may pass an explicit name toSnaccInvokeContextInit. - Context factory: runtime and generated stubs must create contexts only through
SnaccROSESender::CreateInvokeContext()(including helpers such asCreateOutboundInvokeContext()). Never callSnaccInvokeContext::Create()from product/runtime code except the defaultCreateInvokeContextimplementation. SnaccInvokeContext::OperationName(): inbound fromLookUpName(operationID). Outbound only when explicitly passed toSnaccInvokeContextInit(deprecated stubs).SnaccInvokeContextInit::m_strOperationName: mirrors the same rule.
Shutdown contract(done)Fire-and-forget telemetry classification(done)Response-payload decode telemetry(done)Inbound decode reject policy and(done)OnBinaryDataBlockResult()reject cleanupInbound and outbound(done)ROSEMessageownership- Push branch and open PR for UCAAS-1446.
Extract shared decode-failure helpers between(done)OnBinaryDataBlock()andOnBinaryDataBlockResult()