From 5926a11be2329f195e7a95b31b641dcf8b2d0a26 Mon Sep 17 00:00:00 2001 From: Jan Fellner Date: Mon, 6 Jul 2026 11:48:04 +0200 Subject: [PATCH 01/10] UCAAS-1446 Add tests core guidelines for AI agents. Tests define expected public API behavior; never tune test or product code to go green unless explicitly asked. --- .cursor/rules/test-specification-first.mdc | 53 ++++++++++++++++++++++ AGENTS.md | 11 +++++ 2 files changed, 64 insertions(+) create mode 100644 .cursor/rules/test-specification-first.mdc diff --git a/.cursor/rules/test-specification-first.mdc b/.cursor/rules/test-specification-first.mdc new file mode 100644 index 0000000..acd318b --- /dev/null +++ b/.cursor/rules/test-specification-first.mdc @@ -0,0 +1,53 @@ +--- +description: Tests specify public API behavior — never tune tests or product code to go green +alwaysApply: true +--- + +# Tests Define the Public API + +**Hard constraint for the snacc ecosystem (compiler, runtimes, generated stubs, samples).** + +Tests document the **expected behavior** of the public API. They are the specification callers and integrators may rely on — not a mirror of whatever the code happens to do today. + +## Non-negotiable rules + +1. **Write tests to specify the API.** The goal is a precise, reviewable contract for public behavior (encoders, decoders, ROSE runtime, generated handlers, etc.) — not merely a green test count. + +2. **NEVER tune test code** to make the suite pass. Do not weaken, delete, skip, or rewrite assertions because the current implementation fails. Do not change expected return codes, context identity, telemetry shape, or error semantics to match existing behavior. + +3. **NEVER tune production or library code** (compiler, `cpp-lib/`, `c-lib/`, generators, runtimes) merely to make tests pass. Do not add shortcuts, bypasses, test-only branches, or special cases whose purpose is to turn a failing spec test green without implementing the specified behavior. + +4. **NEVER tune both sides** in the same change to "meet in the middle." + +5. **When writing tests, do not change test or product code** beyond adding the new test files and neutral test infrastructure (fixtures, CMake wiring) that does not alter what is asserted. + +6. **Only exception:** the user **explicitly asks** to tune test code, tune product code, or revise the spec. Until then, leave failing tests failing and report what failed and why. + +7. **Test-first workflow:** when asked to add tests before fixes, write the **full** scenario matrix (happy + failure paths), run it, and **stop**. Do not fix product code in the same task unless the user asks for that phase. + +8. **Harness and fakes are for observation only.** Test support (`cpp-lib/tests/test_support/`, mocks, loopback transports) may record state and drive scenarios. It must **not** bypass the API under test or smuggle in behavior the real runtime would not satisfy. + +9. **If the spec is unclear, ask.** Do not guess by aligning test and code. Document locked-in semantics in `cpp-lib/tests/runtime_correctness_notes.md`. + +## When a spec test fails (default behavior) + +- **Report** the failure and which spec assertion is unmet. +- **Do not** change the test or product code unless the user explicitly requests it. +- Neutral test infrastructure (fixtures, capture helpers) is allowed if it does not change what is asserted. + +## Forbidden unless the user explicitly asks + +- Weakening or removing assertions +- Changing expected values to match current output +- Harness workarounds that fake green results +- Production hacks (`if (testing)`, alternate code paths for tests) +- `DISABLED` / skipped tests without user approval + +## Primary paths + +- C++ runtime tests: `cpp-lib/tests/` +- Semantics notes: `cpp-lib/tests/runtime_correctness_notes.md` +- ROSE runtime: `cpp-lib/src/SnaccROSEBase.cpp` +- TypeScript samples: `samples/ts-microservice/` + +**The test specifies what the public API must do. Neither tests nor product code are tuned to go green unless the user specially asks.** diff --git a/AGENTS.md b/AGENTS.md index 15f09cd..fd0f822 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -98,6 +98,17 @@ Requirements and IDE-specific steps are in [ReadMe.md](ReadMe.md). CMake minimum - Generated output shape is defined by ASN.1 inputs plus the relevant `compiler/back-ends/*-gen` implementation — read both before changing behavior. - When documentation and code disagree, verify against `samples/` and tests before updating docs. +### Tests define the public API (hard constraint) + +See [.cursor/rules/test-specification-first.mdc](.cursor/rules/test-specification-first.mdc). + +- Tests specify the **expected behavior** of the snacc ecosystem public API — they are the spec, not a mirror of current code. +- **NEVER tune test code** to match broken or incomplete implementation. +- **NEVER tune product or library code** merely to turn a failing spec test green. +- **NEVER tune both** in one change. +- When writing tests, focus on specifying the API — not on getting a green suite. +- **Only exception:** the user explicitly asks to change tests, change product code, or revise the spec. + ## Related repositories - [esnacc-openapi-sdk](https://github.com/ESTOS/esnacc-openapi-sdk) — Swagger UI integration for generated OpenAPI output From 30eedc4438b12404d07bcd3b9ac694a3bd9ec9db Mon Sep 17 00:00:00 2001 From: Jan Fellner Date: Mon, 6 Jul 2026 14:15:30 +0200 Subject: [PATCH 02/10] UCAAS-1446 Add InvokeContext lifecycle runtime tests. Adds 43 spec tests for custom SnaccInvokeContext on inbound and outbound ROSE paths including failure and orphan scenarios. Test harness uses snapshot-based observation at handler and transport callbacks. 39 pass and 4 fail against current runtime behavior. Co-authored-by: Cursor --- cpp-lib/tests/CMakeLists.txt | 1 + cpp-lib/tests/invoke_context_tests.cpp | 718 ++++++++++++++++++ .../test_support/sample_runtime_harness.h | 235 +++++- 3 files changed, 948 insertions(+), 6 deletions(-) create mode 100644 cpp-lib/tests/invoke_context_tests.cpp diff --git a/cpp-lib/tests/CMakeLists.txt b/cpp-lib/tests/CMakeLists.txt index 6bf7f20..158d155 100644 --- a/cpp-lib/tests/CMakeLists.txt +++ b/cpp-lib/tests/CMakeLists.txt @@ -52,6 +52,7 @@ target_link_libraries(snacc-sample-generated PUBLIC snacc_cpp_lib) add_executable(cpp-lib-sample-runtime-tests call_flow_tests.cpp + invoke_context_tests.cpp logging_tests.cpp logical_failure_tests.cpp public_api_tests.cpp diff --git a/cpp-lib/tests/invoke_context_tests.cpp b/cpp-lib/tests/invoke_context_tests.cpp new file mode 100644 index 0000000..ce3832c --- /dev/null +++ b/cpp-lib/tests/invoke_context_tests.cpp @@ -0,0 +1,718 @@ +#include "test_support/sample_runtime_harness.h" + +#include +#include + +namespace sample_runtime_tests +{ +namespace +{ +const SessionInvokeContext* AsSessionContext(const SnaccInvokeContext* pContext) +{ + return dynamic_cast(pContext); +} + +const SnaccTelemetryData* FindOutboundWaitTelemetry(const std::vector>& entries, const SnaccTelemetryData::Reason reason) +{ + const auto it = std::find_if(entries.begin(), entries.end(), [&](const std::shared_ptr& entry) { + return entry && entry->m_Direction == SnaccTelemetryData::Direction::OUTBOUND && entry->m_Stage == SnaccTelemetryData::Stage::OUTBOUND_WAIT && entry->m_Reason == reason; + }); + return it == entries.end() ? nullptr : it->get(); +} + +bool HasInboundResponseTelemetryForOperation(const std::vector>& entries, const unsigned int operationId) +{ + return std::any_of(entries.begin(), entries.end(), [&](const std::shared_ptr& entry) { + return entry && entry->m_Direction == SnaccTelemetryData::Direction::INBOUND && entry->m_Stage == SnaccTelemetryData::Stage::INBOUND_RESPONSE && + entry->m_uiOperationID == operationId; + }); +} +} // namespace + +// Specifies custom SnaccInvokeContext lifecycle for inbound and outbound ROSE paths. +class InvokeContextRuntimeTest : public RuntimeTestBase +{ +protected: + void AssertInboundHandlerReceivesCustomInvokeContext(const TransportEncoding encoding) + { + InitializeEndpoints(encoding); + + AsnGetSettingsArgument argument; + AsnGetSettingsResult result; + AsnRequestError error; + + const long roseResult = m_clientSettingsModule.InvokeGetSettings(&argument, &result, &error); + EXPECT_EQ(ROSE_NOERROR, roseResult); + + const auto& handlerSnapshot = ServerInboundObservation().HandlerSnapshot(); + ASSERT_TRUE(handlerSnapshot.WasCaptured()); + EXPECT_TRUE(handlerSnapshot.IsSessionContext()) << "inbound handler must receive CreateInvokeContext() product"; + EXPECT_EQ("server-session", handlerSnapshot.LocalSessionId()); + EXPECT_EQ("client-session", handlerSnapshot.InvokeSessionId()); + } + + void AssertInboundResponseSendReusesHandlerInvokeContext(const TransportEncoding encoding) + { + InitializeEndpoints(encoding); + + AsnGetSettingsArgument argument; + AsnGetSettingsResult result; + AsnRequestError error; + + const long roseResult = m_clientSettingsModule.InvokeGetSettings(&argument, &result, &error); + EXPECT_EQ(ROSE_NOERROR, roseResult); + + EXPECT_TRUE(ServerInboundObservation().HandlerWasInvoked()); + EXPECT_GT(ServerInboundObservation().TransportSendCount(), 0u); + EXPECT_TRUE(ServerInboundObservation().SameInstanceHandlerToTransport()) + << "inbound response send must reuse the same invoke context instance seen in the handler"; + } + + void AssertInboundHandlerRejectReusesInvokeContext(const TransportEncoding encoding) + { + InitializeEndpoints(encoding); + HandlerModes handlerModes; + handlerModes.implementGetSettings = false; + ConfigureServerHandlers(handlerModes); + + AsnGetSettingsArgument argument; + AsnGetSettingsResult result; + AsnRequestError error; + + const long roseResult = m_clientSettingsModule.InvokeGetSettings(&argument, &result, &error); + EXPECT_EQ(ROSE_REJECT_FUNCTIONMISSING, roseResult); + + EXPECT_TRUE(ServerInboundObservation().HandlerWasInvoked()); + EXPECT_GT(ServerInboundObservation().TransportSendCount(), 0u); + EXPECT_TRUE(ServerInboundObservation().SameInstanceHandlerToTransport()) + << "inbound reject send must reuse the same invoke context instance seen in the handler"; + } + + void AssertInboundApplicationErrorReusesInvokeContext(const TransportEncoding encoding) + { + InitializeEndpoints(encoding); + HandlerModes handlerModes; + handlerModes.getSettingsReturnsError = true; + ConfigureServerHandlers(handlerModes); + + AsnGetSettingsArgument argument; + AsnGetSettingsResult result; + AsnRequestError error; + + const long roseResult = m_clientSettingsModule.InvokeGetSettings(&argument, &result, &error); + EXPECT_EQ(ROSE_ERROR_VALUE, roseResult); + + EXPECT_TRUE(ServerInboundObservation().HandlerWasInvoked()); + EXPECT_GT(ServerInboundObservation().TransportSendCount(), 0u); + EXPECT_TRUE(ServerInboundObservation().SameInstanceHandlerToTransport()) + << "inbound error response send must reuse the same invoke context instance seen in the handler"; + } + + void AssertInboundResponseSendFailureKeepsInvokeContextOnAttemptedSend(const TransportEncoding encoding) + { + InitializeEndpoints(encoding); + m_server.Transport().EnqueueAction(TransportAction::Fail()); + + AsnGetSettingsArgument argument; + AsnGetSettingsResult result; + AsnRequestError error; + + const long roseResult = m_clientSettingsModule.InvokeGetSettings(&argument, &result, &error, 250); + EXPECT_EQ(ROSE_TE_TRANSPORTFAILED, roseResult); + + EXPECT_TRUE(ServerInboundObservation().HandlerWasInvoked()); + EXPECT_GT(ServerInboundObservation().TransportSendCount(), 0u); + EXPECT_TRUE(ServerInboundObservation().SameInstanceHandlerToTransport()) + << "failed inbound response send must still pass the handler invoke context to transport"; + } + + void AssertUnknownInboundOperationRejectUsesCustomInvokeContext(const TransportEncoding encoding) + { + InitializeEndpoints(encoding); + + const long roseResult = m_clientSettingsModule.InvokeUnknownOperation(); + EXPECT_EQ(ROSE_REJECT_UNKNOWNOPERATION, roseResult); + EXPECT_FALSE(ServerInboundObservation().HandlerWasInvoked()) << "module handler must not run for unknown operations"; + + const auto& transportSnapshot = ServerInboundObservation().LastTransportSnapshot(); + ASSERT_TRUE(transportSnapshot.WasCaptured()); + EXPECT_TRUE(transportSnapshot.IsSessionContext()) << "inbound reject must still use CreateInvokeContext() product"; + EXPECT_EQ("server-session", transportSnapshot.LocalSessionId()); + } + + void AssertUnparsableInboundDoesNotReachHandler(const TransportEncoding encoding) + { + InitializeEndpoints(encoding); + m_server.ReceiveRaw(MalformedPayload(encoding)); + + EXPECT_FALSE(ServerInboundObservation().HandlerWasInvoked()); + EXPECT_EQ(0u, ServerInboundObservation().TransportSendCount()); + } + + void AssertOutboundCallerContextReachesTransport(const TransportEncoding encoding) + { + InitializeEndpoints(encoding); + + AsnGetSettingsArgument argument; + AsnGetSettingsResult result; + AsnRequestError error; + SnaccScopedInvokeMessage invokeMsg(m_client.GetNextInvokeID(), OPID_asnGetSettings, &argument); + auto pContext = CreateClientInvokeContext(invokeMsg.GetPtr(), "asnGetSettings"); + pContext->SetTelemetryNote("outbound-request"); + + const long roseResult = m_clientSettingsModule.InvokeGetSettingsWithContext(&argument, &result, &error, pContext); + EXPECT_EQ(ROSE_NOERROR, roseResult); + + const auto& transportSnapshot = ClientOutboundTransportObservation().LastTransportSnapshot(); + ASSERT_TRUE(transportSnapshot.WasCaptured()); + EXPECT_TRUE(transportSnapshot.IsSessionContext()); + EXPECT_EQ("client-session", transportSnapshot.LocalSessionId()); + EXPECT_EQ("outbound-request", transportSnapshot.TelemetryNote()); + EXPECT_EQ(pContext.get(), ClientOutboundTransportObservation().LastContextAddress()); + } + + void AssertOutboundStubCreatedContextReachesTransport(const TransportEncoding encoding) + { + InitializeEndpoints(encoding); + + AsnGetSettingsArgument argument; + AsnGetSettingsResult result; + AsnRequestError error; + + const long roseResult = m_clientSettingsModule.InvokeGetSettings(&argument, &result, &error); + EXPECT_EQ(ROSE_NOERROR, roseResult); + + const auto& transportSnapshot = ClientOutboundTransportObservation().LastTransportSnapshot(); + ASSERT_TRUE(transportSnapshot.WasCaptured()); + EXPECT_TRUE(transportSnapshot.IsSessionContext()) << "outbound invoke without caller context must use CreateInvokeContext() product"; + EXPECT_EQ("client-session", transportSnapshot.LocalSessionId()); + EXPECT_EQ("client-session", transportSnapshot.InvokeSessionId()); + } + + void AssertOutboundCallerContextSurvivesUntilCallerScopeEnds(const TransportEncoding encoding) + { + InitializeEndpoints(encoding); + + AsnGetSettingsArgument argument; + AsnGetSettingsResult result; + AsnRequestError error; + SnaccScopedInvokeMessage invokeMsg(m_client.GetNextInvokeID(), OPID_asnGetSettings, &argument); + auto pContext = CreateClientInvokeContext(invokeMsg.GetPtr(), "asnGetSettings"); + pContext->SetTelemetryNote("caller-owned"); + + const long roseResult = m_clientSettingsModule.InvokeGetSettingsWithContext(&argument, &result, &error, pContext); + EXPECT_EQ(ROSE_NOERROR, roseResult); + EXPECT_FALSE(pContext->WasPreparedForTelemetry()); + EXPECT_EQ("caller-owned", pContext->TelemetryNote()); + + const SnaccTelemetryData* pTelemetry = FindOutboundWaitTelemetry(TelemetryEntries(), SnaccTelemetryData::Reason::REMOTE_RESULT); + ASSERT_NE(nullptr, pTelemetry); + ASSERT_TRUE(pTelemetry->m_pctx); + const auto* pSessionTelemetryContext = AsSessionContext(pTelemetry->m_pctx.get()); + ASSERT_NE(nullptr, pSessionTelemetryContext); + EXPECT_TRUE(pSessionTelemetryContext->WasPreparedForTelemetry()); + EXPECT_EQ("prepared:caller-owned", pSessionTelemetryContext->TelemetryNote()); + } + + void AssertOutboundEncodeFailureStillUsesCallerContext(const TransportEncoding encoding) + { + InitializeEndpoints(encoding); + + AsnSetSettingsArgument argument; + SetSettingsValues(argument.settings, true, std::string(10'000'000, 'x')); + AsnSetSettingsResult result; + AsnRequestError error; + SnaccScopedInvokeMessage invokeMsg(m_client.GetNextInvokeID(), OPID_asnSetSettings, &argument); + auto pContext = CreateClientInvokeContext(invokeMsg.GetPtr(), "asnSetSettings"); + pContext->SetTelemetryNote("encode-failure"); + + const long roseResult = m_client.SendInvoke(invokeMsg.GetPtr(), &result, &error, "asnSetSettings", 250, pContext); + EXPECT_EQ(ROSE_TE_ENCODE_FAILED, roseResult); + EXPECT_FALSE(pContext->WasPreparedForTelemetry()); + EXPECT_EQ("encode-failure", pContext->TelemetryNote()); + EXPECT_EQ(0u, ClientOutboundTransportObservation().TransportSendCount()) << "encode failure must occur before transport send"; + } + + void AssertNoTransportReturnsTransportFailedForInvoke(const TransportEncoding encoding) + { + ObservedRuntimeEndpoint endpoint{L"NoTransportEndpoint", "solo-session"}; + endpoint.SetEncoding(encoding); + SettingsServiceModule serviceModule(endpoint); + ENetUC_Settings_ManagerROSE component(&endpoint); + + AsnGetSettingsArgument argument; + AsnGetSettingsResult result; + AsnRequestError error; + SnaccScopedInvokeMessage invokeMsg(endpoint.GetNextInvokeID(), OPID_asnGetSettings, &argument); + auto pContext = endpoint.CreateSessionInvokeContext(invokeMsg.GetPtr(), "asnGetSettings"); + pContext->SetTelemetryNote("no-transport"); + + const long roseResult = component.Invoke_asnGetSettings(&argument, &result, &error, 250, pContext); + EXPECT_EQ(ROSE_TE_TRANSPORTFAILED, roseResult); + EXPECT_FALSE(pContext->WasPreparedForTelemetry()); + EXPECT_EQ("no-transport", pContext->TelemetryNote()); + } + + void AssertNoTransportReturnsTransportFailedForEvent(const TransportEncoding encoding) + { + ObservedRuntimeEndpoint endpoint{L"NoTransportEndpoint", "solo-session"}; + endpoint.SetEncoding(encoding); + SettingsServiceModule serviceModule(endpoint); + ENetUC_Settings_ManagerROSE component(&endpoint); + + AsnSettingsChangedArgument eventArgument; + SetSettingsValues(eventArgument.settings, true, "event-without-transport"); + auto pContext = endpoint.CreateSessionInvokeContext(nullptr, "asnSettingsChanged"); + pContext->SetTelemetryNote("event-no-transport"); + + const long roseResult = component.Event_asnSettingsChanged(&eventArgument, pContext); + EXPECT_EQ(ROSE_TE_TRANSPORTFAILED, roseResult); + EXPECT_FALSE(pContext->WasPreparedForTelemetry()); + EXPECT_EQ("event-no-transport", pContext->TelemetryNote()); + } + + void AssertOutboundTransportFailureKeepsCallerContextOnSendAttempt(const TransportEncoding encoding) + { + InitializeEndpoints(encoding); + m_client.Transport().EnqueueAction(TransportAction::Fail()); + + AsnGetSettingsArgument argument; + AsnGetSettingsResult result; + AsnRequestError error; + SnaccScopedInvokeMessage invokeMsg(m_client.GetNextInvokeID(), OPID_asnGetSettings, &argument); + auto pContext = CreateClientInvokeContext(invokeMsg.GetPtr(), "asnGetSettings"); + pContext->SetTelemetryNote("transport-failure"); + + const long roseResult = m_clientSettingsModule.InvokeGetSettingsWithContext(&argument, &result, &error, pContext, 250); + EXPECT_EQ(ROSE_TE_TRANSPORTFAILED, roseResult); + ASSERT_GT(ClientOutboundTransportObservation().TransportSendCount(), 0u); + EXPECT_EQ(pContext.get(), ClientOutboundTransportObservation().LastContextAddress()); + } + + void AssertOutboundTimeoutKeepsCallerContextOnRequestSend(const TransportEncoding encoding) + { + InitializeEndpoints(encoding); + m_server.Transport().EnqueueAction(TransportAction::Drop()); + + AsnGetSettingsArgument argument; + AsnGetSettingsResult result; + AsnRequestError error; + SnaccScopedInvokeMessage invokeMsg(m_client.GetNextInvokeID(), OPID_asnGetSettings, &argument); + auto pContext = CreateClientInvokeContext(invokeMsg.GetPtr(), "asnGetSettings"); + pContext->SetTelemetryNote("timeout"); + + const long roseResult = m_clientSettingsModule.InvokeGetSettingsWithContext(&argument, &result, &error, pContext, 25); + EXPECT_EQ(ROSE_TE_TIMEOUT, roseResult); + ASSERT_GT(ClientOutboundTransportObservation().TransportSendCount(), 0u); + EXPECT_EQ(pContext.get(), ClientOutboundTransportObservation().LastContextAddress()); + + const SnaccTelemetryData* pTelemetry = FindOutboundWaitTelemetry(TelemetryEntries(), SnaccTelemetryData::Reason::TIMEOUT); + ASSERT_NE(nullptr, pTelemetry); + ASSERT_TRUE(pTelemetry->m_pctx); + const auto* pSessionTelemetryContext = AsSessionContext(pTelemetry->m_pctx.get()); + ASSERT_NE(nullptr, pSessionTelemetryContext); + EXPECT_TRUE(pSessionTelemetryContext->WasPreparedForTelemetry()); + EXPECT_EQ("prepared:timeout", pSessionTelemetryContext->TelemetryNote()); + } + + void AssertOutboundRemoteRejectKeepsCallerContextOnRequestSend(const TransportEncoding encoding) + { + InitializeEndpoints(encoding); + HandlerModes handlerModes; + handlerModes.implementGetSettings = false; + ConfigureServerHandlers(handlerModes); + + AsnGetSettingsArgument argument; + AsnGetSettingsResult result; + AsnRequestError error; + SnaccScopedInvokeMessage invokeMsg(m_client.GetNextInvokeID(), OPID_asnGetSettings, &argument); + auto pContext = CreateClientInvokeContext(invokeMsg.GetPtr(), "asnGetSettings"); + pContext->SetTelemetryNote("remote-reject"); + + const long roseResult = m_clientSettingsModule.InvokeGetSettingsWithContext(&argument, &result, &error, pContext, 250); + EXPECT_EQ(ROSE_REJECT_FUNCTIONMISSING, roseResult); + ASSERT_GT(ClientOutboundTransportObservation().TransportSendCount(), 0u); + EXPECT_EQ(pContext.get(), ClientOutboundTransportObservation().LastContextAddress()); + EXPECT_FALSE(pContext->WasPreparedForTelemetry()); + EXPECT_EQ("remote-reject", pContext->TelemetryNote()); + } + + void AssertOutboundRemoteErrorKeepsCallerContextOnRequestSend(const TransportEncoding encoding) + { + InitializeEndpoints(encoding); + HandlerModes handlerModes; + handlerModes.getSettingsReturnsError = true; + ConfigureServerHandlers(handlerModes); + + AsnGetSettingsArgument argument; + AsnGetSettingsResult result; + AsnRequestError error; + SnaccScopedInvokeMessage invokeMsg(m_client.GetNextInvokeID(), OPID_asnGetSettings, &argument); + auto pContext = CreateClientInvokeContext(invokeMsg.GetPtr(), "asnGetSettings"); + pContext->SetTelemetryNote("remote-error"); + + const long roseResult = m_clientSettingsModule.InvokeGetSettingsWithContext(&argument, &result, &error, pContext, 250); + EXPECT_EQ(ROSE_ERROR_VALUE, roseResult); + ASSERT_GT(ClientOutboundTransportObservation().TransportSendCount(), 0u); + EXPECT_EQ(pContext.get(), ClientOutboundTransportObservation().LastContextAddress()); + EXPECT_FALSE(pContext->WasPreparedForTelemetry()); + EXPECT_EQ("remote-error", pContext->TelemetryNote()); + } + + void AssertOutboundMalformedResponseKeepsCallerContextThroughDecodeFailure(const TransportEncoding encoding) + { + InitializeEndpoints(encoding); + m_client.Transport().EnqueueAction(TransportAction::Queue()); + + AsnGetSettingsArgument argument; + AsnGetSettingsResult result; + AsnRequestError error; + SnaccScopedInvokeMessage invokeMsg(m_client.GetNextInvokeID(), OPID_asnGetSettings, &argument); + auto pContext = CreateClientInvokeContext(invokeMsg.GetPtr(), "asnGetSettings"); + pContext->SetTelemetryNote("decode-failure"); + + auto responseFuture = std::async(std::launch::async, [&]() { + return m_client.SendInvoke(invokeMsg.GetPtr(), &result, &error, "asnGetSettings", 250, pContext); + }); + + for (int i = 0; i < 20 && m_client.Transport().QueuedMessageCount() == 0; ++i) + std::this_thread::sleep_for(std::chrono::milliseconds(5)); + ASSERT_EQ(1U, m_client.Transport().QueuedMessageCount()); + m_client.Transport().TakeQueuedMessages(); + + std::string responsePayload; + AsnBool wrongPayload(true); + ASSERT_EQ(ROSE_NOERROR, m_server.EncodeResult(invokeMsg.GetPtr()->invokeID, &wrongPayload, responsePayload)); + m_client.ReceiveRaw(responsePayload); + + const long roseResult = responseFuture.get(); + EXPECT_EQ(ROSE_RE_DECODE_FAILED, roseResult); + ASSERT_GT(ClientOutboundTransportObservation().TransportSendCount(), 0u); + EXPECT_EQ(pContext.get(), ClientOutboundTransportObservation().LastContextAddress()); + EXPECT_FALSE(pContext->WasPreparedForTelemetry()); + EXPECT_EQ("decode-failure", pContext->TelemetryNote()); + } + + void AssertOrphanResultDoesNotBreakPendingInvoke(const TransportEncoding encoding) + { + InitializeEndpoints(encoding); + m_server.Transport().EnqueueAction(TransportAction::Queue()); + + AsnGetSettingsArgument argument; + AsnGetSettingsResult result; + AsnRequestError error; + SnaccScopedInvokeMessage invokeMsg(m_client.GetNextInvokeID(), OPID_asnGetSettings, &argument); + auto pContext = CreateClientInvokeContext(invokeMsg.GetPtr(), "asnGetSettings"); + pContext->SetTelemetryNote("pending-invoke"); + + auto pendingInvoke = std::async(std::launch::async, [&]() { + return m_clientSettingsModule.InvokeGetSettingsWithContext(&argument, &result, &error, pContext, 500); + }); + + for (int i = 0; i < 20 && m_server.Transport().QueuedMessageCount() == 0; ++i) + std::this_thread::sleep_for(std::chrono::milliseconds(5)); + ASSERT_EQ(1u, m_server.Transport().QueuedMessageCount()); + + AsnBool orphanPayload(true); + std::string orphanResponse; + ASSERT_EQ(ROSE_NOERROR, m_client.EncodeResult(4242, &orphanPayload, orphanResponse)); + m_client.ReceiveRaw(orphanResponse); + + FlushServerOutbound(); + EXPECT_EQ(ROSE_NOERROR, pendingInvoke.get()); + EXPECT_FALSE(pContext->WasPreparedForTelemetry()); + EXPECT_EQ("pending-invoke", pContext->TelemetryNote()); + } + + void AssertOrphanResultEmitsInboundResponseTelemetryWithoutPendingOperation(const TransportEncoding encoding) + { + InitializeEndpoints(encoding); + + AsnBool orphanPayload(true); + std::string orphanResponse; + ASSERT_EQ(ROSE_NOERROR, m_client.EncodeResult(4242, &orphanPayload, orphanResponse)); + m_client.ReceiveRaw(orphanResponse); + + EXPECT_TRUE(HasInboundResponseTelemetryForOperation(TelemetryEntries(), 0)); + } + + void AssertOrphanErrorDoesNotBreakPendingInvoke(const TransportEncoding encoding) + { + InitializeEndpoints(encoding); + m_server.Transport().EnqueueAction(TransportAction::Queue()); + + AsnGetSettingsArgument argument; + AsnGetSettingsResult result; + AsnRequestError error; + SnaccScopedInvokeMessage invokeMsg(m_client.GetNextInvokeID(), OPID_asnGetSettings, &argument); + auto pContext = CreateClientInvokeContext(invokeMsg.GetPtr(), "asnGetSettings"); + pContext->SetTelemetryNote("pending-error"); + + auto pendingInvoke = std::async(std::launch::async, [&]() { + return m_clientSettingsModule.InvokeGetSettingsWithContext(&argument, &result, &error, pContext, 500); + }); + + for (int i = 0; i < 20 && m_server.Transport().QueuedMessageCount() == 0; ++i) + std::this_thread::sleep_for(std::chrono::milliseconds(5)); + ASSERT_EQ(1u, m_server.Transport().QueuedMessageCount()); + + AsnRequestError orphanError; + orphanError.iErrorDetail = 4242; + orphanError.u8sErrorString.setASCII("orphan error"); + std::string orphanResponse; + ASSERT_EQ(ROSE_NOERROR, m_client.EncodeError(5252, &orphanError, orphanResponse)); + m_client.ReceiveRaw(orphanResponse); + + FlushServerOutbound(); + EXPECT_EQ(ROSE_NOERROR, pendingInvoke.get()); + EXPECT_FALSE(pContext->WasPreparedForTelemetry()); + EXPECT_EQ("pending-error", pContext->TelemetryNote()); + } + + void AssertOrphanRejectDoesNotBreakPendingInvoke(const TransportEncoding encoding) + { + InitializeEndpoints(encoding); + m_server.Transport().EnqueueAction(TransportAction::Queue()); + + AsnGetSettingsArgument argument; + AsnGetSettingsResult result; + AsnRequestError error; + SnaccScopedInvokeMessage invokeMsg(m_client.GetNextInvokeID(), OPID_asnGetSettings, &argument); + auto pContext = CreateClientInvokeContext(invokeMsg.GetPtr(), "asnGetSettings"); + pContext->SetTelemetryNote("pending-reject"); + + auto pendingInvoke = std::async(std::launch::async, [&]() { + return m_clientSettingsModule.InvokeGetSettingsWithContext(&argument, &result, &error, pContext, 500); + }); + + for (int i = 0; i < 20 && m_server.Transport().QueuedMessageCount() == 0; ++i) + std::this_thread::sleep_for(std::chrono::milliseconds(5)); + ASSERT_EQ(1u, m_server.Transport().QueuedMessageCount()); + + std::string orphanResponse; + ASSERT_EQ(ROSE_NOERROR, m_client.EncodeRejectInvoke(6262, InvokeProblem::unrecognisedOperation, orphanResponse, "orphan reject")); + m_client.ReceiveRaw(orphanResponse); + + FlushServerOutbound(); + EXPECT_EQ(ROSE_NOERROR, pendingInvoke.get()); + EXPECT_FALSE(pContext->WasPreparedForTelemetry()); + EXPECT_EQ("pending-reject", pContext->TelemetryNote()); + } +}; + +TEST_F(InvokeContextRuntimeTest, InboundHandlerReceivesCustomInvokeContextBer) +{ + AssertInboundHandlerReceivesCustomInvokeContext(TransportEncoding::BER); +} + +TEST_F(InvokeContextRuntimeTest, InboundHandlerReceivesCustomInvokeContextJson) +{ + AssertInboundHandlerReceivesCustomInvokeContext(TransportEncoding::JSON); +} + +TEST_F(InvokeContextRuntimeTest, InboundResponseSendReusesHandlerInvokeContextBer) +{ + AssertInboundResponseSendReusesHandlerInvokeContext(TransportEncoding::BER); +} + +TEST_F(InvokeContextRuntimeTest, InboundResponseSendReusesHandlerInvokeContextJson) +{ + AssertInboundResponseSendReusesHandlerInvokeContext(TransportEncoding::JSON); +} + +TEST_F(InvokeContextRuntimeTest, InboundHandlerRejectReusesInvokeContextBer) +{ + AssertInboundHandlerRejectReusesInvokeContext(TransportEncoding::BER); +} + +TEST_F(InvokeContextRuntimeTest, InboundHandlerRejectReusesInvokeContextJson) +{ + AssertInboundHandlerRejectReusesInvokeContext(TransportEncoding::JSON); +} + +TEST_F(InvokeContextRuntimeTest, InboundApplicationErrorReusesInvokeContextBer) +{ + AssertInboundApplicationErrorReusesInvokeContext(TransportEncoding::BER); +} + +TEST_F(InvokeContextRuntimeTest, InboundApplicationErrorReusesInvokeContextJson) +{ + AssertInboundApplicationErrorReusesInvokeContext(TransportEncoding::JSON); +} + +TEST_F(InvokeContextRuntimeTest, InboundResponseSendFailureKeepsInvokeContextBer) +{ + AssertInboundResponseSendFailureKeepsInvokeContextOnAttemptedSend(TransportEncoding::BER); +} + +TEST_F(InvokeContextRuntimeTest, InboundResponseSendFailureKeepsInvokeContextJson) +{ + AssertInboundResponseSendFailureKeepsInvokeContextOnAttemptedSend(TransportEncoding::JSON); +} + +TEST_F(InvokeContextRuntimeTest, UnknownInboundOperationRejectUsesCustomInvokeContextBer) +{ + AssertUnknownInboundOperationRejectUsesCustomInvokeContext(TransportEncoding::BER); +} + +TEST_F(InvokeContextRuntimeTest, UnknownInboundOperationRejectUsesCustomInvokeContextJson) +{ + AssertUnknownInboundOperationRejectUsesCustomInvokeContext(TransportEncoding::JSON); +} + +TEST_F(InvokeContextRuntimeTest, UnparsableInboundDoesNotReachHandlerBer) +{ + AssertUnparsableInboundDoesNotReachHandler(TransportEncoding::BER); +} + +TEST_F(InvokeContextRuntimeTest, UnparsableInboundDoesNotReachHandlerJson) +{ + AssertUnparsableInboundDoesNotReachHandler(TransportEncoding::JSON); +} + +TEST_F(InvokeContextRuntimeTest, OutboundCallerContextReachesTransportBer) +{ + AssertOutboundCallerContextReachesTransport(TransportEncoding::BER); +} + +TEST_F(InvokeContextRuntimeTest, OutboundCallerContextReachesTransportJson) +{ + AssertOutboundCallerContextReachesTransport(TransportEncoding::JSON); +} + +TEST_F(InvokeContextRuntimeTest, OutboundStubCreatedContextReachesTransportBer) +{ + AssertOutboundStubCreatedContextReachesTransport(TransportEncoding::BER); +} + +TEST_F(InvokeContextRuntimeTest, OutboundStubCreatedContextReachesTransportJson) +{ + AssertOutboundStubCreatedContextReachesTransport(TransportEncoding::JSON); +} + +TEST_F(InvokeContextRuntimeTest, OutboundCallerContextSurvivesUntilCallerScopeEndsBer) +{ + AssertOutboundCallerContextSurvivesUntilCallerScopeEnds(TransportEncoding::BER); +} + +TEST_F(InvokeContextRuntimeTest, OutboundCallerContextSurvivesUntilCallerScopeEndsJson) +{ + AssertOutboundCallerContextSurvivesUntilCallerScopeEnds(TransportEncoding::JSON); +} + +TEST_F(InvokeContextRuntimeTest, OutboundEncodeFailureKeepsCallerContextJson) +{ + AssertOutboundEncodeFailureStillUsesCallerContext(TransportEncoding::JSON); +} + +TEST_F(InvokeContextRuntimeTest, NoTransportReturnsTransportFailedForInvokeBer) +{ + AssertNoTransportReturnsTransportFailedForInvoke(TransportEncoding::BER); +} + +TEST_F(InvokeContextRuntimeTest, NoTransportReturnsTransportFailedForInvokeJson) +{ + AssertNoTransportReturnsTransportFailedForInvoke(TransportEncoding::JSON); +} + +TEST_F(InvokeContextRuntimeTest, NoTransportReturnsTransportFailedForEventBer) +{ + AssertNoTransportReturnsTransportFailedForEvent(TransportEncoding::BER); +} + +TEST_F(InvokeContextRuntimeTest, NoTransportReturnsTransportFailedForEventJson) +{ + AssertNoTransportReturnsTransportFailedForEvent(TransportEncoding::JSON); +} + +TEST_F(InvokeContextRuntimeTest, OutboundTransportFailureKeepsCallerContextBer) +{ + AssertOutboundTransportFailureKeepsCallerContextOnSendAttempt(TransportEncoding::BER); +} + +TEST_F(InvokeContextRuntimeTest, OutboundTransportFailureKeepsCallerContextJson) +{ + AssertOutboundTransportFailureKeepsCallerContextOnSendAttempt(TransportEncoding::JSON); +} + +TEST_F(InvokeContextRuntimeTest, OutboundTimeoutKeepsCallerContextBer) +{ + AssertOutboundTimeoutKeepsCallerContextOnRequestSend(TransportEncoding::BER); +} + +TEST_F(InvokeContextRuntimeTest, OutboundTimeoutKeepsCallerContextJson) +{ + AssertOutboundTimeoutKeepsCallerContextOnRequestSend(TransportEncoding::JSON); +} + +TEST_F(InvokeContextRuntimeTest, OutboundRemoteRejectKeepsCallerContextBer) +{ + AssertOutboundRemoteRejectKeepsCallerContextOnRequestSend(TransportEncoding::BER); +} + +TEST_F(InvokeContextRuntimeTest, OutboundRemoteRejectKeepsCallerContextJson) +{ + AssertOutboundRemoteRejectKeepsCallerContextOnRequestSend(TransportEncoding::JSON); +} + +TEST_F(InvokeContextRuntimeTest, OutboundRemoteErrorKeepsCallerContextBer) +{ + AssertOutboundRemoteErrorKeepsCallerContextOnRequestSend(TransportEncoding::BER); +} + +TEST_F(InvokeContextRuntimeTest, OutboundRemoteErrorKeepsCallerContextJson) +{ + AssertOutboundRemoteErrorKeepsCallerContextOnRequestSend(TransportEncoding::JSON); +} + +TEST_F(InvokeContextRuntimeTest, OutboundMalformedResponseKeepsCallerContextBer) +{ + AssertOutboundMalformedResponseKeepsCallerContextThroughDecodeFailure(TransportEncoding::BER); +} + +TEST_F(InvokeContextRuntimeTest, OutboundMalformedResponseKeepsCallerContextJson) +{ + AssertOutboundMalformedResponseKeepsCallerContextThroughDecodeFailure(TransportEncoding::JSON); +} + +TEST_F(InvokeContextRuntimeTest, OrphanResultDoesNotBreakPendingInvokeBer) +{ + AssertOrphanResultDoesNotBreakPendingInvoke(TransportEncoding::BER); +} + +TEST_F(InvokeContextRuntimeTest, OrphanResultDoesNotBreakPendingInvokeJson) +{ + AssertOrphanResultDoesNotBreakPendingInvoke(TransportEncoding::JSON); +} + +TEST_F(InvokeContextRuntimeTest, OrphanResultEmitsInboundResponseTelemetryBer) +{ + AssertOrphanResultEmitsInboundResponseTelemetryWithoutPendingOperation(TransportEncoding::BER); +} + +TEST_F(InvokeContextRuntimeTest, OrphanResultEmitsInboundResponseTelemetryJson) +{ + AssertOrphanResultEmitsInboundResponseTelemetryWithoutPendingOperation(TransportEncoding::JSON); +} + +TEST_F(InvokeContextRuntimeTest, OrphanErrorDoesNotBreakPendingInvokeBer) +{ + AssertOrphanErrorDoesNotBreakPendingInvoke(TransportEncoding::BER); +} + +TEST_F(InvokeContextRuntimeTest, OrphanErrorDoesNotBreakPendingInvokeJson) +{ + AssertOrphanErrorDoesNotBreakPendingInvoke(TransportEncoding::JSON); +} + +TEST_F(InvokeContextRuntimeTest, OrphanRejectDoesNotBreakPendingInvokeBer) +{ + AssertOrphanRejectDoesNotBreakPendingInvoke(TransportEncoding::BER); +} + +TEST_F(InvokeContextRuntimeTest, OrphanRejectDoesNotBreakPendingInvokeJson) +{ + AssertOrphanRejectDoesNotBreakPendingInvoke(TransportEncoding::JSON); +} + +} // namespace sample_runtime_tests diff --git a/cpp-lib/tests/test_support/sample_runtime_harness.h b/cpp-lib/tests/test_support/sample_runtime_harness.h index 5601a1d..75ae0c2 100644 --- a/cpp-lib/tests/test_support/sample_runtime_harness.h +++ b/cpp-lib/tests/test_support/sample_runtime_harness.h @@ -143,9 +143,10 @@ class RuntimeEndpoint; class LoopbackTransport : public ISnaccROSETransport { public: - // Binds the transport to the peer endpoint that should receive outbound data. - explicit LoopbackTransport(RuntimeEndpoint& remote) - : m_remote(remote) + // Binds the transport to the sending endpoint and its loopback peer. + LoopbackTransport(RuntimeEndpoint& owner, RuntimeEndpoint& remote) + : m_owner(owner), + m_remote(remote) { } @@ -176,6 +177,7 @@ class LoopbackTransport : public ISnaccROSETransport } private: + RuntimeEndpoint& m_owner; // endpoint that performs outbound sends RuntimeEndpoint& m_remote; // peer endpoint that receives delivered payloads std::deque m_actions; // scripted behaviors consumed one send at a time std::vector m_queuedMessages; // delayed payloads kept until the test flushes them @@ -263,6 +265,170 @@ class SessionInvokeContext : public SnaccInvokeContext std::string m_strTelemetryNote; // extra test data used to verify clone and prepare semantics }; +// Copies invoke-context fields while the runtime reference is still alive. +class InvokeContextSnapshot +{ +public: + void Reset() + { + m_bCaptured = false; + m_bIsSessionContext = false; + m_strLocalSessionId.clear(); + m_strInvokeSessionId.clear(); + m_strTelemetryNote.clear(); + } + + void Capture(const SnaccInvokeContext& ctx) + { + m_bCaptured = true; + if (const auto* pSessionContext = dynamic_cast(&ctx)) + { + m_bIsSessionContext = true; + m_strLocalSessionId = pSessionContext->m_strLocalSessionId; + m_strInvokeSessionId = pSessionContext->m_strInvokeSessionId; + m_strTelemetryNote = pSessionContext->TelemetryNote(); + return; + } + + m_bIsSessionContext = false; + m_strLocalSessionId.clear(); + m_strInvokeSessionId.clear(); + m_strTelemetryNote.clear(); + } + + bool WasCaptured() const + { + return m_bCaptured; + } + + bool IsSessionContext() const + { + return m_bIsSessionContext; + } + + const std::string& LocalSessionId() const + { + return m_strLocalSessionId; + } + + const std::string& InvokeSessionId() const + { + return m_strInvokeSessionId; + } + + const std::string& TelemetryNote() const + { + return m_strTelemetryNote; + } + +private: + bool m_bCaptured = false; + bool m_bIsSessionContext = false; + std::string m_strLocalSessionId; + std::string m_strInvokeSessionId; + std::string m_strTelemetryNote; +}; + +// Records inbound handler and transport observations for one server endpoint. +class InboundInvokeObservation +{ +public: + void Reset() + { + m_handlerSnapshot.Reset(); + m_lastTransportSnapshot.Reset(); + m_pHandlerContextAddress = nullptr; + m_bSameInstanceHandlerToTransport = false; + m_nTransportSendCount = 0; + } + + void CaptureHandlerContext(SnaccInvokeContext& ctx) + { + m_handlerSnapshot.Capture(ctx); + m_pHandlerContextAddress = &ctx; + m_bSameInstanceHandlerToTransport = false; + } + + void CaptureTransportSend(SnaccInvokeContext& ctx) + { + m_lastTransportSnapshot.Capture(ctx); + ++m_nTransportSendCount; + if (m_pHandlerContextAddress != nullptr) + m_bSameInstanceHandlerToTransport = (&ctx == m_pHandlerContextAddress); + } + + const InvokeContextSnapshot& HandlerSnapshot() const + { + return m_handlerSnapshot; + } + + const InvokeContextSnapshot& LastTransportSnapshot() const + { + return m_lastTransportSnapshot; + } + + bool HandlerWasInvoked() const + { + return m_handlerSnapshot.WasCaptured(); + } + + bool SameInstanceHandlerToTransport() const + { + return m_bSameInstanceHandlerToTransport; + } + + size_t TransportSendCount() const + { + return m_nTransportSendCount; + } + +private: + InvokeContextSnapshot m_handlerSnapshot; + InvokeContextSnapshot m_lastTransportSnapshot; + SnaccInvokeContext* m_pHandlerContextAddress = nullptr; + bool m_bSameInstanceHandlerToTransport = false; + size_t m_nTransportSendCount = 0; +}; + +// Records outbound transport observations copied at send time. +class OutboundTransportObservation +{ +public: + void Reset() + { + m_lastTransportSnapshot.Reset(); + m_pLastContextAddress = nullptr; + m_nTransportSendCount = 0; + } + + void CaptureTransportSend(SnaccInvokeContext& ctx) + { + m_lastTransportSnapshot.Capture(ctx); + m_pLastContextAddress = &ctx; + ++m_nTransportSendCount; + } + + const InvokeContextSnapshot& LastTransportSnapshot() const + { + return m_lastTransportSnapshot; + } + + const SnaccInvokeContext* LastContextAddress() const + { + return m_pLastContextAddress; + } + + size_t TransportSendCount() const + { + return m_nTransportSendCount; + } + +private: + InvokeContextSnapshot m_lastTransportSnapshot; + const SnaccInvokeContext* m_pLastContextAddress = nullptr; + size_t m_nTransportSendCount = 0; +}; + // Session-scoped runtime host used on both client and server sides. It models a // single ROSE connection, owns the transport binding, injects session ids, and // dispatches inbound invokes to the registered modules. @@ -281,10 +447,42 @@ class RuntimeEndpoint : public SnaccROSEBase // Attaches a loopback transport that forwards outbound data to the peer host. void ConnectTo(RuntimeEndpoint& remote) { - m_transport = std::make_unique(remote); + m_transport = std::make_unique(*this, remote); SetSnaccROSETransport(m_transport.get()); } + // Clears invoke-context observations recorded by the test harness. + void ResetInvokeContextObservations() + { + m_inboundObservation.Reset(); + m_outboundTransportObservation.Reset(); + } + + // Returns inbound handler and response-send observations for this endpoint. + const InboundInvokeObservation& InboundObservation() const + { + return m_inboundObservation; + } + + // Returns outbound transport observations copied at send time. + const OutboundTransportObservation& OutboundTransportObservationState() const + { + return m_outboundTransportObservation; + } + + // Records the invoke context seen inside an inbound handler callback. + void RecordInboundHandlerContext(SnaccInvokeContext& ctx) + { + m_inboundObservation.CaptureHandlerContext(ctx); + } + + // Records an outbound transport send on this endpoint. + void RecordTransportSend(SnaccInvokeContext& ctx) + { + m_inboundObservation.CaptureTransportSend(ctx); + m_outboundTransportObservation.CaptureTransportSend(ctx); + } + // Selects the wire encoding used for all outbound traffic on this host. void SetEncoding(const TransportEncoding encoding) { @@ -319,6 +517,11 @@ class RuntimeEndpoint : public SnaccROSEBase return *m_transport; } + const LoopbackTransport& Transport() const + { + return *m_transport; + } + // Injects already encoded transport data directly into the inbound pipeline. void ReceiveRaw(const std::string& payload) { @@ -389,6 +592,8 @@ class RuntimeEndpoint : public SnaccROSEBase std::map m_modules; // registered modules keyed by generated interface id std::string m_strSessionId; // narrow session id used in test assertions and invoke payloads std::wstring m_wstrSessionId; // wide session id used by SnaccROSEBase result/error encoders + InboundInvokeObservation m_inboundObservation; // inbound handler and transport snapshots + OutboundTransportObservation m_outboundTransportObservation; // outbound transport snapshots }; // Runtime endpoint used by the test fixture when log assertions are required. @@ -552,8 +757,10 @@ class SettingsServiceModule : public ENetUC_Settings_ManagerROSEInterface, publi } // Implements the sample "get settings" invoke on the server side. - InvokeResult OnInvoke_asnGetSettings(AsnGetSettingsArgument* /* argument */, AsnGetSettingsResult* result, AsnRequestError* error, SnaccInvokeContext& /* ctx */) override + InvokeResult OnInvoke_asnGetSettings(AsnGetSettingsArgument* /* argument */, AsnGetSettingsResult* result, AsnRequestError* error, SnaccInvokeContext& ctx) override { + m_endpoint.RecordInboundHandlerContext(ctx); + if (!m_handlerModes.implementGetSettings) return InvokeResult::returnReject; if (m_handlerModes.getSettingsReturnsError) @@ -866,8 +1073,10 @@ class EventClientModule : public ENetUC_Event_ManagerROSEInterface, public IRunt // Applies the scripted transport behavior and either delivers, queues, drops, or // corrupts the outbound payload. -inline long LoopbackTransport::SendBinaryDataBlockEx(const char* lpBytes, size_t size, SnaccInvokeContext& /* ctx */) +inline long LoopbackTransport::SendBinaryDataBlockEx(const char* lpBytes, size_t size, SnaccInvokeContext& ctx) { + m_owner.RecordTransportSend(ctx); + TransportAction action; if (!m_actions.empty()) { @@ -947,6 +1156,8 @@ class RuntimeTestBase : public ::testing::Test m_clientSettingsModule.ResetState(); m_serverEventModule.ResetState(); m_clientEventModule.ResetState(); + m_server.ResetInvokeContextObservations(); + m_client.ResetInvokeContextObservations(); m_server.SetLogLevels(EAsnLogLevel::DISABLED, EAsnLogLevel::DISABLED); m_client.SetLogLevels(EAsnLogLevel::DISABLED, EAsnLogLevel::DISABLED); m_server.SetForwardLogsToBase(false); @@ -1047,6 +1258,18 @@ class RuntimeTestBase : public ::testing::Test return m_telemetryRecorder.Snapshot(); } + // Returns inbound invoke-context observations recorded on the server endpoint. + const InboundInvokeObservation& ServerInboundObservation() const + { + return m_server.InboundObservation(); + } + + // Returns outbound transport observations recorded on the client endpoint. + const OutboundTransportObservation& ClientOutboundTransportObservation() const + { + return m_client.OutboundTransportObservationState(); + } + // Gives tests a simple way to create a session-aware outbound context for the client. std::shared_ptr CreateClientInvokeContext(SNACC::ROSEInvoke* pInvoke, const char* szOperationName) { From a257333fd7a45369cc06c23b9f82fcd1328bf86c Mon Sep 17 00:00:00 2001 From: Jan Fellner Date: Mon, 6 Jul 2026 15:26:20 +0200 Subject: [PATCH 03/10] Defer CHOICE choiceId assignment until after successful decode. C++ codegen now sets choiceId only after the selected arm decodes successfully, so failed envelope decodes leave a cleared choice. Regenerate SNACCROSE from the updated generator. Co-authored-by: Cursor --- compiler/back-ends/c++-gen/gen-code.c | 15 ++++++---- cpp-lib/include/SNACCROSE.h | 2 +- cpp-lib/src/SNACCROSE.cpp | 42 +++++++++++++-------------- 3 files changed, 31 insertions(+), 28 deletions(-) diff --git a/compiler/back-ends/c++-gen/gen-code.c b/compiler/back-ends/c++-gen/gen-code.c index 52a7e50..b7d7c2b 100644 --- a/compiler/back-ends/c++-gen/gen-code.c +++ b/compiler/back-ends/c++-gen/gen-code.c @@ -2039,8 +2039,6 @@ void PrintChoiceDefCodeBerDecodeContent(FILE* src, FILE* hdr, Module* m, CxxRule } varName = cxxtri->fieldName; - /* set choice id for to this elment */ - fprintf(src, "\t\t\t%s = %s;\n", r->choiceIdFieldName, cxxtri->choiceIdSymbol); /* alloc elmt if nec */ if (cxxtri->isPtr) @@ -2079,6 +2077,8 @@ void PrintChoiceDefCodeBerDecodeContent(FILE* src, FILE* hdr, Module* m, CxxRule fprintf(src, "%s", getAccessor(cxxtri->isPtr)); fprintf(src, "B%s(_b, tag, elmtLen%d, bytesDecoded);\n", r->decodeContentBaseName, elmtLevel); } + /* set choice id only after the arm decoded successfully */ + fprintf(src, "\t\t\t%s = %s;\n", r->choiceIdFieldName, cxxtri->choiceIdSymbol); /* decode Eoc (s) */ for (i = elmtLevel - 1; i >= 0; i--) @@ -2096,10 +2096,10 @@ void PrintChoiceDefCodeBerDecodeContent(FILE* src, FILE* hdr, Module* m, CxxRule if (extensionsExist) { fprintf(src, "\t\t\tAsnAny extAny;\n"); - fprintf(src, "\t\t\textension = new AsnExtension;\n"); - fprintf(src, "\t\t\tchoiceId = extensionCid;\n"); fprintf(src, "\t\t\textAny.BDecContent(_b, tag, elmtLen0, bytesDecoded);\n"); + fprintf(src, "\t\t\textension = new AsnExtension;\n"); fprintf(src, "\t\t\textension->extList.insert( extension->extList.end(), extAny );\n"); + fprintf(src, "\t\t\t%s = extensionCid;\n", r->choiceIdFieldName); fprintf(src, "\t\t\tbreak;\n"); } else @@ -2279,16 +2279,19 @@ void PrintChoiceDefCodeJsonDec(FILE* src, FILE* hdr, Module* m, CxxRules* r, Typ fprintf(src, "\telse if (b.isMember(\"%s\"))\n", varName); fprintf(src, "\t{\n"); - fprintf(src, "\t\t%s = %s;\n", r->choiceIdFieldName, cxxtri->choiceIdSymbol); if (cxxtri->isPtr) { fprintf(src, "\t\tdelete %s;\n", varName); fprintf(src, "\t\t%s = new %s;\n", varName, cxxtri->className); fprintf(src, "\t\tif (!%s->JDec(b[\"%s\"]))\n", varName, varName); + fprintf(src, "\t\t\tthrow InvalidTagException(typeName(), \"decode failed: %s\", STACK_ENTRY);\n", varName); } else + { fprintf(src, "\t\tif (!%s.JDec(b[\"%s\"]))\n", varName, varName); - fprintf(src, "\t\t\tthrow InvalidTagException(typeName(), \"decode failed: %s\", STACK_ENTRY);\n", varName); + fprintf(src, "\t\t\tthrow InvalidTagException(typeName(), \"decode failed: %s\", STACK_ENTRY);\n", varName); + } + fprintf(src, "\t\t%s = %s;\n", r->choiceIdFieldName, cxxtri->choiceIdSymbol); fprintf(src, "\t}\n"); } diff --git a/cpp-lib/include/SNACCROSE.h b/cpp-lib/include/SNACCROSE.h index 0262932..675de62 100644 --- a/cpp-lib/include/SNACCROSE.h +++ b/cpp-lib/include/SNACCROSE.h @@ -2,7 +2,7 @@ // // SNACCROSE.h - class definitions for ASN.1 module SNACC-ROSE // -// This file was generated by estos esnacc (V7.0.2, 16.04.2026) +// This file was generated by estos esnacc (V7.0.5, 03.07.2026) // based on Coral WinSnacc written by Deepak Gupta // NOTE: This is a machine generated file - editing not recommended // diff --git a/cpp-lib/src/SNACCROSE.cpp b/cpp-lib/src/SNACCROSE.cpp index d4f08a9..490fbdf 100644 --- a/cpp-lib/src/SNACCROSE.cpp +++ b/cpp-lib/src/SNACCROSE.cpp @@ -2,7 +2,7 @@ // // SNACCROSE.cpp - class member functions for ASN.1 module SNACC-ROSE // -// This file was generated by estos esnacc (V7.0.2, 16.04.2026) +// This file was generated by estos esnacc (V7.0.5, 03.07.2026) // based on Coral WinSnacc written by Deepak Gupta // NOTE: This is a machine generated file - editing not recommended // @@ -179,14 +179,14 @@ void ROSERejectChoice::BDecContent(const AsnBuf &_b, AsnTag tag, AsnLen elmtLen0 switch (tag) { case MAKE_TAG_ID(UNIV, PRIM, INTEGER_TAG_CODE): - choiceId = invokedIDCid; invokedID = new AsnInt; invokedID->BDecContent(_b, tag, elmtLen0, bytesDecoded); + choiceId = invokedIDCid; break; case MAKE_TAG_ID(UNIV, PRIM, NULLTYPE_TAG_CODE): - choiceId = invokednullCid; invokednull = new AsnNull; invokednull->BDecContent(_b, tag, elmtLen0, bytesDecoded); + choiceId = invokednullCid; break; default: throw InvalidTagException(typeName(), tag, STACK_ENTRY); @@ -239,19 +239,19 @@ bool ROSERejectChoice::JDec(const SJson::Value& b){ if (b.isMember("invokedID")) { - choiceId = invokedIDCid; delete invokedID; invokedID = new AsnInt; if (!invokedID->JDec(b["invokedID"])) throw InvalidTagException(typeName(), "decode failed: invokedID", STACK_ENTRY); + choiceId = invokedIDCid; } else if (b.isMember("invokednull")) { - choiceId = invokednullCid; delete invokednull; invokednull = new AsnNull; if (!invokednull->JDec(b["invokednull"])) throw InvalidTagException(typeName(), "decode failed: invokednull", STACK_ENTRY); + choiceId = invokednullCid; } else throw InvalidTagException(typeName(), "no valid choice member", STACK_ENTRY); @@ -2758,24 +2758,24 @@ void RejectProblem::BDecContent(const AsnBuf &_b, AsnTag tag, AsnLen elmtLen0, A switch (tag) { case MAKE_TAG_ID(CNTX, PRIM, 0): - choiceId = generalProblemCid; generalProblem = new GeneralProblem; generalProblem->BDecContent(_b, tag, elmtLen0, bytesDecoded); + choiceId = generalProblemCid; break; case MAKE_TAG_ID(CNTX, PRIM, 1): - choiceId = invokeProblemCid; invokeProblem = new InvokeProblem; invokeProblem->BDecContent(_b, tag, elmtLen0, bytesDecoded); + choiceId = invokeProblemCid; break; case MAKE_TAG_ID(CNTX, PRIM, 2): - choiceId = returnResultProblemCid; returnResultProblem = new ReturnResultProblem; returnResultProblem->BDecContent(_b, tag, elmtLen0, bytesDecoded); + choiceId = returnResultProblemCid; break; case MAKE_TAG_ID(CNTX, PRIM, 3): - choiceId = returnErrorProblemCid; returnErrorProblem = new ReturnErrorProblem; returnErrorProblem->BDecContent(_b, tag, elmtLen0, bytesDecoded); + choiceId = returnErrorProblemCid; break; default: throw InvalidTagException(typeName(), tag, STACK_ENTRY); @@ -2834,35 +2834,35 @@ bool RejectProblem::JDec(const SJson::Value& b){ if (b.isMember("generalProblem")) { - choiceId = generalProblemCid; delete generalProblem; generalProblem = new GeneralProblem; if (!generalProblem->JDec(b["generalProblem"])) throw InvalidTagException(typeName(), "decode failed: generalProblem", STACK_ENTRY); + choiceId = generalProblemCid; } else if (b.isMember("invokeProblem")) { - choiceId = invokeProblemCid; delete invokeProblem; invokeProblem = new InvokeProblem; if (!invokeProblem->JDec(b["invokeProblem"])) throw InvalidTagException(typeName(), "decode failed: invokeProblem", STACK_ENTRY); + choiceId = invokeProblemCid; } else if (b.isMember("returnResultProblem")) { - choiceId = returnResultProblemCid; delete returnResultProblem; returnResultProblem = new ReturnResultProblem; if (!returnResultProblem->JDec(b["returnResultProblem"])) throw InvalidTagException(typeName(), "decode failed: returnResultProblem", STACK_ENTRY); + choiceId = returnResultProblemCid; } else if (b.isMember("returnErrorProblem")) { - choiceId = returnErrorProblemCid; delete returnErrorProblem; returnErrorProblem = new ReturnErrorProblem; if (!returnErrorProblem->JDec(b["returnErrorProblem"])) throw InvalidTagException(typeName(), "decode failed: returnErrorProblem", STACK_ENTRY); + choiceId = returnErrorProblemCid; } else throw InvalidTagException(typeName(), "no valid choice member", STACK_ENTRY); @@ -3581,24 +3581,24 @@ void ROSEMessage::BDecContent(const AsnBuf &_b, AsnTag tag, AsnLen elmtLen0, Asn switch (tag) { case MAKE_TAG_ID(CNTX, CONS, 1): - choiceId = invokeCid; invoke = new ROSEInvoke; invoke->BDecContent(_b, tag, elmtLen0, bytesDecoded); + choiceId = invokeCid; break; case MAKE_TAG_ID(CNTX, CONS, 2): - choiceId = resultCid; result = new ROSEResult; result->BDecContent(_b, tag, elmtLen0, bytesDecoded); + choiceId = resultCid; break; case MAKE_TAG_ID(CNTX, CONS, 3): - choiceId = errorCid; error = new ROSEError; error->BDecContent(_b, tag, elmtLen0, bytesDecoded); + choiceId = errorCid; break; case MAKE_TAG_ID(CNTX, CONS, 4): - choiceId = rejectCid; reject = new ROSEReject; reject->BDecContent(_b, tag, elmtLen0, bytesDecoded); + choiceId = rejectCid; break; default: throw InvalidTagException(typeName(), tag, STACK_ENTRY); @@ -3657,35 +3657,35 @@ bool ROSEMessage::JDec(const SJson::Value& b){ if (b.isMember("invoke")) { - choiceId = invokeCid; delete invoke; invoke = new ROSEInvoke; if (!invoke->JDec(b["invoke"])) throw InvalidTagException(typeName(), "decode failed: invoke", STACK_ENTRY); + choiceId = invokeCid; } else if (b.isMember("result")) { - choiceId = resultCid; delete result; result = new ROSEResult; if (!result->JDec(b["result"])) throw InvalidTagException(typeName(), "decode failed: result", STACK_ENTRY); + choiceId = resultCid; } else if (b.isMember("error")) { - choiceId = errorCid; delete error; error = new ROSEError; if (!error->JDec(b["error"])) throw InvalidTagException(typeName(), "decode failed: error", STACK_ENTRY); + choiceId = errorCid; } else if (b.isMember("reject")) { - choiceId = rejectCid; delete reject; reject = new ROSEReject; if (!reject->JDec(b["reject"])) throw InvalidTagException(typeName(), "decode failed: reject", STACK_ENTRY); + choiceId = rejectCid; } else throw InvalidTagException(typeName(), "no valid choice member", STACK_ENTRY); From d14a002c0e0e30794876fe73dfa1509552c870ae Mon Sep 17 00:00:00 2001 From: Jan Fellner Date: Mon, 6 Jul 2026 16:09:16 +0200 Subject: [PATCH 04/10] UCAAS-1446 Gate inbound decode rejects on successful ROSE envelope decode. Only emit mistypedArgument rejects after ROSEMessage decoding succeeds and a real invoke is available. Unparsable BER or JSON wire is logged and telemetried without sending an uncorrelated invokednull reject, matching common RPC practice for garbage framing. Move envelope decode and OnROSEMessage into the same guarded try on OnBinaryDataBlockResult so bRoseEnvelopeDecoded reflects actual decode state on both inbound paths. Expect ROSE_TE_TIMEOUT (not ROSE_TE_TRANSPORTFAILED) when a client invoke never receives a server response; transport failure remains for failed caller-side sends only. --- cpp-lib/src/SnaccROSEBase.cpp | 66 +- cpp-lib/tests/invoke_context_tests.cpp | 1240 ++++++++++++------------ 2 files changed, 637 insertions(+), 669 deletions(-) diff --git a/cpp-lib/src/SnaccROSEBase.cpp b/cpp-lib/src/SnaccROSEBase.cpp index 704e6a5..93eb925 100644 --- a/cpp-lib/src/SnaccROSEBase.cpp +++ b/cpp-lib/src/SnaccROSEBase.cpp @@ -499,9 +499,16 @@ bool SnaccROSEBase::OnBinaryDataBlockResult(const char* lpBytes, unsigned long l AsnBuf buffer((const char*)lpBytes, lSize); ROSEMessage* pmessage = new ROSEMessage; AsnLen bytesDecoded = 0; + bool bRoseEnvelopeDecoded = false; try { pmessage->BDec(buffer, bytesDecoded); + bRoseEnvelopeDecoded = true; + if (bLogTransportData) + LogTransportData(false, m_eTransportEncoding, nullptr, lpBytes, lSize, nullptr, nullptr); + + // pmessage will be deleted inside + bReturn = OnROSEMessage(pmessage, false, lSize); } catch (const SnaccException& ex) { @@ -520,7 +527,7 @@ bool SnaccROSEBase::OnBinaryDataBlockResult(const char* lpBytes, unsigned long l const char* szOperationName = nullptr; auto outcome = SnaccTelemetryData::Outcome::UNHANDLED; long lTelemetryResult = ROSE_RE_DECODE_FAILED; - if (pmessage->choiceId == ROSEMessage::invokeCid && pmessage->invoke) + if (bRoseEnvelopeDecoded && pmessage->choiceId == ROSEMessage::invokeCid && pmessage->invoke) { auto* pInvoke = pmessage->invoke; uiOperationID = pInvoke->operationID; @@ -554,12 +561,6 @@ bool SnaccROSEBase::OnBinaryDataBlockResult(const char* lpBytes, unsigned long l delete pmessage; return true; } - - if (bLogTransportData) - LogTransportData(false, m_eTransportEncoding, nullptr, lpBytes, lSize, nullptr, nullptr); - - // pmessage will be deleted inside - bReturn = OnROSEMessage(pmessage, false, lSize); break; } case SNACC::TransportEncoding::JSON: @@ -570,13 +571,19 @@ bool SnaccROSEBase::OnBinaryDataBlockResult(const char* lpBytes, unsigned long l SJson::Reader reader; if (reader.parse((const char*)lpBytes + iHeaderLen, (const char*)lpBytes + lSize, value)) { - bool bSuccess = false; ROSEMessage* pmessage = new ROSEMessage; + bool bRoseEnvelopeDecoded = false; try { - bSuccess = pmessage->JDec(value); - if (!bSuccess) + if (!pmessage->JDec(value)) throw InvalidTagException("ROSEMessage", "decode failed: ROSEMessage", STACK_ENTRY); + + bRoseEnvelopeDecoded = true; + if (bLogTransportData) + LogTransportData(false, m_eTransportEncoding, nullptr, lpBytes, lSize, pmessage, &value); + + // pmessage will be deleted inside + bReturn = OnROSEMessage(pmessage, false, lSize); } catch (const SnaccException& ex) { @@ -595,7 +602,7 @@ bool SnaccROSEBase::OnBinaryDataBlockResult(const char* lpBytes, unsigned long l const char* szOperationName = nullptr; auto outcome = SnaccTelemetryData::Outcome::UNHANDLED; long lTelemetryResult = ROSE_RE_DECODE_FAILED; - if (pmessage->choiceId == ROSEMessage::invokeCid && pmessage->invoke) + if (bRoseEnvelopeDecoded && pmessage->choiceId == ROSEMessage::invokeCid && pmessage->invoke) { auto* pInvoke = pmessage->invoke; uiOperationID = pInvoke->operationID; @@ -634,12 +641,6 @@ bool SnaccROSEBase::OnBinaryDataBlockResult(const char* lpBytes, unsigned long l delete pmessage; return true; } - - if (bLogTransportData) - LogTransportData(false, m_eTransportEncoding, nullptr, lpBytes, lSize, pmessage, &value); - - // pmessage will be deleted inside - bReturn = OnROSEMessage(pmessage, false, lSize); } else { @@ -792,9 +793,11 @@ void SnaccROSEBase::OnBinaryDataBlock(const char* lpBytes, unsigned long ulSize, AsnBuf buffer((const char*)lpBytes, ulSize); ROSEMessage* pmessage = new ROSEMessage; AsnLen bytesDecoded = 0; + bool bRoseEnvelopeDecoded = false; try { pmessage->BDec(buffer, bytesDecoded); + bRoseEnvelopeDecoded = true; if (bLogTransportData) LogTransportData(false, SNACC::TransportEncoding::BER, nullptr, lpBytes, ulSize, nullptr, nullptr); @@ -819,7 +822,7 @@ void SnaccROSEBase::OnBinaryDataBlock(const char* lpBytes, unsigned long ulSize, const char* szOperationName = nullptr; auto outcome = SnaccTelemetryData::Outcome::UNHANDLED; long lTelemetryResult = ROSE_RE_DECODE_FAILED; - if (pmessage->choiceId == ROSEMessage::invokeCid && pmessage->invoke) + if (bRoseEnvelopeDecoded && pmessage->choiceId == ROSEMessage::invokeCid && pmessage->invoke) { auto* pInvoke = pmessage->invoke; uiOperationID = pInvoke->operationID; @@ -870,10 +873,12 @@ void SnaccROSEBase::OnBinaryDataBlock(const char* lpBytes, unsigned long ulSize, if (reader.parse((const char*)lpBytes + iHeaderLen, (const char*)lpBytes + ulSize, value)) { ROSEMessage* pmessage = new ROSEMessage; + bool bRoseEnvelopeDecoded = false; try { if (!pmessage->JDec(value)) throw InvalidTagException("ROSEMessage", "decode failed: ROSEMessage", STACK_ENTRY); + bRoseEnvelopeDecoded = true; if (bLogTransportData) LogTransportData(false, m_eTransportEncoding, nullptr, lpBytes, ulSize, pmessage, &value); // pmessage will be deleted inside @@ -897,7 +902,7 @@ void SnaccROSEBase::OnBinaryDataBlock(const char* lpBytes, unsigned long ulSize, const char* szOperationName = nullptr; auto outcome = SnaccTelemetryData::Outcome::UNHANDLED; long lTelemetryResult = ROSE_RE_DECODE_FAILED; - if (pmessage->choiceId == ROSEMessage::invokeCid && pmessage->invoke) + if (bRoseEnvelopeDecoded && pmessage->choiceId == ROSEMessage::invokeCid && pmessage->invoke) { auto* pInvoke = pmessage->invoke; uiOperationID = pInvoke->operationID; @@ -948,28 +953,7 @@ void SnaccROSEBase::OnBinaryDataBlock(const char* lpBytes, unsigned long ulSize, PrintJSONToLog(false, true, nullptr, strError.c_str(), strError.length()); OnRoseDecodeError(bLogTransportData, m_eTransportEncoding, lpBytes, ulSize, reader.getFormattedErrorMessages()); - - ROSEReject reject; - reject.invokedID.choiceId = ROSERejectChoice::invokednullCid; - reject.invokedID.invokednull = new AsnNull; - - reject.reject = new RejectProblem; - reject.reject->choiceId = RejectProblem::invokeProblemCid; - reject.reject->invokeProblem = new InvokeProblem; - *reject.reject->invokeProblem = InvokeProblem::mistypedArgument; - reject.details = UTF8String::CreateNewFromASCII(strError.c_str()); - auto outcome = SnaccTelemetryData::Outcome::UNHANDLED; - long lTelemetryResult = ROSE_RE_DECODE_FAILED; - auto pRejectCtx = SnaccInvokeContext::Create(SnaccInvokeContextInit(SnaccInvokeDirection::INBOUND, nullptr, nullptr)); - const long lRejectResult = SendRejectEx(&reject, *pRejectCtx); - if (lRejectResult == ROSE_NOERROR) - { - outcome = SnaccTelemetryData::Outcome::REJECT; - lTelemetryResult = ROSE_REJECT_MISTYPEDARGUMENT; - } - else - lTelemetryResult = lRejectResult; - OnInvokeProcessed(SnaccTelemetryData::CreateFinalized(SnaccTelemetryData::Direction::INBOUND, 0, nullptr, ulSize, outcome, SnaccTelemetryData::Stage::INBOUND_DECODE, SnaccTelemetryData::Reason::DECODE_FAILED, lTelemetryResult)); + OnInvokeProcessed(SnaccTelemetryData::CreateFinalized(SnaccTelemetryData::Direction::INBOUND, 0, nullptr, ulSize, SnaccTelemetryData::Outcome::UNHANDLED, SnaccTelemetryData::Stage::INBOUND_DECODE, SnaccTelemetryData::Reason::DECODE_FAILED, ROSE_RE_DECODE_FAILED)); } break; } diff --git a/cpp-lib/tests/invoke_context_tests.cpp b/cpp-lib/tests/invoke_context_tests.cpp index ce3832c..e2e6301 100644 --- a/cpp-lib/tests/invoke_context_tests.cpp +++ b/cpp-lib/tests/invoke_context_tests.cpp @@ -5,714 +5,698 @@ namespace sample_runtime_tests { -namespace -{ -const SessionInvokeContext* AsSessionContext(const SnaccInvokeContext* pContext) -{ - return dynamic_cast(pContext); -} - -const SnaccTelemetryData* FindOutboundWaitTelemetry(const std::vector>& entries, const SnaccTelemetryData::Reason reason) -{ - const auto it = std::find_if(entries.begin(), entries.end(), [&](const std::shared_ptr& entry) { - return entry && entry->m_Direction == SnaccTelemetryData::Direction::OUTBOUND && entry->m_Stage == SnaccTelemetryData::Stage::OUTBOUND_WAIT && entry->m_Reason == reason; - }); - return it == entries.end() ? nullptr : it->get(); -} - -bool HasInboundResponseTelemetryForOperation(const std::vector>& entries, const unsigned int operationId) -{ - return std::any_of(entries.begin(), entries.end(), [&](const std::shared_ptr& entry) { - return entry && entry->m_Direction == SnaccTelemetryData::Direction::INBOUND && entry->m_Stage == SnaccTelemetryData::Stage::INBOUND_RESPONSE && - entry->m_uiOperationID == operationId; - }); -} -} // namespace - -// Specifies custom SnaccInvokeContext lifecycle for inbound and outbound ROSE paths. -class InvokeContextRuntimeTest : public RuntimeTestBase -{ -protected: - void AssertInboundHandlerReceivesCustomInvokeContext(const TransportEncoding encoding) + namespace { - InitializeEndpoints(encoding); - - AsnGetSettingsArgument argument; - AsnGetSettingsResult result; - AsnRequestError error; - - const long roseResult = m_clientSettingsModule.InvokeGetSettings(&argument, &result, &error); - EXPECT_EQ(ROSE_NOERROR, roseResult); - - const auto& handlerSnapshot = ServerInboundObservation().HandlerSnapshot(); - ASSERT_TRUE(handlerSnapshot.WasCaptured()); - EXPECT_TRUE(handlerSnapshot.IsSessionContext()) << "inbound handler must receive CreateInvokeContext() product"; - EXPECT_EQ("server-session", handlerSnapshot.LocalSessionId()); - EXPECT_EQ("client-session", handlerSnapshot.InvokeSessionId()); + const SessionInvokeContext* AsSessionContext(const SnaccInvokeContext* pContext) + { + return dynamic_cast(pContext); + } + + const SnaccTelemetryData* FindOutboundWaitTelemetry(const std::vector>& entries, const SnaccTelemetryData::Reason reason) + { + const auto it = std::find_if(entries.begin(), entries.end(), [&](const std::shared_ptr& entry) { return entry && entry->m_Direction == SnaccTelemetryData::Direction::OUTBOUND && entry->m_Stage == SnaccTelemetryData::Stage::OUTBOUND_WAIT && entry->m_Reason == reason; }); + return it == entries.end() ? nullptr : it->get(); + } + + bool HasInboundResponseTelemetryForOperation(const std::vector>& entries, const unsigned int operationId) + { + return std::any_of(entries.begin(), entries.end(), [&](const std::shared_ptr& entry) { return entry && entry->m_Direction == SnaccTelemetryData::Direction::INBOUND && entry->m_Stage == SnaccTelemetryData::Stage::INBOUND_RESPONSE && entry->m_uiOperationID == operationId; }); + } + } // namespace + + // Specifies custom SnaccInvokeContext lifecycle for inbound and outbound ROSE paths. + class InvokeContextRuntimeTest : public RuntimeTestBase + { + protected: + void AssertInboundHandlerReceivesCustomInvokeContext(const TransportEncoding encoding) + { + InitializeEndpoints(encoding); + + AsnGetSettingsArgument argument; + AsnGetSettingsResult result; + AsnRequestError error; + + const long roseResult = m_clientSettingsModule.InvokeGetSettings(&argument, &result, &error); + EXPECT_EQ(ROSE_NOERROR, roseResult); + + const auto& handlerSnapshot = ServerInboundObservation().HandlerSnapshot(); + ASSERT_TRUE(handlerSnapshot.WasCaptured()); + EXPECT_TRUE(handlerSnapshot.IsSessionContext()) << "inbound handler must receive CreateInvokeContext() product"; + EXPECT_EQ("server-session", handlerSnapshot.LocalSessionId()); + EXPECT_EQ("client-session", handlerSnapshot.InvokeSessionId()); + } + + void AssertInboundResponseSendReusesHandlerInvokeContext(const TransportEncoding encoding) + { + InitializeEndpoints(encoding); + + AsnGetSettingsArgument argument; + AsnGetSettingsResult result; + AsnRequestError error; + + const long roseResult = m_clientSettingsModule.InvokeGetSettings(&argument, &result, &error); + EXPECT_EQ(ROSE_NOERROR, roseResult); + + EXPECT_TRUE(ServerInboundObservation().HandlerWasInvoked()); + EXPECT_GT(ServerInboundObservation().TransportSendCount(), 0u); + EXPECT_TRUE(ServerInboundObservation().SameInstanceHandlerToTransport()) << "inbound response send must reuse the same invoke context instance seen in the handler"; + } + + void AssertInboundHandlerRejectReusesInvokeContext(const TransportEncoding encoding) + { + InitializeEndpoints(encoding); + HandlerModes handlerModes; + handlerModes.implementGetSettings = false; + ConfigureServerHandlers(handlerModes); + + AsnGetSettingsArgument argument; + AsnGetSettingsResult result; + AsnRequestError error; + + const long roseResult = m_clientSettingsModule.InvokeGetSettings(&argument, &result, &error); + EXPECT_EQ(ROSE_REJECT_FUNCTIONMISSING, roseResult); + + EXPECT_TRUE(ServerInboundObservation().HandlerWasInvoked()); + EXPECT_GT(ServerInboundObservation().TransportSendCount(), 0u); + EXPECT_TRUE(ServerInboundObservation().SameInstanceHandlerToTransport()) << "inbound reject send must reuse the same invoke context instance seen in the handler"; + } + + void AssertInboundApplicationErrorReusesInvokeContext(const TransportEncoding encoding) + { + InitializeEndpoints(encoding); + HandlerModes handlerModes; + handlerModes.getSettingsReturnsError = true; + ConfigureServerHandlers(handlerModes); + + AsnGetSettingsArgument argument; + AsnGetSettingsResult result; + AsnRequestError error; + + const long roseResult = m_clientSettingsModule.InvokeGetSettings(&argument, &result, &error); + EXPECT_EQ(ROSE_ERROR_VALUE, roseResult); + + EXPECT_TRUE(ServerInboundObservation().HandlerWasInvoked()); + EXPECT_GT(ServerInboundObservation().TransportSendCount(), 0u); + EXPECT_TRUE(ServerInboundObservation().SameInstanceHandlerToTransport()) << "inbound error response send must reuse the same invoke context instance seen in the handler"; + } + + void AssertInboundResponseSendFailureKeepsInvokeContextOnAttemptedSend(const TransportEncoding encoding) + { + InitializeEndpoints(encoding); + m_server.Transport().EnqueueAction(TransportAction::Fail()); + + AsnGetSettingsArgument argument; + AsnGetSettingsResult result; + AsnRequestError error; + + const long roseResult = m_clientSettingsModule.InvokeGetSettings(&argument, &result, &error, 250); + EXPECT_EQ(ROSE_TE_TIMEOUT, roseResult) << "client must time out when the inbound response never arrives; ROSE_TE_TRANSPORTFAILED applies only to a failed send on the caller side"; + + EXPECT_TRUE(ServerInboundObservation().HandlerWasInvoked()); + EXPECT_GT(ServerInboundObservation().TransportSendCount(), 0u); + EXPECT_TRUE(ServerInboundObservation().SameInstanceHandlerToTransport()) << "failed inbound response send must still pass the handler invoke context to transport"; + } + + void AssertUnknownInboundOperationRejectUsesCustomInvokeContext(const TransportEncoding encoding) + { + InitializeEndpoints(encoding); + + const long roseResult = m_clientSettingsModule.InvokeUnknownOperation(); + EXPECT_EQ(ROSE_REJECT_UNKNOWNOPERATION, roseResult); + EXPECT_FALSE(ServerInboundObservation().HandlerWasInvoked()) << "module handler must not run for unknown operations"; + + const auto& transportSnapshot = ServerInboundObservation().LastTransportSnapshot(); + ASSERT_TRUE(transportSnapshot.WasCaptured()); + EXPECT_TRUE(transportSnapshot.IsSessionContext()) << "inbound reject must still use CreateInvokeContext() product"; + EXPECT_EQ("server-session", transportSnapshot.LocalSessionId()); + } + + void AssertUnparsableInboundDoesNotReachHandler(const TransportEncoding encoding) + { + InitializeEndpoints(encoding); + m_server.ReceiveRaw(MalformedPayload(encoding)); + + EXPECT_FALSE(ServerInboundObservation().HandlerWasInvoked()); + auto count = ServerInboundObservation().TransportSendCount(); + EXPECT_EQ(0u, count); + } + + void AssertOutboundCallerContextReachesTransport(const TransportEncoding encoding) + { + InitializeEndpoints(encoding); + + AsnGetSettingsArgument argument; + AsnGetSettingsResult result; + AsnRequestError error; + SnaccScopedInvokeMessage invokeMsg(m_client.GetNextInvokeID(), OPID_asnGetSettings, &argument); + auto pContext = CreateClientInvokeContext(invokeMsg.GetPtr(), "asnGetSettings"); + pContext->SetTelemetryNote("outbound-request"); + + const long roseResult = m_clientSettingsModule.InvokeGetSettingsWithContext(&argument, &result, &error, pContext); + EXPECT_EQ(ROSE_NOERROR, roseResult); + + const auto& transportSnapshot = ClientOutboundTransportObservation().LastTransportSnapshot(); + ASSERT_TRUE(transportSnapshot.WasCaptured()); + EXPECT_TRUE(transportSnapshot.IsSessionContext()); + EXPECT_EQ("client-session", transportSnapshot.LocalSessionId()); + EXPECT_EQ("outbound-request", transportSnapshot.TelemetryNote()); + EXPECT_EQ(pContext.get(), ClientOutboundTransportObservation().LastContextAddress()); + } + + void AssertOutboundStubCreatedContextReachesTransport(const TransportEncoding encoding) + { + InitializeEndpoints(encoding); + + AsnGetSettingsArgument argument; + AsnGetSettingsResult result; + AsnRequestError error; + + const long roseResult = m_clientSettingsModule.InvokeGetSettings(&argument, &result, &error); + EXPECT_EQ(ROSE_NOERROR, roseResult); + + const auto& transportSnapshot = ClientOutboundTransportObservation().LastTransportSnapshot(); + ASSERT_TRUE(transportSnapshot.WasCaptured()); + EXPECT_TRUE(transportSnapshot.IsSessionContext()) << "outbound invoke without caller context must use CreateInvokeContext() product"; + EXPECT_EQ("client-session", transportSnapshot.LocalSessionId()); + EXPECT_EQ("client-session", transportSnapshot.InvokeSessionId()); + } + + void AssertOutboundCallerContextSurvivesUntilCallerScopeEnds(const TransportEncoding encoding) + { + InitializeEndpoints(encoding); + + AsnGetSettingsArgument argument; + AsnGetSettingsResult result; + AsnRequestError error; + SnaccScopedInvokeMessage invokeMsg(m_client.GetNextInvokeID(), OPID_asnGetSettings, &argument); + auto pContext = CreateClientInvokeContext(invokeMsg.GetPtr(), "asnGetSettings"); + pContext->SetTelemetryNote("caller-owned"); + + const long roseResult = m_clientSettingsModule.InvokeGetSettingsWithContext(&argument, &result, &error, pContext); + EXPECT_EQ(ROSE_NOERROR, roseResult); + EXPECT_FALSE(pContext->WasPreparedForTelemetry()); + EXPECT_EQ("caller-owned", pContext->TelemetryNote()); + + const SnaccTelemetryData* pTelemetry = FindOutboundWaitTelemetry(TelemetryEntries(), SnaccTelemetryData::Reason::REMOTE_RESULT); + ASSERT_NE(nullptr, pTelemetry); + ASSERT_TRUE(pTelemetry->m_pctx); + const auto* pSessionTelemetryContext = AsSessionContext(pTelemetry->m_pctx.get()); + ASSERT_NE(nullptr, pSessionTelemetryContext); + EXPECT_TRUE(pSessionTelemetryContext->WasPreparedForTelemetry()); + EXPECT_EQ("prepared:caller-owned", pSessionTelemetryContext->TelemetryNote()); + } + + void AssertOutboundEncodeFailureStillUsesCallerContext(const TransportEncoding encoding) + { + InitializeEndpoints(encoding); + + AsnSetSettingsArgument argument; + SetSettingsValues(argument.settings, true, std::string(10'000'000, 'x')); + AsnSetSettingsResult result; + AsnRequestError error; + SnaccScopedInvokeMessage invokeMsg(m_client.GetNextInvokeID(), OPID_asnSetSettings, &argument); + auto pContext = CreateClientInvokeContext(invokeMsg.GetPtr(), "asnSetSettings"); + pContext->SetTelemetryNote("encode-failure"); + + const long roseResult = m_client.SendInvoke(invokeMsg.GetPtr(), &result, &error, "asnSetSettings", 250, pContext); + EXPECT_EQ(ROSE_TE_ENCODE_FAILED, roseResult); + EXPECT_FALSE(pContext->WasPreparedForTelemetry()); + EXPECT_EQ("encode-failure", pContext->TelemetryNote()); + EXPECT_EQ(0u, ClientOutboundTransportObservation().TransportSendCount()) << "encode failure must occur before transport send"; + } + + void AssertNoTransportReturnsTransportFailedForInvoke(const TransportEncoding encoding) + { + ObservedRuntimeEndpoint endpoint{L"NoTransportEndpoint", "solo-session"}; + endpoint.SetEncoding(encoding); + SettingsServiceModule serviceModule(endpoint); + ENetUC_Settings_ManagerROSE component(&endpoint); + + AsnGetSettingsArgument argument; + AsnGetSettingsResult result; + AsnRequestError error; + SnaccScopedInvokeMessage invokeMsg(endpoint.GetNextInvokeID(), OPID_asnGetSettings, &argument); + auto pContext = endpoint.CreateSessionInvokeContext(invokeMsg.GetPtr(), "asnGetSettings"); + pContext->SetTelemetryNote("no-transport"); + + const long roseResult = component.Invoke_asnGetSettings(&argument, &result, &error, 250, pContext); + EXPECT_EQ(ROSE_TE_TRANSPORTFAILED, roseResult); + EXPECT_FALSE(pContext->WasPreparedForTelemetry()); + EXPECT_EQ("no-transport", pContext->TelemetryNote()); + } + + void AssertNoTransportReturnsTransportFailedForEvent(const TransportEncoding encoding) + { + ObservedRuntimeEndpoint endpoint{L"NoTransportEndpoint", "solo-session"}; + endpoint.SetEncoding(encoding); + SettingsServiceModule serviceModule(endpoint); + ENetUC_Settings_ManagerROSE component(&endpoint); + + AsnSettingsChangedArgument eventArgument; + SetSettingsValues(eventArgument.settings, true, "event-without-transport"); + auto pContext = endpoint.CreateSessionInvokeContext(nullptr, "asnSettingsChanged"); + pContext->SetTelemetryNote("event-no-transport"); + + const long roseResult = component.Event_asnSettingsChanged(&eventArgument, pContext); + EXPECT_EQ(ROSE_TE_TRANSPORTFAILED, roseResult); + EXPECT_FALSE(pContext->WasPreparedForTelemetry()); + EXPECT_EQ("event-no-transport", pContext->TelemetryNote()); + } + + void AssertOutboundTransportFailureKeepsCallerContextOnSendAttempt(const TransportEncoding encoding) + { + InitializeEndpoints(encoding); + m_client.Transport().EnqueueAction(TransportAction::Fail()); + + AsnGetSettingsArgument argument; + AsnGetSettingsResult result; + AsnRequestError error; + SnaccScopedInvokeMessage invokeMsg(m_client.GetNextInvokeID(), OPID_asnGetSettings, &argument); + auto pContext = CreateClientInvokeContext(invokeMsg.GetPtr(), "asnGetSettings"); + pContext->SetTelemetryNote("transport-failure"); + + const long roseResult = m_clientSettingsModule.InvokeGetSettingsWithContext(&argument, &result, &error, pContext, 250); + EXPECT_EQ(ROSE_TE_TRANSPORTFAILED, roseResult); + ASSERT_GT(ClientOutboundTransportObservation().TransportSendCount(), 0u); + EXPECT_EQ(pContext.get(), ClientOutboundTransportObservation().LastContextAddress()); + } + + void AssertOutboundTimeoutKeepsCallerContextOnRequestSend(const TransportEncoding encoding) + { + InitializeEndpoints(encoding); + m_server.Transport().EnqueueAction(TransportAction::Drop()); + + AsnGetSettingsArgument argument; + AsnGetSettingsResult result; + AsnRequestError error; + SnaccScopedInvokeMessage invokeMsg(m_client.GetNextInvokeID(), OPID_asnGetSettings, &argument); + auto pContext = CreateClientInvokeContext(invokeMsg.GetPtr(), "asnGetSettings"); + pContext->SetTelemetryNote("timeout"); + + const long roseResult = m_clientSettingsModule.InvokeGetSettingsWithContext(&argument, &result, &error, pContext, 25); + EXPECT_EQ(ROSE_TE_TIMEOUT, roseResult); + ASSERT_GT(ClientOutboundTransportObservation().TransportSendCount(), 0u); + EXPECT_EQ(pContext.get(), ClientOutboundTransportObservation().LastContextAddress()); + + const SnaccTelemetryData* pTelemetry = FindOutboundWaitTelemetry(TelemetryEntries(), SnaccTelemetryData::Reason::TIMEOUT); + ASSERT_NE(nullptr, pTelemetry); + ASSERT_TRUE(pTelemetry->m_pctx); + const auto* pSessionTelemetryContext = AsSessionContext(pTelemetry->m_pctx.get()); + ASSERT_NE(nullptr, pSessionTelemetryContext); + EXPECT_TRUE(pSessionTelemetryContext->WasPreparedForTelemetry()); + EXPECT_EQ("prepared:timeout", pSessionTelemetryContext->TelemetryNote()); + } + + void AssertOutboundRemoteRejectKeepsCallerContextOnRequestSend(const TransportEncoding encoding) + { + InitializeEndpoints(encoding); + HandlerModes handlerModes; + handlerModes.implementGetSettings = false; + ConfigureServerHandlers(handlerModes); + + AsnGetSettingsArgument argument; + AsnGetSettingsResult result; + AsnRequestError error; + SnaccScopedInvokeMessage invokeMsg(m_client.GetNextInvokeID(), OPID_asnGetSettings, &argument); + auto pContext = CreateClientInvokeContext(invokeMsg.GetPtr(), "asnGetSettings"); + pContext->SetTelemetryNote("remote-reject"); + + const long roseResult = m_clientSettingsModule.InvokeGetSettingsWithContext(&argument, &result, &error, pContext, 250); + EXPECT_EQ(ROSE_REJECT_FUNCTIONMISSING, roseResult); + ASSERT_GT(ClientOutboundTransportObservation().TransportSendCount(), 0u); + EXPECT_EQ(pContext.get(), ClientOutboundTransportObservation().LastContextAddress()); + EXPECT_FALSE(pContext->WasPreparedForTelemetry()); + EXPECT_EQ("remote-reject", pContext->TelemetryNote()); + } + + void AssertOutboundRemoteErrorKeepsCallerContextOnRequestSend(const TransportEncoding encoding) + { + InitializeEndpoints(encoding); + HandlerModes handlerModes; + handlerModes.getSettingsReturnsError = true; + ConfigureServerHandlers(handlerModes); + + AsnGetSettingsArgument argument; + AsnGetSettingsResult result; + AsnRequestError error; + SnaccScopedInvokeMessage invokeMsg(m_client.GetNextInvokeID(), OPID_asnGetSettings, &argument); + auto pContext = CreateClientInvokeContext(invokeMsg.GetPtr(), "asnGetSettings"); + pContext->SetTelemetryNote("remote-error"); + + const long roseResult = m_clientSettingsModule.InvokeGetSettingsWithContext(&argument, &result, &error, pContext, 250); + EXPECT_EQ(ROSE_ERROR_VALUE, roseResult); + ASSERT_GT(ClientOutboundTransportObservation().TransportSendCount(), 0u); + EXPECT_EQ(pContext.get(), ClientOutboundTransportObservation().LastContextAddress()); + EXPECT_FALSE(pContext->WasPreparedForTelemetry()); + EXPECT_EQ("remote-error", pContext->TelemetryNote()); + } + + void AssertOutboundMalformedResponseKeepsCallerContextThroughDecodeFailure(const TransportEncoding encoding) + { + InitializeEndpoints(encoding); + m_client.Transport().EnqueueAction(TransportAction::Queue()); + + AsnGetSettingsArgument argument; + AsnGetSettingsResult result; + AsnRequestError error; + SnaccScopedInvokeMessage invokeMsg(m_client.GetNextInvokeID(), OPID_asnGetSettings, &argument); + auto pContext = CreateClientInvokeContext(invokeMsg.GetPtr(), "asnGetSettings"); + pContext->SetTelemetryNote("decode-failure"); + + auto responseFuture = std::async(std::launch::async, [&]() { return m_client.SendInvoke(invokeMsg.GetPtr(), &result, &error, "asnGetSettings", 250, pContext); }); + + for (int i = 0; i < 20 && m_client.Transport().QueuedMessageCount() == 0; ++i) + std::this_thread::sleep_for(std::chrono::milliseconds(5)); + ASSERT_EQ(1U, m_client.Transport().QueuedMessageCount()); + m_client.Transport().TakeQueuedMessages(); + + std::string responsePayload; + AsnBool wrongPayload(true); + ASSERT_EQ(ROSE_NOERROR, m_server.EncodeResult(invokeMsg.GetPtr()->invokeID, &wrongPayload, responsePayload)); + m_client.ReceiveRaw(responsePayload); + + const long roseResult = responseFuture.get(); + EXPECT_EQ(ROSE_RE_DECODE_FAILED, roseResult); + ASSERT_GT(ClientOutboundTransportObservation().TransportSendCount(), 0u); + EXPECT_EQ(pContext.get(), ClientOutboundTransportObservation().LastContextAddress()); + EXPECT_FALSE(pContext->WasPreparedForTelemetry()); + EXPECT_EQ("decode-failure", pContext->TelemetryNote()); + } + + void AssertOrphanResultDoesNotBreakPendingInvoke(const TransportEncoding encoding) + { + InitializeEndpoints(encoding); + m_server.Transport().EnqueueAction(TransportAction::Queue()); + + AsnGetSettingsArgument argument; + AsnGetSettingsResult result; + AsnRequestError error; + SnaccScopedInvokeMessage invokeMsg(m_client.GetNextInvokeID(), OPID_asnGetSettings, &argument); + auto pContext = CreateClientInvokeContext(invokeMsg.GetPtr(), "asnGetSettings"); + pContext->SetTelemetryNote("pending-invoke"); + + auto pendingInvoke = std::async(std::launch::async, [&]() { return m_clientSettingsModule.InvokeGetSettingsWithContext(&argument, &result, &error, pContext, 500); }); + + for (int i = 0; i < 20 && m_server.Transport().QueuedMessageCount() == 0; ++i) + std::this_thread::sleep_for(std::chrono::milliseconds(5)); + ASSERT_EQ(1u, m_server.Transport().QueuedMessageCount()); + + AsnBool orphanPayload(true); + std::string orphanResponse; + ASSERT_EQ(ROSE_NOERROR, m_client.EncodeResult(4242, &orphanPayload, orphanResponse)); + m_client.ReceiveRaw(orphanResponse); + + FlushServerOutbound(); + EXPECT_EQ(ROSE_NOERROR, pendingInvoke.get()); + EXPECT_FALSE(pContext->WasPreparedForTelemetry()); + EXPECT_EQ("pending-invoke", pContext->TelemetryNote()); + } + + void AssertOrphanResultEmitsInboundResponseTelemetryWithoutPendingOperation(const TransportEncoding encoding) + { + InitializeEndpoints(encoding); + + AsnBool orphanPayload(true); + std::string orphanResponse; + ASSERT_EQ(ROSE_NOERROR, m_client.EncodeResult(4242, &orphanPayload, orphanResponse)); + m_client.ReceiveRaw(orphanResponse); + + EXPECT_TRUE(HasInboundResponseTelemetryForOperation(TelemetryEntries(), 0)); + } + + void AssertOrphanErrorDoesNotBreakPendingInvoke(const TransportEncoding encoding) + { + InitializeEndpoints(encoding); + m_server.Transport().EnqueueAction(TransportAction::Queue()); + + AsnGetSettingsArgument argument; + AsnGetSettingsResult result; + AsnRequestError error; + SnaccScopedInvokeMessage invokeMsg(m_client.GetNextInvokeID(), OPID_asnGetSettings, &argument); + auto pContext = CreateClientInvokeContext(invokeMsg.GetPtr(), "asnGetSettings"); + pContext->SetTelemetryNote("pending-error"); + + auto pendingInvoke = std::async(std::launch::async, [&]() { return m_clientSettingsModule.InvokeGetSettingsWithContext(&argument, &result, &error, pContext, 500); }); + + for (int i = 0; i < 20 && m_server.Transport().QueuedMessageCount() == 0; ++i) + std::this_thread::sleep_for(std::chrono::milliseconds(5)); + ASSERT_EQ(1u, m_server.Transport().QueuedMessageCount()); + + AsnRequestError orphanError; + orphanError.iErrorDetail = 4242; + orphanError.u8sErrorString.setASCII("orphan error"); + std::string orphanResponse; + ASSERT_EQ(ROSE_NOERROR, m_client.EncodeError(5252, &orphanError, orphanResponse)); + m_client.ReceiveRaw(orphanResponse); + + FlushServerOutbound(); + EXPECT_EQ(ROSE_NOERROR, pendingInvoke.get()); + EXPECT_FALSE(pContext->WasPreparedForTelemetry()); + EXPECT_EQ("pending-error", pContext->TelemetryNote()); + } + + void AssertOrphanRejectDoesNotBreakPendingInvoke(const TransportEncoding encoding) + { + InitializeEndpoints(encoding); + m_server.Transport().EnqueueAction(TransportAction::Queue()); + + AsnGetSettingsArgument argument; + AsnGetSettingsResult result; + AsnRequestError error; + SnaccScopedInvokeMessage invokeMsg(m_client.GetNextInvokeID(), OPID_asnGetSettings, &argument); + auto pContext = CreateClientInvokeContext(invokeMsg.GetPtr(), "asnGetSettings"); + pContext->SetTelemetryNote("pending-reject"); + + auto pendingInvoke = std::async(std::launch::async, [&]() { return m_clientSettingsModule.InvokeGetSettingsWithContext(&argument, &result, &error, pContext, 500); }); + + for (int i = 0; i < 20 && m_server.Transport().QueuedMessageCount() == 0; ++i) + std::this_thread::sleep_for(std::chrono::milliseconds(5)); + ASSERT_EQ(1u, m_server.Transport().QueuedMessageCount()); + + std::string orphanResponse; + ASSERT_EQ(ROSE_NOERROR, m_client.EncodeRejectInvoke(6262, InvokeProblem::unrecognisedOperation, orphanResponse, "orphan reject")); + m_client.ReceiveRaw(orphanResponse); + + FlushServerOutbound(); + EXPECT_EQ(ROSE_NOERROR, pendingInvoke.get()); + EXPECT_FALSE(pContext->WasPreparedForTelemetry()); + EXPECT_EQ("pending-reject", pContext->TelemetryNote()); + } + }; + + TEST_F(InvokeContextRuntimeTest, InboundHandlerReceivesCustomInvokeContextBer) + { + AssertInboundHandlerReceivesCustomInvokeContext(TransportEncoding::BER); } - void AssertInboundResponseSendReusesHandlerInvokeContext(const TransportEncoding encoding) + TEST_F(InvokeContextRuntimeTest, InboundHandlerReceivesCustomInvokeContextJson) { - InitializeEndpoints(encoding); - - AsnGetSettingsArgument argument; - AsnGetSettingsResult result; - AsnRequestError error; - - const long roseResult = m_clientSettingsModule.InvokeGetSettings(&argument, &result, &error); - EXPECT_EQ(ROSE_NOERROR, roseResult); - - EXPECT_TRUE(ServerInboundObservation().HandlerWasInvoked()); - EXPECT_GT(ServerInboundObservation().TransportSendCount(), 0u); - EXPECT_TRUE(ServerInboundObservation().SameInstanceHandlerToTransport()) - << "inbound response send must reuse the same invoke context instance seen in the handler"; + AssertInboundHandlerReceivesCustomInvokeContext(TransportEncoding::JSON); } - void AssertInboundHandlerRejectReusesInvokeContext(const TransportEncoding encoding) + TEST_F(InvokeContextRuntimeTest, InboundResponseSendReusesHandlerInvokeContextBer) { - InitializeEndpoints(encoding); - HandlerModes handlerModes; - handlerModes.implementGetSettings = false; - ConfigureServerHandlers(handlerModes); - - AsnGetSettingsArgument argument; - AsnGetSettingsResult result; - AsnRequestError error; - - const long roseResult = m_clientSettingsModule.InvokeGetSettings(&argument, &result, &error); - EXPECT_EQ(ROSE_REJECT_FUNCTIONMISSING, roseResult); - - EXPECT_TRUE(ServerInboundObservation().HandlerWasInvoked()); - EXPECT_GT(ServerInboundObservation().TransportSendCount(), 0u); - EXPECT_TRUE(ServerInboundObservation().SameInstanceHandlerToTransport()) - << "inbound reject send must reuse the same invoke context instance seen in the handler"; + AssertInboundResponseSendReusesHandlerInvokeContext(TransportEncoding::BER); } - void AssertInboundApplicationErrorReusesInvokeContext(const TransportEncoding encoding) + TEST_F(InvokeContextRuntimeTest, InboundResponseSendReusesHandlerInvokeContextJson) { - InitializeEndpoints(encoding); - HandlerModes handlerModes; - handlerModes.getSettingsReturnsError = true; - ConfigureServerHandlers(handlerModes); - - AsnGetSettingsArgument argument; - AsnGetSettingsResult result; - AsnRequestError error; - - const long roseResult = m_clientSettingsModule.InvokeGetSettings(&argument, &result, &error); - EXPECT_EQ(ROSE_ERROR_VALUE, roseResult); - - EXPECT_TRUE(ServerInboundObservation().HandlerWasInvoked()); - EXPECT_GT(ServerInboundObservation().TransportSendCount(), 0u); - EXPECT_TRUE(ServerInboundObservation().SameInstanceHandlerToTransport()) - << "inbound error response send must reuse the same invoke context instance seen in the handler"; + AssertInboundResponseSendReusesHandlerInvokeContext(TransportEncoding::JSON); } - void AssertInboundResponseSendFailureKeepsInvokeContextOnAttemptedSend(const TransportEncoding encoding) + TEST_F(InvokeContextRuntimeTest, InboundHandlerRejectReusesInvokeContextBer) { - InitializeEndpoints(encoding); - m_server.Transport().EnqueueAction(TransportAction::Fail()); - - AsnGetSettingsArgument argument; - AsnGetSettingsResult result; - AsnRequestError error; - - const long roseResult = m_clientSettingsModule.InvokeGetSettings(&argument, &result, &error, 250); - EXPECT_EQ(ROSE_TE_TRANSPORTFAILED, roseResult); - - EXPECT_TRUE(ServerInboundObservation().HandlerWasInvoked()); - EXPECT_GT(ServerInboundObservation().TransportSendCount(), 0u); - EXPECT_TRUE(ServerInboundObservation().SameInstanceHandlerToTransport()) - << "failed inbound response send must still pass the handler invoke context to transport"; + AssertInboundHandlerRejectReusesInvokeContext(TransportEncoding::BER); } - void AssertUnknownInboundOperationRejectUsesCustomInvokeContext(const TransportEncoding encoding) + TEST_F(InvokeContextRuntimeTest, InboundHandlerRejectReusesInvokeContextJson) { - InitializeEndpoints(encoding); - - const long roseResult = m_clientSettingsModule.InvokeUnknownOperation(); - EXPECT_EQ(ROSE_REJECT_UNKNOWNOPERATION, roseResult); - EXPECT_FALSE(ServerInboundObservation().HandlerWasInvoked()) << "module handler must not run for unknown operations"; - - const auto& transportSnapshot = ServerInboundObservation().LastTransportSnapshot(); - ASSERT_TRUE(transportSnapshot.WasCaptured()); - EXPECT_TRUE(transportSnapshot.IsSessionContext()) << "inbound reject must still use CreateInvokeContext() product"; - EXPECT_EQ("server-session", transportSnapshot.LocalSessionId()); + AssertInboundHandlerRejectReusesInvokeContext(TransportEncoding::JSON); } - void AssertUnparsableInboundDoesNotReachHandler(const TransportEncoding encoding) + TEST_F(InvokeContextRuntimeTest, InboundApplicationErrorReusesInvokeContextBer) { - InitializeEndpoints(encoding); - m_server.ReceiveRaw(MalformedPayload(encoding)); - - EXPECT_FALSE(ServerInboundObservation().HandlerWasInvoked()); - EXPECT_EQ(0u, ServerInboundObservation().TransportSendCount()); + AssertInboundApplicationErrorReusesInvokeContext(TransportEncoding::BER); } - void AssertOutboundCallerContextReachesTransport(const TransportEncoding encoding) + TEST_F(InvokeContextRuntimeTest, InboundApplicationErrorReusesInvokeContextJson) { - InitializeEndpoints(encoding); - - AsnGetSettingsArgument argument; - AsnGetSettingsResult result; - AsnRequestError error; - SnaccScopedInvokeMessage invokeMsg(m_client.GetNextInvokeID(), OPID_asnGetSettings, &argument); - auto pContext = CreateClientInvokeContext(invokeMsg.GetPtr(), "asnGetSettings"); - pContext->SetTelemetryNote("outbound-request"); - - const long roseResult = m_clientSettingsModule.InvokeGetSettingsWithContext(&argument, &result, &error, pContext); - EXPECT_EQ(ROSE_NOERROR, roseResult); - - const auto& transportSnapshot = ClientOutboundTransportObservation().LastTransportSnapshot(); - ASSERT_TRUE(transportSnapshot.WasCaptured()); - EXPECT_TRUE(transportSnapshot.IsSessionContext()); - EXPECT_EQ("client-session", transportSnapshot.LocalSessionId()); - EXPECT_EQ("outbound-request", transportSnapshot.TelemetryNote()); - EXPECT_EQ(pContext.get(), ClientOutboundTransportObservation().LastContextAddress()); + AssertInboundApplicationErrorReusesInvokeContext(TransportEncoding::JSON); } - void AssertOutboundStubCreatedContextReachesTransport(const TransportEncoding encoding) + TEST_F(InvokeContextRuntimeTest, InboundResponseSendFailureKeepsInvokeContextBer) { - InitializeEndpoints(encoding); - - AsnGetSettingsArgument argument; - AsnGetSettingsResult result; - AsnRequestError error; - - const long roseResult = m_clientSettingsModule.InvokeGetSettings(&argument, &result, &error); - EXPECT_EQ(ROSE_NOERROR, roseResult); - - const auto& transportSnapshot = ClientOutboundTransportObservation().LastTransportSnapshot(); - ASSERT_TRUE(transportSnapshot.WasCaptured()); - EXPECT_TRUE(transportSnapshot.IsSessionContext()) << "outbound invoke without caller context must use CreateInvokeContext() product"; - EXPECT_EQ("client-session", transportSnapshot.LocalSessionId()); - EXPECT_EQ("client-session", transportSnapshot.InvokeSessionId()); + AssertInboundResponseSendFailureKeepsInvokeContextOnAttemptedSend(TransportEncoding::BER); } - void AssertOutboundCallerContextSurvivesUntilCallerScopeEnds(const TransportEncoding encoding) + TEST_F(InvokeContextRuntimeTest, InboundResponseSendFailureKeepsInvokeContextJson) { - InitializeEndpoints(encoding); - - AsnGetSettingsArgument argument; - AsnGetSettingsResult result; - AsnRequestError error; - SnaccScopedInvokeMessage invokeMsg(m_client.GetNextInvokeID(), OPID_asnGetSettings, &argument); - auto pContext = CreateClientInvokeContext(invokeMsg.GetPtr(), "asnGetSettings"); - pContext->SetTelemetryNote("caller-owned"); - - const long roseResult = m_clientSettingsModule.InvokeGetSettingsWithContext(&argument, &result, &error, pContext); - EXPECT_EQ(ROSE_NOERROR, roseResult); - EXPECT_FALSE(pContext->WasPreparedForTelemetry()); - EXPECT_EQ("caller-owned", pContext->TelemetryNote()); - - const SnaccTelemetryData* pTelemetry = FindOutboundWaitTelemetry(TelemetryEntries(), SnaccTelemetryData::Reason::REMOTE_RESULT); - ASSERT_NE(nullptr, pTelemetry); - ASSERT_TRUE(pTelemetry->m_pctx); - const auto* pSessionTelemetryContext = AsSessionContext(pTelemetry->m_pctx.get()); - ASSERT_NE(nullptr, pSessionTelemetryContext); - EXPECT_TRUE(pSessionTelemetryContext->WasPreparedForTelemetry()); - EXPECT_EQ("prepared:caller-owned", pSessionTelemetryContext->TelemetryNote()); + AssertInboundResponseSendFailureKeepsInvokeContextOnAttemptedSend(TransportEncoding::JSON); } - void AssertOutboundEncodeFailureStillUsesCallerContext(const TransportEncoding encoding) + TEST_F(InvokeContextRuntimeTest, UnknownInboundOperationRejectUsesCustomInvokeContextBer) { - InitializeEndpoints(encoding); - - AsnSetSettingsArgument argument; - SetSettingsValues(argument.settings, true, std::string(10'000'000, 'x')); - AsnSetSettingsResult result; - AsnRequestError error; - SnaccScopedInvokeMessage invokeMsg(m_client.GetNextInvokeID(), OPID_asnSetSettings, &argument); - auto pContext = CreateClientInvokeContext(invokeMsg.GetPtr(), "asnSetSettings"); - pContext->SetTelemetryNote("encode-failure"); - - const long roseResult = m_client.SendInvoke(invokeMsg.GetPtr(), &result, &error, "asnSetSettings", 250, pContext); - EXPECT_EQ(ROSE_TE_ENCODE_FAILED, roseResult); - EXPECT_FALSE(pContext->WasPreparedForTelemetry()); - EXPECT_EQ("encode-failure", pContext->TelemetryNote()); - EXPECT_EQ(0u, ClientOutboundTransportObservation().TransportSendCount()) << "encode failure must occur before transport send"; + AssertUnknownInboundOperationRejectUsesCustomInvokeContext(TransportEncoding::BER); } - void AssertNoTransportReturnsTransportFailedForInvoke(const TransportEncoding encoding) + TEST_F(InvokeContextRuntimeTest, UnknownInboundOperationRejectUsesCustomInvokeContextJson) { - ObservedRuntimeEndpoint endpoint{L"NoTransportEndpoint", "solo-session"}; - endpoint.SetEncoding(encoding); - SettingsServiceModule serviceModule(endpoint); - ENetUC_Settings_ManagerROSE component(&endpoint); - - AsnGetSettingsArgument argument; - AsnGetSettingsResult result; - AsnRequestError error; - SnaccScopedInvokeMessage invokeMsg(endpoint.GetNextInvokeID(), OPID_asnGetSettings, &argument); - auto pContext = endpoint.CreateSessionInvokeContext(invokeMsg.GetPtr(), "asnGetSettings"); - pContext->SetTelemetryNote("no-transport"); - - const long roseResult = component.Invoke_asnGetSettings(&argument, &result, &error, 250, pContext); - EXPECT_EQ(ROSE_TE_TRANSPORTFAILED, roseResult); - EXPECT_FALSE(pContext->WasPreparedForTelemetry()); - EXPECT_EQ("no-transport", pContext->TelemetryNote()); + AssertUnknownInboundOperationRejectUsesCustomInvokeContext(TransportEncoding::JSON); } - void AssertNoTransportReturnsTransportFailedForEvent(const TransportEncoding encoding) + TEST_F(InvokeContextRuntimeTest, UnparsableInboundDoesNotReachHandlerBer) { - ObservedRuntimeEndpoint endpoint{L"NoTransportEndpoint", "solo-session"}; - endpoint.SetEncoding(encoding); - SettingsServiceModule serviceModule(endpoint); - ENetUC_Settings_ManagerROSE component(&endpoint); - - AsnSettingsChangedArgument eventArgument; - SetSettingsValues(eventArgument.settings, true, "event-without-transport"); - auto pContext = endpoint.CreateSessionInvokeContext(nullptr, "asnSettingsChanged"); - pContext->SetTelemetryNote("event-no-transport"); - - const long roseResult = component.Event_asnSettingsChanged(&eventArgument, pContext); - EXPECT_EQ(ROSE_TE_TRANSPORTFAILED, roseResult); - EXPECT_FALSE(pContext->WasPreparedForTelemetry()); - EXPECT_EQ("event-no-transport", pContext->TelemetryNote()); + AssertUnparsableInboundDoesNotReachHandler(TransportEncoding::BER); } - void AssertOutboundTransportFailureKeepsCallerContextOnSendAttempt(const TransportEncoding encoding) + TEST_F(InvokeContextRuntimeTest, UnparsableInboundDoesNotReachHandlerJson) { - InitializeEndpoints(encoding); - m_client.Transport().EnqueueAction(TransportAction::Fail()); - - AsnGetSettingsArgument argument; - AsnGetSettingsResult result; - AsnRequestError error; - SnaccScopedInvokeMessage invokeMsg(m_client.GetNextInvokeID(), OPID_asnGetSettings, &argument); - auto pContext = CreateClientInvokeContext(invokeMsg.GetPtr(), "asnGetSettings"); - pContext->SetTelemetryNote("transport-failure"); - - const long roseResult = m_clientSettingsModule.InvokeGetSettingsWithContext(&argument, &result, &error, pContext, 250); - EXPECT_EQ(ROSE_TE_TRANSPORTFAILED, roseResult); - ASSERT_GT(ClientOutboundTransportObservation().TransportSendCount(), 0u); - EXPECT_EQ(pContext.get(), ClientOutboundTransportObservation().LastContextAddress()); + AssertUnparsableInboundDoesNotReachHandler(TransportEncoding::JSON); } - void AssertOutboundTimeoutKeepsCallerContextOnRequestSend(const TransportEncoding encoding) - { - InitializeEndpoints(encoding); - m_server.Transport().EnqueueAction(TransportAction::Drop()); - - AsnGetSettingsArgument argument; - AsnGetSettingsResult result; - AsnRequestError error; - SnaccScopedInvokeMessage invokeMsg(m_client.GetNextInvokeID(), OPID_asnGetSettings, &argument); - auto pContext = CreateClientInvokeContext(invokeMsg.GetPtr(), "asnGetSettings"); - pContext->SetTelemetryNote("timeout"); - - const long roseResult = m_clientSettingsModule.InvokeGetSettingsWithContext(&argument, &result, &error, pContext, 25); - EXPECT_EQ(ROSE_TE_TIMEOUT, roseResult); - ASSERT_GT(ClientOutboundTransportObservation().TransportSendCount(), 0u); - EXPECT_EQ(pContext.get(), ClientOutboundTransportObservation().LastContextAddress()); - - const SnaccTelemetryData* pTelemetry = FindOutboundWaitTelemetry(TelemetryEntries(), SnaccTelemetryData::Reason::TIMEOUT); - ASSERT_NE(nullptr, pTelemetry); - ASSERT_TRUE(pTelemetry->m_pctx); - const auto* pSessionTelemetryContext = AsSessionContext(pTelemetry->m_pctx.get()); - ASSERT_NE(nullptr, pSessionTelemetryContext); - EXPECT_TRUE(pSessionTelemetryContext->WasPreparedForTelemetry()); - EXPECT_EQ("prepared:timeout", pSessionTelemetryContext->TelemetryNote()); - } - - void AssertOutboundRemoteRejectKeepsCallerContextOnRequestSend(const TransportEncoding encoding) - { - InitializeEndpoints(encoding); - HandlerModes handlerModes; - handlerModes.implementGetSettings = false; - ConfigureServerHandlers(handlerModes); - - AsnGetSettingsArgument argument; - AsnGetSettingsResult result; - AsnRequestError error; - SnaccScopedInvokeMessage invokeMsg(m_client.GetNextInvokeID(), OPID_asnGetSettings, &argument); - auto pContext = CreateClientInvokeContext(invokeMsg.GetPtr(), "asnGetSettings"); - pContext->SetTelemetryNote("remote-reject"); - - const long roseResult = m_clientSettingsModule.InvokeGetSettingsWithContext(&argument, &result, &error, pContext, 250); - EXPECT_EQ(ROSE_REJECT_FUNCTIONMISSING, roseResult); - ASSERT_GT(ClientOutboundTransportObservation().TransportSendCount(), 0u); - EXPECT_EQ(pContext.get(), ClientOutboundTransportObservation().LastContextAddress()); - EXPECT_FALSE(pContext->WasPreparedForTelemetry()); - EXPECT_EQ("remote-reject", pContext->TelemetryNote()); + TEST_F(InvokeContextRuntimeTest, OutboundCallerContextReachesTransportBer) + { + AssertOutboundCallerContextReachesTransport(TransportEncoding::BER); } - void AssertOutboundRemoteErrorKeepsCallerContextOnRequestSend(const TransportEncoding encoding) + TEST_F(InvokeContextRuntimeTest, OutboundCallerContextReachesTransportJson) { - InitializeEndpoints(encoding); - HandlerModes handlerModes; - handlerModes.getSettingsReturnsError = true; - ConfigureServerHandlers(handlerModes); - - AsnGetSettingsArgument argument; - AsnGetSettingsResult result; - AsnRequestError error; - SnaccScopedInvokeMessage invokeMsg(m_client.GetNextInvokeID(), OPID_asnGetSettings, &argument); - auto pContext = CreateClientInvokeContext(invokeMsg.GetPtr(), "asnGetSettings"); - pContext->SetTelemetryNote("remote-error"); - - const long roseResult = m_clientSettingsModule.InvokeGetSettingsWithContext(&argument, &result, &error, pContext, 250); - EXPECT_EQ(ROSE_ERROR_VALUE, roseResult); - ASSERT_GT(ClientOutboundTransportObservation().TransportSendCount(), 0u); - EXPECT_EQ(pContext.get(), ClientOutboundTransportObservation().LastContextAddress()); - EXPECT_FALSE(pContext->WasPreparedForTelemetry()); - EXPECT_EQ("remote-error", pContext->TelemetryNote()); + AssertOutboundCallerContextReachesTransport(TransportEncoding::JSON); } - void AssertOutboundMalformedResponseKeepsCallerContextThroughDecodeFailure(const TransportEncoding encoding) + TEST_F(InvokeContextRuntimeTest, OutboundStubCreatedContextReachesTransportBer) { - InitializeEndpoints(encoding); - m_client.Transport().EnqueueAction(TransportAction::Queue()); - - AsnGetSettingsArgument argument; - AsnGetSettingsResult result; - AsnRequestError error; - SnaccScopedInvokeMessage invokeMsg(m_client.GetNextInvokeID(), OPID_asnGetSettings, &argument); - auto pContext = CreateClientInvokeContext(invokeMsg.GetPtr(), "asnGetSettings"); - pContext->SetTelemetryNote("decode-failure"); - - auto responseFuture = std::async(std::launch::async, [&]() { - return m_client.SendInvoke(invokeMsg.GetPtr(), &result, &error, "asnGetSettings", 250, pContext); - }); - - for (int i = 0; i < 20 && m_client.Transport().QueuedMessageCount() == 0; ++i) - std::this_thread::sleep_for(std::chrono::milliseconds(5)); - ASSERT_EQ(1U, m_client.Transport().QueuedMessageCount()); - m_client.Transport().TakeQueuedMessages(); - - std::string responsePayload; - AsnBool wrongPayload(true); - ASSERT_EQ(ROSE_NOERROR, m_server.EncodeResult(invokeMsg.GetPtr()->invokeID, &wrongPayload, responsePayload)); - m_client.ReceiveRaw(responsePayload); - - const long roseResult = responseFuture.get(); - EXPECT_EQ(ROSE_RE_DECODE_FAILED, roseResult); - ASSERT_GT(ClientOutboundTransportObservation().TransportSendCount(), 0u); - EXPECT_EQ(pContext.get(), ClientOutboundTransportObservation().LastContextAddress()); - EXPECT_FALSE(pContext->WasPreparedForTelemetry()); - EXPECT_EQ("decode-failure", pContext->TelemetryNote()); - } - - void AssertOrphanResultDoesNotBreakPendingInvoke(const TransportEncoding encoding) - { - InitializeEndpoints(encoding); - m_server.Transport().EnqueueAction(TransportAction::Queue()); - - AsnGetSettingsArgument argument; - AsnGetSettingsResult result; - AsnRequestError error; - SnaccScopedInvokeMessage invokeMsg(m_client.GetNextInvokeID(), OPID_asnGetSettings, &argument); - auto pContext = CreateClientInvokeContext(invokeMsg.GetPtr(), "asnGetSettings"); - pContext->SetTelemetryNote("pending-invoke"); - - auto pendingInvoke = std::async(std::launch::async, [&]() { - return m_clientSettingsModule.InvokeGetSettingsWithContext(&argument, &result, &error, pContext, 500); - }); - - for (int i = 0; i < 20 && m_server.Transport().QueuedMessageCount() == 0; ++i) - std::this_thread::sleep_for(std::chrono::milliseconds(5)); - ASSERT_EQ(1u, m_server.Transport().QueuedMessageCount()); - - AsnBool orphanPayload(true); - std::string orphanResponse; - ASSERT_EQ(ROSE_NOERROR, m_client.EncodeResult(4242, &orphanPayload, orphanResponse)); - m_client.ReceiveRaw(orphanResponse); - - FlushServerOutbound(); - EXPECT_EQ(ROSE_NOERROR, pendingInvoke.get()); - EXPECT_FALSE(pContext->WasPreparedForTelemetry()); - EXPECT_EQ("pending-invoke", pContext->TelemetryNote()); + AssertOutboundStubCreatedContextReachesTransport(TransportEncoding::BER); } - void AssertOrphanResultEmitsInboundResponseTelemetryWithoutPendingOperation(const TransportEncoding encoding) + TEST_F(InvokeContextRuntimeTest, OutboundStubCreatedContextReachesTransportJson) { - InitializeEndpoints(encoding); - - AsnBool orphanPayload(true); - std::string orphanResponse; - ASSERT_EQ(ROSE_NOERROR, m_client.EncodeResult(4242, &orphanPayload, orphanResponse)); - m_client.ReceiveRaw(orphanResponse); - - EXPECT_TRUE(HasInboundResponseTelemetryForOperation(TelemetryEntries(), 0)); + AssertOutboundStubCreatedContextReachesTransport(TransportEncoding::JSON); } - void AssertOrphanErrorDoesNotBreakPendingInvoke(const TransportEncoding encoding) + TEST_F(InvokeContextRuntimeTest, OutboundCallerContextSurvivesUntilCallerScopeEndsBer) { - InitializeEndpoints(encoding); - m_server.Transport().EnqueueAction(TransportAction::Queue()); - - AsnGetSettingsArgument argument; - AsnGetSettingsResult result; - AsnRequestError error; - SnaccScopedInvokeMessage invokeMsg(m_client.GetNextInvokeID(), OPID_asnGetSettings, &argument); - auto pContext = CreateClientInvokeContext(invokeMsg.GetPtr(), "asnGetSettings"); - pContext->SetTelemetryNote("pending-error"); - - auto pendingInvoke = std::async(std::launch::async, [&]() { - return m_clientSettingsModule.InvokeGetSettingsWithContext(&argument, &result, &error, pContext, 500); - }); - - for (int i = 0; i < 20 && m_server.Transport().QueuedMessageCount() == 0; ++i) - std::this_thread::sleep_for(std::chrono::milliseconds(5)); - ASSERT_EQ(1u, m_server.Transport().QueuedMessageCount()); - - AsnRequestError orphanError; - orphanError.iErrorDetail = 4242; - orphanError.u8sErrorString.setASCII("orphan error"); - std::string orphanResponse; - ASSERT_EQ(ROSE_NOERROR, m_client.EncodeError(5252, &orphanError, orphanResponse)); - m_client.ReceiveRaw(orphanResponse); - - FlushServerOutbound(); - EXPECT_EQ(ROSE_NOERROR, pendingInvoke.get()); - EXPECT_FALSE(pContext->WasPreparedForTelemetry()); - EXPECT_EQ("pending-error", pContext->TelemetryNote()); + AssertOutboundCallerContextSurvivesUntilCallerScopeEnds(TransportEncoding::BER); } - void AssertOrphanRejectDoesNotBreakPendingInvoke(const TransportEncoding encoding) + TEST_F(InvokeContextRuntimeTest, OutboundCallerContextSurvivesUntilCallerScopeEndsJson) { - InitializeEndpoints(encoding); - m_server.Transport().EnqueueAction(TransportAction::Queue()); - - AsnGetSettingsArgument argument; - AsnGetSettingsResult result; - AsnRequestError error; - SnaccScopedInvokeMessage invokeMsg(m_client.GetNextInvokeID(), OPID_asnGetSettings, &argument); - auto pContext = CreateClientInvokeContext(invokeMsg.GetPtr(), "asnGetSettings"); - pContext->SetTelemetryNote("pending-reject"); - - auto pendingInvoke = std::async(std::launch::async, [&]() { - return m_clientSettingsModule.InvokeGetSettingsWithContext(&argument, &result, &error, pContext, 500); - }); - - for (int i = 0; i < 20 && m_server.Transport().QueuedMessageCount() == 0; ++i) - std::this_thread::sleep_for(std::chrono::milliseconds(5)); - ASSERT_EQ(1u, m_server.Transport().QueuedMessageCount()); - - std::string orphanResponse; - ASSERT_EQ(ROSE_NOERROR, m_client.EncodeRejectInvoke(6262, InvokeProblem::unrecognisedOperation, orphanResponse, "orphan reject")); - m_client.ReceiveRaw(orphanResponse); - - FlushServerOutbound(); - EXPECT_EQ(ROSE_NOERROR, pendingInvoke.get()); - EXPECT_FALSE(pContext->WasPreparedForTelemetry()); - EXPECT_EQ("pending-reject", pContext->TelemetryNote()); + AssertOutboundCallerContextSurvivesUntilCallerScopeEnds(TransportEncoding::JSON); } -}; - -TEST_F(InvokeContextRuntimeTest, InboundHandlerReceivesCustomInvokeContextBer) -{ - AssertInboundHandlerReceivesCustomInvokeContext(TransportEncoding::BER); -} - -TEST_F(InvokeContextRuntimeTest, InboundHandlerReceivesCustomInvokeContextJson) -{ - AssertInboundHandlerReceivesCustomInvokeContext(TransportEncoding::JSON); -} - -TEST_F(InvokeContextRuntimeTest, InboundResponseSendReusesHandlerInvokeContextBer) -{ - AssertInboundResponseSendReusesHandlerInvokeContext(TransportEncoding::BER); -} - -TEST_F(InvokeContextRuntimeTest, InboundResponseSendReusesHandlerInvokeContextJson) -{ - AssertInboundResponseSendReusesHandlerInvokeContext(TransportEncoding::JSON); -} - -TEST_F(InvokeContextRuntimeTest, InboundHandlerRejectReusesInvokeContextBer) -{ - AssertInboundHandlerRejectReusesInvokeContext(TransportEncoding::BER); -} - -TEST_F(InvokeContextRuntimeTest, InboundHandlerRejectReusesInvokeContextJson) -{ - AssertInboundHandlerRejectReusesInvokeContext(TransportEncoding::JSON); -} - -TEST_F(InvokeContextRuntimeTest, InboundApplicationErrorReusesInvokeContextBer) -{ - AssertInboundApplicationErrorReusesInvokeContext(TransportEncoding::BER); -} - -TEST_F(InvokeContextRuntimeTest, InboundApplicationErrorReusesInvokeContextJson) -{ - AssertInboundApplicationErrorReusesInvokeContext(TransportEncoding::JSON); -} - -TEST_F(InvokeContextRuntimeTest, InboundResponseSendFailureKeepsInvokeContextBer) -{ - AssertInboundResponseSendFailureKeepsInvokeContextOnAttemptedSend(TransportEncoding::BER); -} - -TEST_F(InvokeContextRuntimeTest, InboundResponseSendFailureKeepsInvokeContextJson) -{ - AssertInboundResponseSendFailureKeepsInvokeContextOnAttemptedSend(TransportEncoding::JSON); -} -TEST_F(InvokeContextRuntimeTest, UnknownInboundOperationRejectUsesCustomInvokeContextBer) -{ - AssertUnknownInboundOperationRejectUsesCustomInvokeContext(TransportEncoding::BER); -} - -TEST_F(InvokeContextRuntimeTest, UnknownInboundOperationRejectUsesCustomInvokeContextJson) -{ - AssertUnknownInboundOperationRejectUsesCustomInvokeContext(TransportEncoding::JSON); -} - -TEST_F(InvokeContextRuntimeTest, UnparsableInboundDoesNotReachHandlerBer) -{ - AssertUnparsableInboundDoesNotReachHandler(TransportEncoding::BER); -} - -TEST_F(InvokeContextRuntimeTest, UnparsableInboundDoesNotReachHandlerJson) -{ - AssertUnparsableInboundDoesNotReachHandler(TransportEncoding::JSON); -} - -TEST_F(InvokeContextRuntimeTest, OutboundCallerContextReachesTransportBer) -{ - AssertOutboundCallerContextReachesTransport(TransportEncoding::BER); -} - -TEST_F(InvokeContextRuntimeTest, OutboundCallerContextReachesTransportJson) -{ - AssertOutboundCallerContextReachesTransport(TransportEncoding::JSON); -} - -TEST_F(InvokeContextRuntimeTest, OutboundStubCreatedContextReachesTransportBer) -{ - AssertOutboundStubCreatedContextReachesTransport(TransportEncoding::BER); -} - -TEST_F(InvokeContextRuntimeTest, OutboundStubCreatedContextReachesTransportJson) -{ - AssertOutboundStubCreatedContextReachesTransport(TransportEncoding::JSON); -} - -TEST_F(InvokeContextRuntimeTest, OutboundCallerContextSurvivesUntilCallerScopeEndsBer) -{ - AssertOutboundCallerContextSurvivesUntilCallerScopeEnds(TransportEncoding::BER); -} - -TEST_F(InvokeContextRuntimeTest, OutboundCallerContextSurvivesUntilCallerScopeEndsJson) -{ - AssertOutboundCallerContextSurvivesUntilCallerScopeEnds(TransportEncoding::JSON); -} - -TEST_F(InvokeContextRuntimeTest, OutboundEncodeFailureKeepsCallerContextJson) -{ - AssertOutboundEncodeFailureStillUsesCallerContext(TransportEncoding::JSON); -} + TEST_F(InvokeContextRuntimeTest, OutboundEncodeFailureKeepsCallerContextJson) + { + AssertOutboundEncodeFailureStillUsesCallerContext(TransportEncoding::JSON); + } -TEST_F(InvokeContextRuntimeTest, NoTransportReturnsTransportFailedForInvokeBer) -{ - AssertNoTransportReturnsTransportFailedForInvoke(TransportEncoding::BER); -} + TEST_F(InvokeContextRuntimeTest, NoTransportReturnsTransportFailedForInvokeBer) + { + AssertNoTransportReturnsTransportFailedForInvoke(TransportEncoding::BER); + } -TEST_F(InvokeContextRuntimeTest, NoTransportReturnsTransportFailedForInvokeJson) -{ - AssertNoTransportReturnsTransportFailedForInvoke(TransportEncoding::JSON); -} + TEST_F(InvokeContextRuntimeTest, NoTransportReturnsTransportFailedForInvokeJson) + { + AssertNoTransportReturnsTransportFailedForInvoke(TransportEncoding::JSON); + } -TEST_F(InvokeContextRuntimeTest, NoTransportReturnsTransportFailedForEventBer) -{ - AssertNoTransportReturnsTransportFailedForEvent(TransportEncoding::BER); -} + TEST_F(InvokeContextRuntimeTest, NoTransportReturnsTransportFailedForEventBer) + { + AssertNoTransportReturnsTransportFailedForEvent(TransportEncoding::BER); + } -TEST_F(InvokeContextRuntimeTest, NoTransportReturnsTransportFailedForEventJson) -{ - AssertNoTransportReturnsTransportFailedForEvent(TransportEncoding::JSON); -} + TEST_F(InvokeContextRuntimeTest, NoTransportReturnsTransportFailedForEventJson) + { + AssertNoTransportReturnsTransportFailedForEvent(TransportEncoding::JSON); + } -TEST_F(InvokeContextRuntimeTest, OutboundTransportFailureKeepsCallerContextBer) -{ - AssertOutboundTransportFailureKeepsCallerContextOnSendAttempt(TransportEncoding::BER); -} + TEST_F(InvokeContextRuntimeTest, OutboundTransportFailureKeepsCallerContextBer) + { + AssertOutboundTransportFailureKeepsCallerContextOnSendAttempt(TransportEncoding::BER); + } -TEST_F(InvokeContextRuntimeTest, OutboundTransportFailureKeepsCallerContextJson) -{ - AssertOutboundTransportFailureKeepsCallerContextOnSendAttempt(TransportEncoding::JSON); -} + TEST_F(InvokeContextRuntimeTest, OutboundTransportFailureKeepsCallerContextJson) + { + AssertOutboundTransportFailureKeepsCallerContextOnSendAttempt(TransportEncoding::JSON); + } -TEST_F(InvokeContextRuntimeTest, OutboundTimeoutKeepsCallerContextBer) -{ - AssertOutboundTimeoutKeepsCallerContextOnRequestSend(TransportEncoding::BER); -} + TEST_F(InvokeContextRuntimeTest, OutboundTimeoutKeepsCallerContextBer) + { + AssertOutboundTimeoutKeepsCallerContextOnRequestSend(TransportEncoding::BER); + } -TEST_F(InvokeContextRuntimeTest, OutboundTimeoutKeepsCallerContextJson) -{ - AssertOutboundTimeoutKeepsCallerContextOnRequestSend(TransportEncoding::JSON); -} + TEST_F(InvokeContextRuntimeTest, OutboundTimeoutKeepsCallerContextJson) + { + AssertOutboundTimeoutKeepsCallerContextOnRequestSend(TransportEncoding::JSON); + } -TEST_F(InvokeContextRuntimeTest, OutboundRemoteRejectKeepsCallerContextBer) -{ - AssertOutboundRemoteRejectKeepsCallerContextOnRequestSend(TransportEncoding::BER); -} + TEST_F(InvokeContextRuntimeTest, OutboundRemoteRejectKeepsCallerContextBer) + { + AssertOutboundRemoteRejectKeepsCallerContextOnRequestSend(TransportEncoding::BER); + } -TEST_F(InvokeContextRuntimeTest, OutboundRemoteRejectKeepsCallerContextJson) -{ - AssertOutboundRemoteRejectKeepsCallerContextOnRequestSend(TransportEncoding::JSON); -} + TEST_F(InvokeContextRuntimeTest, OutboundRemoteRejectKeepsCallerContextJson) + { + AssertOutboundRemoteRejectKeepsCallerContextOnRequestSend(TransportEncoding::JSON); + } -TEST_F(InvokeContextRuntimeTest, OutboundRemoteErrorKeepsCallerContextBer) -{ - AssertOutboundRemoteErrorKeepsCallerContextOnRequestSend(TransportEncoding::BER); -} + TEST_F(InvokeContextRuntimeTest, OutboundRemoteErrorKeepsCallerContextBer) + { + AssertOutboundRemoteErrorKeepsCallerContextOnRequestSend(TransportEncoding::BER); + } -TEST_F(InvokeContextRuntimeTest, OutboundRemoteErrorKeepsCallerContextJson) -{ - AssertOutboundRemoteErrorKeepsCallerContextOnRequestSend(TransportEncoding::JSON); -} + TEST_F(InvokeContextRuntimeTest, OutboundRemoteErrorKeepsCallerContextJson) + { + AssertOutboundRemoteErrorKeepsCallerContextOnRequestSend(TransportEncoding::JSON); + } -TEST_F(InvokeContextRuntimeTest, OutboundMalformedResponseKeepsCallerContextBer) -{ - AssertOutboundMalformedResponseKeepsCallerContextThroughDecodeFailure(TransportEncoding::BER); -} + TEST_F(InvokeContextRuntimeTest, OutboundMalformedResponseKeepsCallerContextBer) + { + AssertOutboundMalformedResponseKeepsCallerContextThroughDecodeFailure(TransportEncoding::BER); + } -TEST_F(InvokeContextRuntimeTest, OutboundMalformedResponseKeepsCallerContextJson) -{ - AssertOutboundMalformedResponseKeepsCallerContextThroughDecodeFailure(TransportEncoding::JSON); -} + TEST_F(InvokeContextRuntimeTest, OutboundMalformedResponseKeepsCallerContextJson) + { + AssertOutboundMalformedResponseKeepsCallerContextThroughDecodeFailure(TransportEncoding::JSON); + } -TEST_F(InvokeContextRuntimeTest, OrphanResultDoesNotBreakPendingInvokeBer) -{ - AssertOrphanResultDoesNotBreakPendingInvoke(TransportEncoding::BER); -} + TEST_F(InvokeContextRuntimeTest, OrphanResultDoesNotBreakPendingInvokeBer) + { + AssertOrphanResultDoesNotBreakPendingInvoke(TransportEncoding::BER); + } -TEST_F(InvokeContextRuntimeTest, OrphanResultDoesNotBreakPendingInvokeJson) -{ - AssertOrphanResultDoesNotBreakPendingInvoke(TransportEncoding::JSON); -} + TEST_F(InvokeContextRuntimeTest, OrphanResultDoesNotBreakPendingInvokeJson) + { + AssertOrphanResultDoesNotBreakPendingInvoke(TransportEncoding::JSON); + } -TEST_F(InvokeContextRuntimeTest, OrphanResultEmitsInboundResponseTelemetryBer) -{ - AssertOrphanResultEmitsInboundResponseTelemetryWithoutPendingOperation(TransportEncoding::BER); -} + TEST_F(InvokeContextRuntimeTest, OrphanResultEmitsInboundResponseTelemetryBer) + { + AssertOrphanResultEmitsInboundResponseTelemetryWithoutPendingOperation(TransportEncoding::BER); + } -TEST_F(InvokeContextRuntimeTest, OrphanResultEmitsInboundResponseTelemetryJson) -{ - AssertOrphanResultEmitsInboundResponseTelemetryWithoutPendingOperation(TransportEncoding::JSON); -} + TEST_F(InvokeContextRuntimeTest, OrphanResultEmitsInboundResponseTelemetryJson) + { + AssertOrphanResultEmitsInboundResponseTelemetryWithoutPendingOperation(TransportEncoding::JSON); + } -TEST_F(InvokeContextRuntimeTest, OrphanErrorDoesNotBreakPendingInvokeBer) -{ - AssertOrphanErrorDoesNotBreakPendingInvoke(TransportEncoding::BER); -} + TEST_F(InvokeContextRuntimeTest, OrphanErrorDoesNotBreakPendingInvokeBer) + { + AssertOrphanErrorDoesNotBreakPendingInvoke(TransportEncoding::BER); + } -TEST_F(InvokeContextRuntimeTest, OrphanErrorDoesNotBreakPendingInvokeJson) -{ - AssertOrphanErrorDoesNotBreakPendingInvoke(TransportEncoding::JSON); -} + TEST_F(InvokeContextRuntimeTest, OrphanErrorDoesNotBreakPendingInvokeJson) + { + AssertOrphanErrorDoesNotBreakPendingInvoke(TransportEncoding::JSON); + } -TEST_F(InvokeContextRuntimeTest, OrphanRejectDoesNotBreakPendingInvokeBer) -{ - AssertOrphanRejectDoesNotBreakPendingInvoke(TransportEncoding::BER); -} + TEST_F(InvokeContextRuntimeTest, OrphanRejectDoesNotBreakPendingInvokeBer) + { + AssertOrphanRejectDoesNotBreakPendingInvoke(TransportEncoding::BER); + } -TEST_F(InvokeContextRuntimeTest, OrphanRejectDoesNotBreakPendingInvokeJson) -{ - AssertOrphanRejectDoesNotBreakPendingInvoke(TransportEncoding::JSON); -} + TEST_F(InvokeContextRuntimeTest, OrphanRejectDoesNotBreakPendingInvokeJson) + { + AssertOrphanRejectDoesNotBreakPendingInvoke(TransportEncoding::JSON); + } } // namespace sample_runtime_tests From 5ebab97771b25a06ea516fd8e4d79c9eb3e0680a Mon Sep 17 00:00:00 2001 From: Jan Fellner Date: Tue, 7 Jul 2026 09:22:24 +0200 Subject: [PATCH 05/10] UCAAS-1446 Adopt unique_ptr ownership for inbound ROSE dispatch. Move ROSEMessage ownership through the decode and dispatch path with unique_ptr, snapshot invoke fields for decode-failure rejects, and align the C++ generator and runtime harness with the updated OnInvoke and sub-message APIs. Co-authored-by: Cursor --- compiler/back-ends/c++-gen/gen-code.c | 18 +- cpp-lib/include/SnaccROSEBase.h | 34 +- cpp-lib/include/SnaccROSEInterfaces.h | 6 +- cpp-lib/src/SnaccROSEBase.cpp | 516 +++++++----------- cpp-lib/tests/runtime_correctness_notes.md | 185 ++++--- .../test_support/sample_runtime_harness.h | 12 +- 6 files changed, 368 insertions(+), 403 deletions(-) diff --git a/compiler/back-ends/c++-gen/gen-code.c b/compiler/back-ends/c++-gen/gen-code.c index b7d7c2b..e43001e 100644 --- a/compiler/back-ends/c++-gen/gen-code.c +++ b/compiler/back-ends/c++-gen/gen-code.c @@ -1178,10 +1178,10 @@ static void PrintROSEOnInvokeHandler(FILE* src, int bEvents, Module* mod, ValueD const char* pszHandlerPrefix = bEvents ? "RoseEvent" : "RoseInvoke"; fprintf(src, "// [%s] OPID_%s\n", __FUNCTION__, vd->definedName); - fprintf(src, "static long %s_OPID_%s(SNACC::ROSEMessage* pMsg, SNACC::ROSEInvoke* pInvoke, SnaccROSESender* pBase, %sInterface* pInt, SnaccInvokeContext& ctx, std::string& strResponseData)\n", pszHandlerPrefix, vd->definedName, mod->ROSEClassName); + fprintf(src, "static long %s_OPID_%s(std::unique_ptr& pMsg, SnaccROSESender* pBase, %sInterface* pInt, SnaccInvokeContext& ctx, std::string& strResponseData)\n", pszHandlerPrefix, vd->definedName, mod->ROSEClassName); fprintf(src, "{\n"); fprintf(src, "\t%s argument;\n", pszArgument); - fprintf(src, "\tlong lRoseResult = pBase->DecodeInvoke(pMsg, &argument);\n"); + fprintf(src, "\tlong lRoseResult = pBase->DecodeInvoke(*pMsg, &argument);\n"); fprintf(src, "\tif (lRoseResult == ROSE_NOERROR)\n"); const bool bIsDeprecated = IsDeprecatedFlaggedOperation(mod, vd->definedName); const bool bIsMultiLine = bIsDeprecated || pszResult; @@ -1202,12 +1202,12 @@ static void PrintROSEOnInvokeHandler(FILE* src, int bEvents, Module* mod, ValueD { fprintf(src, "\t\t%s error;\n", pszError); fprintf(src, "\t\tauto invokeResult = pInt->OnInvoke_%s(&argument, &result, &error, ctx);\n", vd->definedName); - fprintf(src, "\t\tlRoseResult = pBase->HandleOnInvokeResult(invokeResult, pInvoke, ctx, strResponseData, &result, &error);\n"); + fprintf(src, "\t\tlRoseResult = pBase->HandleOnInvokeResult(invokeResult, *pMsg->invoke, ctx, strResponseData, &result, &error);\n"); } else { fprintf(src, "\t\tauto invokeResult = pInt->OnInvoke_%s(&argument, &result, ctx);\n", vd->definedName); - fprintf(src, "\t\tlRoseResult = pBase->HandleOnInvokeResult(invokeResult, pInvoke, ctx, strResponseData, &result);\n"); + fprintf(src, "\t\tlRoseResult = pBase->HandleOnInvokeResult(invokeResult, *pMsg->invoke, ctx, strResponseData, &result);\n"); } } else @@ -1239,7 +1239,7 @@ static void PrintROSEOnInvokeswitchCase(FILE* src, int bEvents, Module* mod, Val const char* pszHandlerPrefix = bEvents ? "RoseEvent" : "RoseInvoke"; fprintf(src, "\tcase OPID_%s:\n", vd->definedName); - fprintf(src, "\t\treturn %s_OPID_%s(pMsg, pInvoke, pBase, pInt, ctx, strResponseData);\n", pszHandlerPrefix, vd->definedName); + fprintf(src, "\t\treturn %s_OPID_%s(pMsg, pBase, pInt, ctx, strResponseData);\n", pszHandlerPrefix, vd->definedName); } } } /* PrintROSEOnInvokeswitchCase */ @@ -4623,11 +4623,11 @@ void PrintROSECode(FILE* src, FILE* hdr, FILE* hdrInterface, ModuleList* mods, M // generate the InvokeHandler fprintf(hdr, "\t// The main Invoke Dispatcher\n"); - fprintf(hdr, "\tstatic long OnInvoke(SNACC::ROSEMessage* pMsg, SnaccROSESender* pBase, %sInterface* pInt, SnaccInvokeContext& ctx, std::string& strResponseData);\n", m->ROSEClassName); - fprintf(src, "long %s::OnInvoke(SNACC::ROSEMessage* pMsg, SnaccROSESender* pBase, %sInterface* pInt, SnaccInvokeContext& ctx, std::string& strResponseData)\n", m->ROSEClassName, m->ROSEClassName); + fprintf(hdr, "\tstatic long OnInvoke(std::unique_ptr& pMsg, SnaccROSESender* pBase, %sInterface* pInt, SnaccInvokeContext& ctx, std::string& strResponseData);\n", m->ROSEClassName); + fprintf(src, "long %s::OnInvoke(std::unique_ptr& pMsg, SnaccROSESender* pBase, %sInterface* pInt, SnaccInvokeContext& ctx, std::string& strResponseData)\n", m->ROSEClassName, m->ROSEClassName); fprintf(src, "{\n"); - fprintf(src, "\tauto pInvoke = pMsg->invoke;\n"); - fprintf(src, "\tswitch (pInvoke->operationID)\n"); + fprintf(src, "\tauto& invoke = *pMsg->invoke;\n"); + fprintf(src, "\tswitch (invoke.operationID)\n"); fprintf(src, "\t{\n"); // Rose Interface class diff --git a/cpp-lib/include/SnaccROSEBase.h b/cpp-lib/include/SnaccROSEBase.h index 8284ab3..6377d54 100644 --- a/cpp-lib/include/SnaccROSEBase.h +++ b/cpp-lib/include/SnaccROSEBase.h @@ -58,7 +58,7 @@ class SnaccROSEPendingOperation const std::string m_strOperationName; /*! The answer message. */ - const SNACC::ROSEMessage* m_pAnswerMessage; + std::unique_ptr m_pAnswerMessage; /*! Error code (one of the ROSE_ error codes. */ long m_lRoseResult; @@ -69,11 +69,8 @@ class SnaccROSEPendingOperation /*! Outbound invoke telemetry tracked alongside the pending completion state. */ std::shared_ptr m_pTelemetry; - /*! Async Operation completed. - Attention: The AnswerMessage will not be copied. - The AnswerMessage must be new allocated and will be deleted - when processed. */ - void CompleteOperation(long lRoseResult, const SNACC::ROSEMessage* pAnswerMessage, size_t stResponseData = 0); + /*! Async Operation completed. */ + void CompleteOperation(long lRoseResult, std::unique_ptr pAnswerMessage = {}, size_t stResponseData = 0); void FinalizeTelemetry(long lFinalRoseResult, std::shared_ptr pctx); /*! Wait for answer received. */ @@ -285,7 +282,7 @@ class SnaccROSEBase : public SnaccROSESender, public SnaccTelemetryCallback * pResult - the result object (Base type pointer, the caller of the invoke provides the proper type) * pError - the error object (Base type pointer, the caller of the invoke provides the proper type) */ - virtual long HandleOnInvokeResult(SNACC::InvokeResult invokeResult, const SNACC::ROSEInvoke* pInvoke, SnaccInvokeContext& ctx, std::string& strResponse, SNACC::AsnType* pResult, SNACC::AsnType* pError) override; + virtual long HandleOnInvokeResult(SNACC::InvokeResult invokeResult, SNACC::ROSEInvoke& invoke, SnaccInvokeContext& ctx, std::string& strResponse, SNACC::AsnType* pResult, SNACC::AsnType* pError) override; /** * Allows implementers to customize the decoded invoke response before it is handed back @@ -297,7 +294,7 @@ class SnaccROSEBase : public SnaccROSESender, public SnaccTelemetryCallback * error - the decoded error payload in case an error response is received * ctx - contextual data for the invoke */ - virtual long HandleInvokeResult(long lRoseResult, const SNACC::ROSEMessage* pResponseMsg, SNACC::AsnType* result, SNACC::AsnType* error, SnaccInvokeContext& ctx) override; + virtual long HandleInvokeResult(long lRoseResult, SNACC::ROSEMessage& responseMsg, SNACC::AsnType* result, SNACC::AsnType* error, SnaccInvokeContext& ctx) override; /** * An event (invoke without result) that is send to the other side. Should only be called by the ROSE stub itself generated files @@ -319,7 +316,7 @@ class SnaccROSEBase : public SnaccROSESender, public SnaccTelemetryCallback * pInvokeMessage - the invoke message as provided from the other side * argument - the argument object (Base type pointer, the caller of the provides the proper type) */ - long DecodeInvoke(const SNACC::ROSEMessage* pInvokeMessage, SNACC::AsnType* argument) override; + long DecodeInvoke(SNACC::ROSEMessage& invokeMessage, SNACC::AsnType* argument) override; protected: // Get the length prefix for a given strJson payload @@ -329,7 +326,7 @@ class SnaccROSEBase : public SnaccROSESender, public SnaccTelemetryCallback /*! The functions and events. The implementation of this functions is contained in the generated code from the esnacc. */ - virtual long OnInvoke(SNACC::ROSEMessage* pMsg, SnaccInvokeContext& ctx, std::string& strResponse) = 0; + virtual long OnInvoke(std::unique_ptr& pMsg, SnaccInvokeContext& ctx, std::string& strResponse) = 0; /* Function is called when a received data Packet cannot be decoded (invalid Rose Message) * bAlreadyTransportLogged - true if the transport data has already been logged in the transport log (so we do not need to log it again) @@ -343,10 +340,11 @@ class SnaccROSEBase : public SnaccROSESender, public SnaccTelemetryCallback } /*! The decoded message. - pmessage is new allocated and must be deleted inside this function. + Ownership transfers via move; invoke/event messages move into `OnInvokeMessage()`, + responses move into `CompletePendingOperation()`. returns true if the message was processed. set bAllowInvokes to false, if invokes are not processed. */ - virtual bool OnROSEMessage(SNACC::ROSEMessage* pmessage, bool bAllowInvokes, unsigned long ulMessageSize); + virtual bool OnROSEMessage(std::unique_ptr pmessage, bool bAllowInvokes, unsigned long ulMessageSize); /* * This callback provides telemetry data for processed ROSE messages. @@ -358,10 +356,10 @@ class SnaccROSEBase : public SnaccROSESender, public SnaccTelemetryCallback These are called from the OnROSEMessage. Do not dete the parameters. The functions are called before the CompleteOperation */ - virtual void OnInvokeMessage(SNACC::ROSEMessage* pMessage, unsigned long lMessageSize); - virtual void OnResultMessage(SNACC::ROSEResult* pResult, unsigned long lMessageSize); - virtual void OnErrorMessage(SNACC::ROSEError* pError, unsigned long lMessageSize); - virtual void OnRejectMessage(SNACC::ROSEReject* pReject, unsigned long lMessageSize); + virtual void OnInvokeMessage(std::unique_ptr pMessage, unsigned long lMessageSize); + virtual void OnResultMessage(SNACC::ROSEResult& result, unsigned long lMessageSize); + virtual void OnErrorMessage(SNACC::ROSEError& error, unsigned long lMessageSize); + virtual void OnRejectMessage(SNACC::ROSEReject& reject, unsigned long lMessageSize); // The central process wide telemetry callback static inline SnaccTelemetryCallback* m_pTelemetryCallback{}; @@ -390,11 +388,11 @@ class SnaccROSEBase : public SnaccROSESender, public SnaccTelemetryCallback /*! Pending Operation result has been received. Attention: The pMessage Objekt will be taken over from the SnaccROSEPendingOperation Object and deleted in the end. */ - bool CompletePendingOperation(int invokeID, const SNACC::ROSEMessage* pMessage, unsigned long ulMessageSize); + bool CompletePendingOperation(int invokeID, std::unique_ptr pMessage, unsigned long ulMessageSize); bool GetPendingOperationTelemetryInfo(int invokeID, unsigned int& uiOperationID, std::string& strOperationName); static long GetRejectResultCode(const SNACC::ROSEReject* pReject); static long GetRejectResultCode(const SNACC::ROSEReject* pReject, SnaccInvokeContext& ctx); - static long DecodeResponse(const SNACC::ROSEMessage* pResponse, SNACC::ROSEResult** ppResult, SNACC::ROSEError** ppError, SnaccInvokeContext& ctx); + static long DecodeResponse(const SNACC::ROSEMessage& response, SNACC::ROSEResult*& pResult, SNACC::ROSEError*& pError, SnaccInvokeContext& ctx); void CompleteAllPendingOperations(); bool IsProcessingAllowed(); diff --git a/cpp-lib/include/SnaccROSEInterfaces.h b/cpp-lib/include/SnaccROSEInterfaces.h index 5074374..ef63afc 100644 --- a/cpp-lib/include/SnaccROSEInterfaces.h +++ b/cpp-lib/include/SnaccROSEInterfaces.h @@ -227,7 +227,7 @@ class SnaccROSESender * error - the error object (Base type pointer, the caller of the invoke provides the proper type) * pCtx - contextual data for the invoke */ - virtual long HandleInvokeResult(long lRoseResult, const SNACC::ROSEMessage* pResponseMsg, SNACC::AsnType* result, SNACC::AsnType* error, SnaccInvokeContext& ctx) = 0; + virtual long HandleInvokeResult(long lRoseResult, SNACC::ROSEMessage& responseMsg, SNACC::AsnType* result, SNACC::AsnType* error, SnaccInvokeContext& ctx) = 0; /** * Encodes the result or error from an OnInvoke request. Retrieves the result or error from the response @@ -239,7 +239,7 @@ class SnaccROSESender * result - the result object (Base type pointer, the caller of the invoke provides the proper type) * error - the error object (Base type pointer, the caller of the invoke provides the proper type) */ - virtual long HandleOnInvokeResult(SNACC::InvokeResult invokeResult, const SNACC::ROSEInvoke* pInvoke, SnaccInvokeContext& ctx, std::string& strResponse, SNACC::AsnType* pResult, SNACC::AsnType* pError) = 0; + virtual long HandleOnInvokeResult(SNACC::InvokeResult invokeResult, SNACC::ROSEInvoke& invoke, SnaccInvokeContext& ctx, std::string& strResponse, SNACC::AsnType* pResult, SNACC::AsnType* pError) = 0; /** * Decodes an invoke and properly handles logging for it @@ -247,7 +247,7 @@ class SnaccROSESender * pInvokeMessage - the invoke message as provided from the other side * argument - the argument object (Base type pointer, the caller of the provides the proper type) */ - virtual long DecodeInvoke(const SNACC::ROSEMessage* pInvokeMessage, SNACC::AsnType* argument) = 0; + virtual long DecodeInvoke(SNACC::ROSEMessage& invokeMessage, SNACC::AsnType* argument) = 0; /** An event (invoke without result) that is send to the other side. Should only be called by the ROSE stub itself generated files * diff --git a/cpp-lib/src/SnaccROSEBase.cpp b/cpp-lib/src/SnaccROSEBase.cpp index 93eb925..f7c22ec 100644 --- a/cpp-lib/src/SnaccROSEBase.cpp +++ b/cpp-lib/src/SnaccROSEBase.cpp @@ -7,6 +7,7 @@ #include #include #include +#include using namespace SNACC; @@ -24,6 +25,90 @@ namespace } return ""; } + + class InboundInvokeRejectContext + { + public: + AsnIntType m_invokeId = 0; + unsigned int m_uiOperationID = 0; + std::string m_strOperationName; + + static std::optional TryFrom(const SNACC::ROSEMessage& message) + { + if (message.choiceId != ROSEMessage::invokeCid || !message.invoke) + return std::nullopt; + const auto* pInvoke = message.invoke; + InboundInvokeRejectContext ctx; + ctx.m_invokeId = (AsnIntType)pInvoke->invokeID; + ctx.m_uiOperationID = pInvoke->operationID; + const char* szName = SnaccRoseOperationLookup::LookUpName(ctx.m_uiOperationID); + if (szName) + ctx.m_strOperationName = szName; + return ctx; + } + + bool CanSendReject() const + { + return m_invokeId != 99999; + } + + const char* OperationNameCStr() const + { + return m_strOperationName.empty() ? nullptr : m_strOperationName.c_str(); + } + }; + + struct InboundDecodeFailureResult + { + unsigned int uiOperationID = 0; + const char* szOperationName = nullptr; + SnaccTelemetryData::Outcome outcome = SnaccTelemetryData::Outcome::UNHANDLED; + long lTelemetryResult = ROSE_RE_DECODE_FAILED; + }; + + InboundDecodeFailureResult HandleInboundRoseDecodeFailure( + SnaccROSEBase& rose, + const std::optional& rejectCtx, + bool bSendReject, + const std::string& strRejectDetails) + { + InboundDecodeFailureResult result; + if (!rejectCtx) + return result; + result.uiOperationID = rejectCtx->m_uiOperationID; + result.szOperationName = rejectCtx->OperationNameCStr(); + if (!bSendReject || !rejectCtx->CanSendReject()) + return result; + + ROSEReject reject; + if (rejectCtx->m_invokeId) + { + reject.invokedID.choiceId = ROSERejectChoice::invokedIDCid; + reject.invokedID.invokedID = new AsnInt(rejectCtx->m_invokeId); + } + else + { + reject.invokedID.choiceId = ROSERejectChoice::invokednullCid; + reject.invokedID.invokednull = new AsnNull; + } + + reject.reject = new RejectProblem(); + reject.reject->choiceId = RejectProblem::invokeProblemCid; + reject.reject->invokeProblem = new InvokeProblem(SNACC::InvokeProblem::mistypedArgument); + reject.details = UTF8String::CreateNewFromASCII(strRejectDetails.c_str()); + + auto pRejectCtx = SnaccInvokeContext::Create( + SnaccInvokeContextInit(SnaccInvokeDirection::INBOUND, nullptr, rejectCtx->OperationNameCStr())); + const long lRejectResult = rose.SendRejectEx(&reject, *pRejectCtx); + if (lRejectResult == ROSE_NOERROR) + { + result.outcome = SnaccTelemetryData::Outcome::REJECT; + result.lTelemetryResult = ROSE_REJECT_MISTYPEDARGUMENT; + } + else + result.lTelemetryResult = lRejectResult; + return result; + } } // namespace SnaccInvokeContextInit::SnaccInvokeContextInit(SnaccInvokeDirection direction, SNACC::ROSEInvoke* pInvoke, const char* szOperationName /*= nullptr*/) @@ -246,29 +331,24 @@ SnaccROSEPendingOperation::SnaccROSEPendingOperation(long lInvokeID, unsigned in : m_lInvokeID(lInvokeID), m_uiOperationID(uiOperationID), m_strOperationName(szOperationName ? szOperationName : ""), - m_pAnswerMessage(nullptr), m_lRoseResult(0), m_stResponseData(0) { } -SnaccROSEPendingOperation::~SnaccROSEPendingOperation() -{ - if (m_pAnswerMessage) - delete m_pAnswerMessage; -} +SnaccROSEPendingOperation::~SnaccROSEPendingOperation() = default; bool SnaccROSEPendingOperation::WaitForComplete(long ulTimeOut /*= -1*/) { return m_CompletedEvent.waitfor(ulTimeOut); } -void SnaccROSEPendingOperation::CompleteOperation(long lRoseResult, const SNACC::ROSEMessage* pAnswerMessage, size_t stResponseData) +void SnaccROSEPendingOperation::CompleteOperation(long lRoseResult, std::unique_ptr pAnswerMessage, size_t stResponseData) { m_lRoseResult = lRoseResult; m_stResponseData = stResponseData; if (pAnswerMessage) - m_pAnswerMessage = pAnswerMessage; + m_pAnswerMessage = std::move(pAnswerMessage); m_CompletedEvent.signal(); } @@ -355,7 +435,7 @@ void SnaccROSEBase::CompleteAllPendingOperations() std::lock_guard guard(m_InternalProtectMutex); for (auto it = m_PendingOperations.begin(); it != m_PendingOperations.end(); it++) - it->second->CompleteOperation(ROSE_TE_SHUTDOWN, NULL); + it->second->CompleteOperation(ROSE_TE_SHUTDOWN); } bool SnaccROSEBase::IsProcessingAllowed() @@ -383,7 +463,7 @@ void SnaccROSEBase::RemovePendingOperation(int invokeID) m_PendingOperations.erase(it); } -bool SnaccROSEBase::CompletePendingOperation(int invokeID, const SNACC::ROSEMessage* pMessage, unsigned long ulMessageSize) +bool SnaccROSEBase::CompletePendingOperation(int invokeID, std::unique_ptr pMessage, unsigned long ulMessageSize) { std::lock_guard guard(m_InternalProtectMutex); @@ -407,13 +487,9 @@ bool SnaccROSEBase::CompletePendingOperation(int invokeID, const SNACC::ROSEMess break; } - it->second->CompleteOperation(lRoseResult, pMessage, ulMessageSize); + it->second->CompleteOperation(lRoseResult, std::move(pMessage), ulMessageSize); return true; } - else - { - delete pMessage; - } return false; } @@ -497,18 +573,17 @@ bool SnaccROSEBase::OnBinaryDataBlockResult(const char* lpBytes, unsigned long l case SNACC::TransportEncoding::BER: { AsnBuf buffer((const char*)lpBytes, lSize); - ROSEMessage* pmessage = new ROSEMessage; + auto pmessage = std::make_unique(); AsnLen bytesDecoded = 0; - bool bRoseEnvelopeDecoded = false; + std::optional rejectCtx; try { pmessage->BDec(buffer, bytesDecoded); - bRoseEnvelopeDecoded = true; + rejectCtx = InboundInvokeRejectContext::TryFrom(*pmessage); if (bLogTransportData) LogTransportData(false, m_eTransportEncoding, nullptr, lpBytes, lSize, nullptr, nullptr); - // pmessage will be deleted inside - bReturn = OnROSEMessage(pmessage, false, lSize); + bReturn = OnROSEMessage(std::move(pmessage), false, lSize); } catch (const SnaccException& ex) { @@ -523,42 +598,8 @@ bool SnaccROSEBase::OnBinaryDataBlockResult(const char* lpBytes, unsigned long l PrintJSONToLog(false, true, nullptr, strError.c_str(), strError.length()); OnRoseDecodeError(bLogTransportData, SNACC::TransportEncoding::BER, lpBytes, lSize, ex.what()); - unsigned int uiOperationID = 0; - const char* szOperationName = nullptr; - auto outcome = SnaccTelemetryData::Outcome::UNHANDLED; - long lTelemetryResult = ROSE_RE_DECODE_FAILED; - if (bRoseEnvelopeDecoded && pmessage->choiceId == ROSEMessage::invokeCid && pmessage->invoke) - { - auto* pInvoke = pmessage->invoke; - uiOperationID = pInvoke->operationID; - szOperationName = SnaccRoseOperationLookup::LookUpName(uiOperationID); - if ((AsnIntType)pInvoke->invokeID != 99999) - { - ROSEReject reject; - if ((AsnIntType)pInvoke->invokeID) - { - reject.invokedID.choiceId = ROSERejectChoice::invokedIDCid; - reject.invokedID.invokedID = new AsnInt(pInvoke->invokeID); - } - else - { - reject.invokedID.choiceId = ROSERejectChoice::invokednullCid; - reject.invokedID.invokednull = new AsnNull; - } - - auto pRejectCtx = SnaccInvokeContext::Create(SnaccInvokeContextInit(SnaccInvokeDirection::INBOUND, pInvoke, szOperationName)); - const long lRejectResult = SendRejectEx(&reject, *pRejectCtx); - if (lRejectResult == ROSE_NOERROR) - { - outcome = SnaccTelemetryData::Outcome::REJECT; - lTelemetryResult = ROSE_REJECT_MISTYPEDARGUMENT; - } - else - lTelemetryResult = lRejectResult; - } - } - OnInvokeProcessed(SnaccTelemetryData::CreateFinalized(SnaccTelemetryData::Direction::INBOUND, uiOperationID, szOperationName, lSize, outcome, SnaccTelemetryData::Stage::INBOUND_DECODE, SnaccTelemetryData::Reason::DECODE_FAILED, lTelemetryResult)); - delete pmessage; + const auto failure = HandleInboundRoseDecodeFailure(*this, rejectCtx, false, ex.what()); + OnInvokeProcessed(SnaccTelemetryData::CreateFinalized(SnaccTelemetryData::Direction::INBOUND, failure.uiOperationID, failure.szOperationName, lSize, failure.outcome, SnaccTelemetryData::Stage::INBOUND_DECODE, SnaccTelemetryData::Reason::DECODE_FAILED, failure.lTelemetryResult)); return true; } break; @@ -571,19 +612,18 @@ bool SnaccROSEBase::OnBinaryDataBlockResult(const char* lpBytes, unsigned long l SJson::Reader reader; if (reader.parse((const char*)lpBytes + iHeaderLen, (const char*)lpBytes + lSize, value)) { - ROSEMessage* pmessage = new ROSEMessage; - bool bRoseEnvelopeDecoded = false; + auto pmessage = std::make_unique(); + std::optional rejectCtx; try { if (!pmessage->JDec(value)) throw InvalidTagException("ROSEMessage", "decode failed: ROSEMessage", STACK_ENTRY); - - bRoseEnvelopeDecoded = true; + + rejectCtx = InboundInvokeRejectContext::TryFrom(*pmessage); if (bLogTransportData) - LogTransportData(false, m_eTransportEncoding, nullptr, lpBytes, lSize, pmessage, &value); + LogTransportData(false, m_eTransportEncoding, nullptr, lpBytes, lSize, pmessage.get(), &value); - // pmessage will be deleted inside - bReturn = OnROSEMessage(pmessage, false, lSize); + bReturn = OnROSEMessage(std::move(pmessage), false, lSize); } catch (const SnaccException& ex) { @@ -598,47 +638,8 @@ bool SnaccROSEBase::OnBinaryDataBlockResult(const char* lpBytes, unsigned long l PrintJSONToLog(false, true, nullptr, strError.c_str(), strError.length()); OnRoseDecodeError(bLogTransportData, m_eTransportEncoding, lpBytes, lSize, ex.what()); - unsigned int uiOperationID = 0; - const char* szOperationName = nullptr; - auto outcome = SnaccTelemetryData::Outcome::UNHANDLED; - long lTelemetryResult = ROSE_RE_DECODE_FAILED; - if (bRoseEnvelopeDecoded && pmessage->choiceId == ROSEMessage::invokeCid && pmessage->invoke) - { - auto* pInvoke = pmessage->invoke; - uiOperationID = pInvoke->operationID; - szOperationName = SnaccRoseOperationLookup::LookUpName(uiOperationID); - if ((AsnIntType)pInvoke->invokeID != 99999) - { - ROSEReject reject; - if ((AsnIntType)pInvoke->invokeID) - { - reject.invokedID.choiceId = ROSERejectChoice::invokedIDCid; - reject.invokedID.invokedID = new AsnInt(pInvoke->invokeID); - } - else - { - reject.invokedID.choiceId = ROSERejectChoice::invokednullCid; - reject.invokedID.invokednull = new AsnNull; - } - reject.reject = new RejectProblem; - reject.reject->choiceId = RejectProblem::invokeProblemCid; - reject.reject->invokeProblem = new InvokeProblem; - *reject.reject->invokeProblem = InvokeProblem::mistypedArgument; - reject.details = UTF8String::CreateNewFromASCII(strError.c_str()); - - auto pRejectCtx = SnaccInvokeContext::Create(SnaccInvokeContextInit(SnaccInvokeDirection::INBOUND, pInvoke, szOperationName)); - const long lRejectResult = SendRejectEx(&reject, *pRejectCtx); - if (lRejectResult == ROSE_NOERROR) - { - outcome = SnaccTelemetryData::Outcome::REJECT; - lTelemetryResult = ROSE_REJECT_MISTYPEDARGUMENT; - } - else - lTelemetryResult = lRejectResult; - } - } - OnInvokeProcessed(SnaccTelemetryData::CreateFinalized(SnaccTelemetryData::Direction::INBOUND, uiOperationID, szOperationName, lSize, outcome, SnaccTelemetryData::Stage::INBOUND_DECODE, SnaccTelemetryData::Reason::DECODE_FAILED, lTelemetryResult)); - delete pmessage; + const auto failure = HandleInboundRoseDecodeFailure(*this, rejectCtx, false, strError); + OnInvokeProcessed(SnaccTelemetryData::CreateFinalized(SnaccTelemetryData::Direction::INBOUND, failure.uiOperationID, failure.szOperationName, lSize, failure.outcome, SnaccTelemetryData::Stage::INBOUND_DECODE, SnaccTelemetryData::Reason::DECODE_FAILED, failure.lTelemetryResult)); return true; } } @@ -791,18 +792,17 @@ void SnaccROSEBase::OnBinaryDataBlock(const char* lpBytes, unsigned long ulSize, case SNACC::TransportEncoding::BER: { AsnBuf buffer((const char*)lpBytes, ulSize); - ROSEMessage* pmessage = new ROSEMessage; + auto pmessage = std::make_unique(); AsnLen bytesDecoded = 0; - bool bRoseEnvelopeDecoded = false; + std::optional rejectCtx; try { pmessage->BDec(buffer, bytesDecoded); - bRoseEnvelopeDecoded = true; + rejectCtx = InboundInvokeRejectContext::TryFrom(*pmessage); if (bLogTransportData) LogTransportData(false, SNACC::TransportEncoding::BER, nullptr, lpBytes, ulSize, nullptr, nullptr); - // pmessage will be deleted inside - OnROSEMessage(pmessage, true, ulSize); + OnROSEMessage(std::move(pmessage), true, ulSize); } catch (const SnaccException& ex) { @@ -818,48 +818,8 @@ void SnaccROSEBase::OnBinaryDataBlock(const char* lpBytes, unsigned long ulSize, OnRoseDecodeError(bLogTransportData, SNACC::TransportEncoding::BER, lpBytes, ulSize, ex.what()); - unsigned int uiOperationID = 0; - const char* szOperationName = nullptr; - auto outcome = SnaccTelemetryData::Outcome::UNHANDLED; - long lTelemetryResult = ROSE_RE_DECODE_FAILED; - if (bRoseEnvelopeDecoded && pmessage->choiceId == ROSEMessage::invokeCid && pmessage->invoke) - { - auto* pInvoke = pmessage->invoke; - uiOperationID = pInvoke->operationID; - szOperationName = SnaccRoseOperationLookup::LookUpName(uiOperationID); - if ((AsnIntType)pInvoke->invokeID != 99999) - { - // Provide the detail that the argument was non decodable to the client - ROSEReject reject; - reject.reject = new RejectProblem(); - reject.reject->choiceId = RejectProblem::invokeProblemCid; - reject.reject->invokeProblem = new InvokeProblem(SNACC::InvokeProblem::mistypedArgument); - reject.details = new UTF8String(); - reject.details->setASCII(ex.what()); - if ((AsnIntType)pInvoke->invokeID) - { - reject.invokedID.choiceId = ROSERejectChoice::invokedIDCid; - reject.invokedID.invokedID = new AsnInt(pInvoke->invokeID); - } - else - { - reject.invokedID.choiceId = ROSERejectChoice::invokednullCid; - reject.invokedID.invokednull = new AsnNull; - } - - auto pRejectCtx = SnaccInvokeContext::Create(SnaccInvokeContextInit(SnaccInvokeDirection::INBOUND, pInvoke, szOperationName)); - const long lRejectResult = SendRejectEx(&reject, *pRejectCtx); - if (lRejectResult == ROSE_NOERROR) - { - outcome = SnaccTelemetryData::Outcome::REJECT; - lTelemetryResult = ROSE_REJECT_MISTYPEDARGUMENT; - } - else - lTelemetryResult = lRejectResult; - } - } - OnInvokeProcessed(SnaccTelemetryData::CreateFinalized(SnaccTelemetryData::Direction::INBOUND, uiOperationID, szOperationName, ulSize, outcome, SnaccTelemetryData::Stage::INBOUND_DECODE, SnaccTelemetryData::Reason::DECODE_FAILED, lTelemetryResult)); - delete pmessage; + const auto failure = HandleInboundRoseDecodeFailure(*this, rejectCtx, true, ex.what()); + OnInvokeProcessed(SnaccTelemetryData::CreateFinalized(SnaccTelemetryData::Direction::INBOUND, failure.uiOperationID, failure.szOperationName, ulSize, failure.outcome, SnaccTelemetryData::Stage::INBOUND_DECODE, SnaccTelemetryData::Reason::DECODE_FAILED, failure.lTelemetryResult)); } break; } @@ -872,17 +832,16 @@ void SnaccROSEBase::OnBinaryDataBlock(const char* lpBytes, unsigned long ulSize, SJson::Reader reader; if (reader.parse((const char*)lpBytes + iHeaderLen, (const char*)lpBytes + ulSize, value)) { - ROSEMessage* pmessage = new ROSEMessage; - bool bRoseEnvelopeDecoded = false; + auto pmessage = std::make_unique(); + std::optional rejectCtx; try { if (!pmessage->JDec(value)) throw InvalidTagException("ROSEMessage", "decode failed: ROSEMessage", STACK_ENTRY); - bRoseEnvelopeDecoded = true; + rejectCtx = InboundInvokeRejectContext::TryFrom(*pmessage); if (bLogTransportData) - LogTransportData(false, m_eTransportEncoding, nullptr, lpBytes, ulSize, pmessage, &value); - // pmessage will be deleted inside - OnROSEMessage(pmessage, true, ulSize); + LogTransportData(false, m_eTransportEncoding, nullptr, lpBytes, ulSize, pmessage.get(), &value); + OnROSEMessage(std::move(pmessage), true, ulSize); } catch (const SnaccException& ex) { @@ -898,47 +857,8 @@ void SnaccROSEBase::OnBinaryDataBlock(const char* lpBytes, unsigned long ulSize, OnRoseDecodeError(bLogTransportData, m_eTransportEncoding, lpBytes, ulSize, ex.what()); - unsigned int uiOperationID = 0; - const char* szOperationName = nullptr; - auto outcome = SnaccTelemetryData::Outcome::UNHANDLED; - long lTelemetryResult = ROSE_RE_DECODE_FAILED; - if (bRoseEnvelopeDecoded && pmessage->choiceId == ROSEMessage::invokeCid && pmessage->invoke) - { - auto* pInvoke = pmessage->invoke; - uiOperationID = pInvoke->operationID; - szOperationName = SnaccRoseOperationLookup::LookUpName(uiOperationID); - if ((AsnIntType)pInvoke->invokeID != 99999) - { - ROSEReject reject; - if ((AsnIntType)pInvoke->invokeID) - { - reject.invokedID.choiceId = ROSERejectChoice::invokedIDCid; - reject.invokedID.invokedID = new AsnInt(pInvoke->invokeID); - } - else - { - reject.invokedID.choiceId = ROSERejectChoice::invokednullCid; - reject.invokedID.invokednull = new AsnNull; - } - reject.reject = new RejectProblem; - reject.reject->choiceId = RejectProblem::invokeProblemCid; - reject.reject->invokeProblem = new InvokeProblem; - *reject.reject->invokeProblem = InvokeProblem::mistypedArgument; - reject.details = UTF8String::CreateNewFromASCII(strError.c_str()); - - auto pRejectCtx = SnaccInvokeContext::Create(SnaccInvokeContextInit(SnaccInvokeDirection::INBOUND, pInvoke, szOperationName)); - const long lRejectResult = SendRejectEx(&reject, *pRejectCtx); - if (lRejectResult == ROSE_NOERROR) - { - outcome = SnaccTelemetryData::Outcome::REJECT; - lTelemetryResult = ROSE_REJECT_MISTYPEDARGUMENT; - } - else - lTelemetryResult = lRejectResult; - } - } - OnInvokeProcessed(SnaccTelemetryData::CreateFinalized(SnaccTelemetryData::Direction::INBOUND, uiOperationID, szOperationName, ulSize, outcome, SnaccTelemetryData::Stage::INBOUND_DECODE, SnaccTelemetryData::Reason::DECODE_FAILED, lTelemetryResult)); - delete pmessage; + const auto failure = HandleInboundRoseDecodeFailure(*this, rejectCtx, true, strError); + OnInvokeProcessed(SnaccTelemetryData::CreateFinalized(SnaccTelemetryData::Direction::INBOUND, failure.uiOperationID, failure.szOperationName, ulSize, failure.outcome, SnaccTelemetryData::Stage::INBOUND_DECODE, SnaccTelemetryData::Reason::DECODE_FAILED, failure.lTelemetryResult)); } } else @@ -998,65 +918,56 @@ void SnaccROSEBase::OnBinaryDataBlock(const char* lpBytes, unsigned long ulSize, } } -bool SnaccROSEBase::OnROSEMessage(SNACC::ROSEMessage* pMessage, bool bAllowAllInvokes, unsigned long ulMessageSize) +bool SnaccROSEBase::OnROSEMessage(std::unique_ptr pMessage, bool bAllowAllInvokes, unsigned long ulMessageSize) { bool bProcessed = false; switch (pMessage->choiceId) { case ROSEMessage::invokeCid: - if (pMessage->invoke) + { + auto& invoke = *pMessage->invoke; + if (bAllowAllInvokes || m_multithreadInvokeIDs.find(invoke.operationID) != m_multithreadInvokeIDs.end()) { - auto* pInvoke = pMessage->invoke; - if (bAllowAllInvokes || m_multithreadInvokeIDs.find(pInvoke->operationID) != m_multithreadInvokeIDs.end()) - { - if (pInvoke->operationName && pInvoke->operationID == 0) - pInvoke->operationID = SnaccRoseOperationLookup::LookUpID(pInvoke->operationName->getASCII().c_str()); + if (invoke.operationName && invoke.operationID == 0) + invoke.operationID = SnaccRoseOperationLookup::LookUpID(invoke.operationName->getASCII().c_str()); - if (pInvoke->operationID || pInvoke->operationName) - OnInvokeMessage(pMessage, ulMessageSize); + if (invoke.operationID || invoke.operationName) + OnInvokeMessage(std::move(pMessage), ulMessageSize); - bProcessed = true; - } + bProcessed = true; } break; + } case ROSEMessage::resultCid: - if (pMessage->result) - { - OnResultMessage(pMessage->result, ulMessageSize); - // do not intercept anything if pmessage->result->result == nullptr is in place, because the missing invokeID already takes this into account - CompletePendingOperation(pMessage->result->invokeID, pMessage, ulMessageSize); - return true; - } - break; + { + const int invokeID = pMessage->result->invokeID; + OnResultMessage(*pMessage->result, ulMessageSize); + CompletePendingOperation(invokeID, std::move(pMessage), ulMessageSize); + return true; + } case ROSEMessage::errorCid: - if (pMessage->error) - { - OnErrorMessage(pMessage->error, ulMessageSize); - // do not intercept anything if pmessage->error->error == nullptr is in place, because the missing invokeID already takes this into account - CompletePendingOperation(pMessage->error->invokedID, pMessage, ulMessageSize); - return true; - } - break; + { + const int invokeID = pMessage->error->invokedID; + OnErrorMessage(*pMessage->error, ulMessageSize); + CompletePendingOperation(invokeID, std::move(pMessage), ulMessageSize); + return true; + } case ROSEMessage::rejectCid: - if (pMessage->reject) + { + auto& reject = *pMessage->reject; + OnRejectMessage(reject, ulMessageSize); + if (reject.invokedID.choiceId == ROSERejectChoice::invokedIDCid && reject.invokedID.invokedID != nullptr) { - OnRejectMessage(pMessage->reject, ulMessageSize); - if (pMessage->reject->invokedID.choiceId == ROSERejectChoice::invokedIDCid) - { - // Test! with REJECT the InvokeID is a choice and therefore the ID itself is a pointer! and it can become nullptr. - if (pMessage->reject->invokedID.invokedID != nullptr) - { - CompletePendingOperation(*pMessage->reject->invokedID.invokedID, pMessage, ulMessageSize); - return true; - } - } - bProcessed = true; + const int invokeID = *reject.invokedID.invokedID; + CompletePendingOperation(invokeID, std::move(pMessage), ulMessageSize); + return true; } + bProcessed = true; break; + } default: break; } - delete pMessage; return bProcessed; } @@ -1211,16 +1122,16 @@ long SnaccROSEBase::EncodeInvokeRejectResponse(const SNACC::ROSEInvoke* pInvoke, return lEncodeResult == ROSE_NOERROR ? lProtocolResult : lEncodeResult; } -void SnaccROSEBase::OnInvokeMessage(SNACC::ROSEMessage* pMessage, unsigned long ulMessageSize) +void SnaccROSEBase::OnInvokeMessage(std::unique_ptr pMessage, unsigned long ulMessageSize) { std::string strResponse; long lResult = ROSE_REJECT_UNKNOWNOPERATION; - auto* pInvoke = pMessage->invoke; - const char* szOperationName = SnaccRoseOperationLookup::LookUpName(pInvoke->operationID); + auto& invoke = *pMessage->invoke; + const char* szOperationName = SnaccRoseOperationLookup::LookUpName(invoke.operationID); - SnaccInvokeContextInit init(SnaccInvokeDirection::INBOUND, pInvoke, szOperationName); + SnaccInvokeContextInit init(SnaccInvokeDirection::INBOUND, &invoke, szOperationName); auto pCtx = CreateInvokeContext(init); - auto telemetry = SnaccTelemetryData::Create(SnaccTelemetryData::Direction::INBOUND, pInvoke->operationID, szOperationName, ulMessageSize); + auto telemetry = SnaccTelemetryData::Create(SnaccTelemetryData::Direction::INBOUND, invoke.operationID, szOperationName, ulMessageSize); auto telemetryResult = SnaccTelemetryData::Outcome::UNHANDLED; auto telemetryReason = SnaccTelemetryData::Reason::UNKNOWN_FAILURE; bool bInvokeException = false; @@ -1242,20 +1153,20 @@ void SnaccROSEBase::OnInvokeMessage(SNACC::ROSEMessage* pMessage, unsigned long error["exception"] = ex.what(); error["method"] = __FUNCTION__; error["error"] = (int)ex.m_errorCode; - error["invokeID"] = (int)pInvoke->invokeID; + error["invokeID"] = (int)invoke.invokeID; std::string jsonString = getPrettyPrinted(error); PrintJSONToLog(false, true, nullptr, jsonString.c_str(), jsonString.length()); } } - const bool bIsInvoke = (AsnIntType)pInvoke->invokeID != 99999; + const bool bIsInvoke = (AsnIntType)invoke.invokeID != 99999; const bool bIsRejectResponse = lResult != ROSE_NOERROR && bIsInvoke; // if the Result is ROSE_NOERROR the request has been processed with a result or an error (the invoke context points out if it was replied with an error) // if the result is ROSE_REJECT_ASYNCOPERATION, the result will be sent async if (bIsRejectResponse) { - lResult = EncodeInvokeRejectResponse(pInvoke, lResult, *pCtx, strResponse); + lResult = EncodeInvokeRejectResponse(&invoke, lResult, *pCtx, strResponse); if (lResult != ROSE_NOERROR) telemetryReason = GetUnhandledReasonFromResult(lResult); } @@ -1305,29 +1216,29 @@ void SnaccROSEBase::OnInvokeMessage(SNACC::ROSEMessage* pMessage, unsigned long OnInvokeProcessed(telemetry); } -void SnaccROSEBase::OnResultMessage(SNACC::ROSEResult* presult, unsigned long ulMessageSize) +void SnaccROSEBase::OnResultMessage(SNACC::ROSEResult& result, unsigned long ulMessageSize) { unsigned int uiOperationID = 0; std::string strOperationName; - GetPendingOperationTelemetryInfo(presult->invokeID, uiOperationID, strOperationName); + GetPendingOperationTelemetryInfo(result.invokeID, uiOperationID, strOperationName); OnInvokeProcessed(SnaccTelemetryData::CreateFinalized(SnaccTelemetryData::Direction::INBOUND, uiOperationID, strOperationName.c_str(), ulMessageSize, SnaccTelemetryData::Outcome::RESULT, SnaccTelemetryData::Stage::INBOUND_RESPONSE, SnaccTelemetryData::Reason::REMOTE_RESULT, ROSE_NOERROR)); } -void SnaccROSEBase::OnErrorMessage(SNACC::ROSEError* perror, unsigned long ulMessageSize) +void SnaccROSEBase::OnErrorMessage(SNACC::ROSEError& error, unsigned long ulMessageSize) { unsigned int uiOperationID = 0; std::string strOperationName; - GetPendingOperationTelemetryInfo(perror->invokedID, uiOperationID, strOperationName); + GetPendingOperationTelemetryInfo(error.invokedID, uiOperationID, strOperationName); OnInvokeProcessed(SnaccTelemetryData::CreateFinalized(SnaccTelemetryData::Direction::INBOUND, uiOperationID, strOperationName.c_str(), ulMessageSize, SnaccTelemetryData::Outcome::ERR, SnaccTelemetryData::Stage::INBOUND_RESPONSE, SnaccTelemetryData::Reason::REMOTE_ERROR, ROSE_ERROR_VALUE)); } -void SnaccROSEBase::OnRejectMessage(SNACC::ROSEReject* preject, unsigned long ulMessageSize) +void SnaccROSEBase::OnRejectMessage(SNACC::ROSEReject& reject, unsigned long ulMessageSize) { unsigned int uiOperationID = 0; std::string strOperationName; - if (preject->invokedID.choiceId == ROSERejectChoice::invokedIDCid && preject->invokedID.invokedID != nullptr) - GetPendingOperationTelemetryInfo(*preject->invokedID.invokedID, uiOperationID, strOperationName); - OnInvokeProcessed(SnaccTelemetryData::CreateFinalized(SnaccTelemetryData::Direction::INBOUND, uiOperationID, strOperationName.c_str(), ulMessageSize, SnaccTelemetryData::Outcome::REJECT, SnaccTelemetryData::Stage::INBOUND_RESPONSE, SnaccTelemetryData::Reason::REMOTE_REJECT, GetRejectResultCode(preject))); + if (reject.invokedID.choiceId == ROSERejectChoice::invokedIDCid && reject.invokedID.invokedID != nullptr) + GetPendingOperationTelemetryInfo(*reject.invokedID.invokedID, uiOperationID, strOperationName); + OnInvokeProcessed(SnaccTelemetryData::CreateFinalized(SnaccTelemetryData::Direction::INBOUND, uiOperationID, strOperationName.c_str(), ulMessageSize, SnaccTelemetryData::Outcome::REJECT, SnaccTelemetryData::Stage::INBOUND_RESPONSE, SnaccTelemetryData::Reason::REMOTE_REJECT, GetRejectResultCode(&reject))); } long SnaccROSEBase::GetNextInvokeID() @@ -1558,7 +1469,7 @@ long SnaccROSEBase::SendInvoke(SNACC::ROSEInvoke* pinvoke, SNACC::AsnType* resul pendingOP.m_lRoseResult = lRoseResult; if (pendingOP.m_pAnswerMessage) - lRoseResult = HandleInvokeResult(lRoseResult, pendingOP.m_pAnswerMessage, result, error, ctx); + lRoseResult = HandleInvokeResult(lRoseResult, *pendingOP.m_pAnswerMessage, result, error, ctx); pendingOP.FinalizeTelemetry(lRoseResult, pCtx); OnInvokeProcessed(pendingOP.m_pTelemetry); @@ -1568,13 +1479,13 @@ long SnaccROSEBase::SendInvoke(SNACC::ROSEInvoke* pinvoke, SNACC::AsnType* resul return lRoseResult; } -long SnaccROSEBase::DecodeResponse(const SNACC::ROSEMessage* pResponse, SNACC::ROSEResult** ppResult, SNACC::ROSEError** ppError, SnaccInvokeContext& ctx) +long SnaccROSEBase::DecodeResponse(const SNACC::ROSEMessage& response, SNACC::ROSEResult*& pResult, SNACC::ROSEError*& pError, SnaccInvokeContext& ctx) { long lRoseResult = ROSE_RE_INVALID_ANSWER; - if (!pResponse) - return lRoseResult; + pResult = nullptr; + pError = nullptr; - switch (pResponse->choiceId) + switch (response.choiceId) { case ROSEMessage::notinitialized: throw std::runtime_error("response ROSEMessage choiceId is notinitialized"); @@ -1582,16 +1493,14 @@ long SnaccROSEBase::DecodeResponse(const SNACC::ROSEMessage* pResponse, SNACC::R break; case ROSEMessage::resultCid: lRoseResult = ROSE_NOERROR; - if (pResponse->result && ppResult) - *ppResult = pResponse->result; + pResult = response.result; break; case ROSEMessage::errorCid: lRoseResult = ROSE_ERROR_VALUE; - if (pResponse->error && ppError) - *ppError = pResponse->error; + pError = response.error; break; case ROSEMessage::rejectCid: - lRoseResult = GetRejectResultCode(pResponse->reject, ctx); + lRoseResult = GetRejectResultCode(response.reject, ctx); break; } @@ -1756,7 +1665,7 @@ SNACC::TransportEncoding SnaccROSEBase::GetTransportEncoding() const return m_eTransportEncoding; } -long SnaccROSEBase::HandleInvokeResult(long lRoseResult, const SNACC::ROSEMessage* pResponseMsg, SNACC::AsnType* result, SNACC::AsnType* error, SnaccInvokeContext& ctx) +long SnaccROSEBase::HandleInvokeResult(long lRoseResult, SNACC::ROSEMessage& responseMsg, SNACC::AsnType* result, SNACC::AsnType* error, SnaccInvokeContext& ctx) { // In case of transport errors, we hand that value back as we have no response to parse if (ISROSE_TE(lRoseResult)) @@ -1764,7 +1673,7 @@ long SnaccROSEBase::HandleInvokeResult(long lRoseResult, const SNACC::ROSEMessag SNACC::ROSEError* pError = nullptr; SNACC::ROSEResult* pResult = nullptr; - lRoseResult = DecodeResponse(pResponseMsg, &pResult, &pError, ctx); + lRoseResult = DecodeResponse(responseMsg, pResult, pError, ctx); if (lRoseResult == ROSE_NOERROR) { @@ -1786,18 +1695,18 @@ long SnaccROSEBase::HandleInvokeResult(long lRoseResult, const SNACC::ROSEMessag if (logLevel & ((int)SNACC::EAsnLogLevel::JSON | (int)SNACC::EAsnLogLevel::JSON_ALWAYS_PRETTY_PRINTED)) { // Backup the original response object inside the response message - ROSEResultSeq* pOriginalResponse = pResponseMsg->result->result; + ROSEResultSeq* pOriginalResponse = responseMsg.result->result; // Set the decoded result object into the response to be able to fully log the whole message - pResponseMsg->result->result = new ROSEResultSeq(); - pResponseMsg->result->result->result.value = result; - LogTransportData(false, SNACC::TransportEncoding::BER, nullptr, nullptr, 0, pResponseMsg, nullptr); + responseMsg.result->result = new ROSEResultSeq(); + responseMsg.result->result->result.value = result; + LogTransportData(false, SNACC::TransportEncoding::BER, nullptr, nullptr, 0, &responseMsg, nullptr); // As we hand back the result object to the outer world (function argument) we need to set it to NULL to prevent deletion if we discard the inserted object - pResponseMsg->result->result->result.value = NULL; + responseMsg.result->result->result.value = NULL; // Delete the inserted result object and reset to the original response object - delete pResponseMsg->result->result; - pResponseMsg->result->result = pOriginalResponse; + delete responseMsg.result->result; + responseMsg.result->result = pOriginalResponse; } } else if (pResult->result->result.jsonBuf) @@ -1840,18 +1749,18 @@ long SnaccROSEBase::HandleInvokeResult(long lRoseResult, const SNACC::ROSEMessag if (logLevel & ((int)SNACC::EAsnLogLevel::JSON | (int)SNACC::EAsnLogLevel::JSON_ALWAYS_PRETTY_PRINTED)) { // Backup the original error object inside the response message - AsnAny* pOriginalError = pResponseMsg->error->error; + AsnAny* pOriginalError = responseMsg.error->error; // Set the decoded error object into the response to be able to fully log the whole message - pResponseMsg->error->error = new AsnAny(); - pResponseMsg->error->error->value = error; - LogTransportData(false, SNACC::TransportEncoding::BER, nullptr, nullptr, 0, pResponseMsg, nullptr); + responseMsg.error->error = new AsnAny(); + responseMsg.error->error->value = error; + LogTransportData(false, SNACC::TransportEncoding::BER, nullptr, nullptr, 0, &responseMsg, nullptr); // As we hand back the error object to the outer world (function argument) we need to set it to NULL to prevent deletion if we discard the inserted object - pResponseMsg->error->error->value = NULL; + responseMsg.error->error->value = NULL; // Delete the inserted error object and reset to the original response object - delete pResponseMsg->error->error; - pResponseMsg->error->error = pOriginalError; + delete responseMsg.error->error; + responseMsg.error->error = pOriginalError; } } else if (pError->error->jsonBuf) @@ -1878,9 +1787,9 @@ long SnaccROSEBase::HandleInvokeResult(long lRoseResult, const SNACC::ROSEMessag return lRoseResult; } -long SnaccROSEBase::HandleOnInvokeResult(SNACC::InvokeResult invokeResult, const SNACC::ROSEInvoke* pInvoke, SnaccInvokeContext& ctx, std::string& strResponse, SNACC::AsnType* pResult, SNACC::AsnType* pError) +long SnaccROSEBase::HandleOnInvokeResult(SNACC::InvokeResult invokeResult, SNACC::ROSEInvoke& invoke, SnaccInvokeContext& ctx, std::string& strResponse, SNACC::AsnType* pResult, SNACC::AsnType* pError) { - if (pInvoke && (AsnIntType)pInvoke->invokeID == 99999) + if ((AsnIntType)invoke.invokeID == 99999) { strResponse.clear(); ctx.m_bResponseIsError = false; @@ -1890,22 +1799,19 @@ long SnaccROSEBase::HandleOnInvokeResult(SNACC::InvokeResult invokeResult, const switch (invokeResult) { case InvokeResult::returnResult: - return EncodeResult(pInvoke->invokeID, pResult, strResponse); + return EncodeResult(invoke.invokeID, pResult, strResponse); case InvokeResult::returnError: ctx.m_bResponseIsError = true; - return EncodeError(pInvoke->invokeID, pError, strResponse); + return EncodeError(invoke.invokeID, pError, strResponse); default: return ctx.m_lRejectResult ? ctx.m_lRejectResult : ROSE_REJECT_FUNCTIONMISSING; } } -long SnaccROSEBase::DecodeInvoke(const SNACC::ROSEMessage* pMessage, SNACC::AsnType* argument) +long SnaccROSEBase::DecodeInvoke(SNACC::ROSEMessage& message, SNACC::AsnType* argument) { - if (!pMessage || !pMessage->invoke) - return ROSE_RE_DECODE_FAILED; - - auto* pInvoke = pMessage->invoke; - if (!pInvoke->argument) + auto& invoke = *message.invoke; + if (!invoke.argument) { if (argument->mayBeEmpty()) return ROSE_NOERROR; @@ -1917,48 +1823,48 @@ long SnaccROSEBase::DecodeInvoke(const SNACC::ROSEMessage* pMessage, SNACC::AsnT try { - if (pInvoke->argument->anyBuf) + if (invoke.argument->anyBuf) { AsnLen len = 0; - argument->BDec(*pInvoke->argument->anyBuf, len); + argument->BDec(*invoke.argument->anyBuf, len); // Special to log the *full* ROSE Message in json // While for JSON Transport we can simply decode the full message on BER we need to decode the envelop and the payload // // We log the plain received BER payload in OnBinaryDataBlock and the fully decoded message here - // To be able to do this we need to put the decoded payload (here result) into the pResponseMsg and then log it + // To be able to do this we need to put the decoded payload (here result) into the response message and then log it // We only do this if JSON logging is enabled int logLevel = (int)GetLogLevel(false); if (logLevel & ((int)SNACC::EAsnLogLevel::JSON | (int)SNACC::EAsnLogLevel::JSON_ALWAYS_PRETTY_PRINTED)) { // Backup the original response object inside the response message - AsnAny* pOriginalArgument = pInvoke->argument; + AsnAny* pOriginalArgument = invoke.argument; // Set the decoded result object into the response to be able to fully log the whole message - pInvoke->argument = new AsnAny(); - pInvoke->argument->value = argument; + invoke.argument = new AsnAny(); + invoke.argument->value = argument; // Get the name of the called operation for logging std::string strOperationName; const char* szOperationName = nullptr; - if (pInvoke->operationName) + if (invoke.operationName) { - strOperationName = pInvoke->operationName->getUTF8(); + strOperationName = invoke.operationName->getUTF8(); szOperationName = strOperationName.c_str(); } if (!szOperationName) - szOperationName = SnaccRoseOperationLookup::LookUpName(pInvoke->operationID); - LogTransportData(false, SNACC::TransportEncoding::BER, szOperationName, nullptr, 0, pMessage, nullptr); + szOperationName = SnaccRoseOperationLookup::LookUpName(invoke.operationID); + LogTransportData(false, SNACC::TransportEncoding::BER, szOperationName, nullptr, 0, &message, nullptr); // As we hand back the result object to the outer world (function argument) we need to set it to NULL to prevent deletion if we discard the inserted object - pInvoke->argument->value = NULL; + invoke.argument->value = NULL; // Delete the inserted result object and reset to the original response object - delete pInvoke->argument; - pInvoke->argument = pOriginalArgument; + delete invoke.argument; + invoke.argument = pOriginalArgument; } } - else if (pInvoke->argument->jsonBuf) + else if (invoke.argument->jsonBuf) { - argument->JDec(*pInvoke->argument->jsonBuf); + argument->JDec(*invoke.argument->jsonBuf); // No logging here as the full object has already been logged in OnBinaryDataBlock } else diff --git a/cpp-lib/tests/runtime_correctness_notes.md b/cpp-lib/tests/runtime_correctness_notes.md index 0630b1b..1267c3f 100644 --- a/cpp-lib/tests/runtime_correctness_notes.md +++ b/cpp-lib/tests/runtime_correctness_notes.md @@ -35,7 +35,9 @@ Primary reference points: | `StopProcessing()` | Completes pending operations, but does not block new work in practice | Shutdown must refuse new outbound work and stop inbound invoke/event dispatch | | `iTimeout == 0` telemetry | Reported as `Outcome::UNHANDLED` with `WAIT_SKIPPED` | Treated as a successful fire-and-forget dispatch, not as a failure | | Response payload decode after a valid reply envelope | Caller sees decode failure, telemetry still looks like remote result/error | Telemetry must primarily reflect the caller-visible outcome | -| `OnBinaryDataBlockResult()` decode failures | Logs and telemetry are emitted, but `OnRoseDecodeError()` parity is missing | Result-path decode failure handling should mirror `OnBinaryDataBlock()` | +| Inbound decode failures and ROSE rejects | Enforced: garbage wire is silent; targeted reject only after envelope decode | See section 5 (implemented on `feature/UCAAS-1446-rose-invoke-context-runtime-tests`) | +| `OnBinaryDataBlockResult()` decode-error hooks | `OnRoseDecodeError()` and `bAlreadyTransportLogged` parity with `OnBinaryDataBlock()` | Implemented; covered by `PublicApiSmokeTest.OnBinaryDataBlockResultDecodeErrorsInvokeHook*` | +| `ROSEMessage` ownership on inbound decode | `unique_ptr` at decode sites; `std::move` into `OnROSEMessage()` | See section 6 | ## 1. `StopProcessing()` Shutdown Contract @@ -282,81 +284,136 @@ That means: ## 4. `OnBinaryDataBlockResult()` Decode-Error Hook and Logging Parity -### Current behavior +### Status: implemented -`OnBinaryDataBlock()` invokes `OnRoseDecodeError()` on BER, JSON decode, JSON -parse, and unknown-encoding failures. It also updates `bLogTransportData` with -the actual return value from `LogTransportData()` before calling the hook. +Both inbound entry points now 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. -```780:792:cpp-lib/src/SnaccROSEBase.cpp - catch (const SnaccException& ex) - { - if (bLogTransportData) - bLogTransportData = LogTransportData(false, m_eTransportEncoding, nullptr, lpBytes, ulSize, nullptr, nullptr); +### Tests that enforce this - SJson::Value error; - error["exception"] = ex.what(); - error["method"] = __FUNCTION__; - error["error"] = (int)ex.m_errorCode; - std::string strError = getPrettyPrinted(error); - PrintJSONToLog(false, true, nullptr, strError.c_str(), strError.length()); +- `PublicApiSmokeTest.OnBinaryDataBlockResultDecodeErrorsInvokeHookBer` +- `PublicApiSmokeTest.OnBinaryDataBlockResultDecodeErrorsInvokeHookJson` - OnRoseDecodeError(bLogTransportData, SNACC::TransportEncoding::BER, lpBytes, ulSize, ex.what()); -``` +### Remaining maintenance note -`OnBinaryDataBlockResult()` performs similar logging and telemetry work, but it -never calls `OnRoseDecodeError()` in the corresponding failure cases. - -```493:503:cpp-lib/src/SnaccROSEBase.cpp - catch (const SnaccException& ex) - { - if (bLogTransportData) - LogTransportData(false, m_eTransportEncoding, nullptr, lpBytes, lSize, nullptr, nullptr); - - SJson::Value error; - error["exception"] = ex.what(); - error["method"] = __FUNCTION__; - error["error"] = (int)ex.m_errorCode; - std::string strError = getPrettyPrinted(error); - PrintJSONToLog(false, true, nullptr, strError.c_str(), strError.length()); -``` +Hook and logging behavior is duplicated between `OnBinaryDataBlock()` and +`OnBinaryDataBlockResult()`. A shared helper would reduce future drift, but +parity itself is no longer an open gap. -### Why this is inconsistent +## 5. Inbound Decode Layers, Reject Policy, and BER vs JSON -- `OnRoseDecodeError()` is the dedicated extension point for decode failures. -- Result-path decode failures are still decode failures, but the override hook is - bypassed there. -- Consumers overriding that hook cannot get symmetric behavior for the two - inbound entry points. -- The `bAlreadyTransportLogged` signal is only maintained on the - `OnBinaryDataBlock()` side. +### Why BER and JSON are not symmetric at the wire layer -### Intended behavior +**BER** is decoded incrementally as nested TLVs. The runtime can fail at +different depths on the same buffer: -`OnBinaryDataBlockResult()` should mirror `OnBinaryDataBlock()` for decode -failure handling unless there is an explicitly documented reason not to. +1. **Wire garbage** — not even a decodable `ROSEMessage` (for example truncated + tag/length). +2. **Envelope incomplete** — some bytes consumed, but `ROSEMessage::BDec` did not + finish; generated CHOICE `choiceId` must not be trusted (codegen defers + `choiceId` until the selected arm decodes successfully). +3. **Envelope OK, payload bad** — invoke envelope is valid; operation argument + decode fails later in `OnInvokeMessage`. -That means: +**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: -1. invoke `OnRoseDecodeError()` for comparable decode-failure classes -2. pass the real `bAlreadyTransportLogged` state derived from - `LogTransportData()` -3. preserve any legitimate differences in reply-specific protocol handling, but - not differences in error-hook visibility +- If parse fails, there is no `SJson::Value` tree 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::JDec` run field-by-field (layer 2 + above). -### Tests that should enforce this later +So BER admits **layered** failure classification at runtime; JSON only admits +layers **after** syntactically valid JSON exists. -- A test endpoint overriding `OnRoseDecodeError()` sees BER result-path decode - failures. -- The same endpoint sees JSON result-path decode failures. -- The hook receives the same "already transport logged" semantics as the normal - inbound path. +### Intended reject policy (implemented) -### Likely code paths to change +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). + +### Intentional asymmetry: `OnBinaryDataBlock()` vs `OnBinaryDataBlockResult()` + +| 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. + +**Cleanup still open:** `OnBinaryDataBlockResult()` catch blocks still contain +legacy reject branches gated on `bRoseEnvelopeDecoded && invoke`. They do not fire +for normal garbage or malformed replies, but should be removed for strict asymmetry +(see leftovers below). +### Tests that enforce this + +- `InvokeContextRuntimeTest.UnparsableInboundDoesNotReachHandlerBer` +- `InvokeContextRuntimeTest.UnparsableInboundDoesNotReachHandlerJson` +- CHOICE `choiceId` deferral: `compiler/back-ends/c++-gen/gen-code.c` (regenerated + `SNACCROSE.cpp`) + +### Likely code paths + +- `SnaccROSEBase::OnBinaryDataBlock()` +- `SnaccROSEBase::OnBinaryDataBlockResult()` +- `SnaccROSEBase::OnInvokeMessage()` (argument-layer rejects) +- `compiler/back-ends/c++-gen/gen-code.c` (CHOICE decode / `choiceId`) + +## 6. `ROSEMessage` Ownership on Inbound Decode Paths + +### Contract (implemented) + +Inbound decode paths allocate with `std::make_unique()`. 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`: + +| 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 | `OnROSEMessage()` — destroyed on return | +| Matched result/error/reject | `CompletePendingOperation()` via `release()` → `SnaccROSEPendingOperation::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 | + +### Likely code paths + +- `SnaccROSEBase::OnBinaryDataBlock()` - `SnaccROSEBase::OnBinaryDataBlockResult()` -- possibly shared helper extraction between `OnBinaryDataBlock()` and - `OnBinaryDataBlockResult()` to avoid drift +- `SnaccROSEBase::OnROSEMessage()` +- `SnaccROSEBase::CompletePendingOperation()` +- `SnaccROSEPendingOperation::CompleteOperation()` ## Recommended Follow-Up Order @@ -366,5 +423,9 @@ That means: intentional behavior as a failure. 3. Fix response-payload decode telemetry so caller-visible failures and telemetry agree. -4. Bring `OnBinaryDataBlockResult()` decode-error hooks into parity with - `OnBinaryDataBlock()` to avoid continuing divergence. +4. ~~Remove legacy reject branches from `OnBinaryDataBlockResult()` decode catches~~ (done) + and fix the BER branch there (reject object missing `invokeProblem` if that + path ever fired). +5. Optionally extract shared decode-failure helpers between + `OnBinaryDataBlock()` and `OnBinaryDataBlockResult()` to avoid drift. +6. Commit branch updates; push and open PR for UCAAS-1446. diff --git a/cpp-lib/tests/test_support/sample_runtime_harness.h b/cpp-lib/tests/test_support/sample_runtime_harness.h index 75ae0c2..8e7b0ae 100644 --- a/cpp-lib/tests/test_support/sample_runtime_harness.h +++ b/cpp-lib/tests/test_support/sample_runtime_harness.h @@ -193,7 +193,7 @@ class IRuntimeModule // Returns the generated interface id used for dispatch lookup. virtual unsigned int InterfaceId() const = 0; // Hands the decoded ROSE message to the generated dispatcher owned by the module. - virtual long Dispatch(SNACC::ROSEMessage* pMsg, SnaccInvokeContext& ctx, std::string& strResponse) = 0; + virtual long Dispatch(std::unique_ptr& pMsg, SnaccInvokeContext& ctx, std::string& strResponse) = 0; }; // Test-specific invoke context that remembers both the local session owning the @@ -567,7 +567,7 @@ class RuntimeEndpoint : public SnaccROSEBase } // Generic interface-id based dispatcher used by all tests in this harness. - long OnInvoke(ROSEMessage* pMsg, SnaccInvokeContext& ctx, std::string& strResponse) override + long OnInvoke(std::unique_ptr& pMsg, SnaccInvokeContext& ctx, std::string& strResponse) override { const unsigned int interfaceId = SnaccRoseOperationLookup::LookUpInterfaceID(pMsg->invoke->operationID); const auto it = m_modules.find(interfaceId); @@ -751,7 +751,7 @@ class SettingsServiceModule : public ENetUC_Settings_ManagerROSEInterface, publi } // Calls the generated dispatcher for all settings traffic addressed to this module. - long Dispatch(SNACC::ROSEMessage* pMsg, SnaccInvokeContext& ctx, std::string& strResponse) override + long Dispatch(std::unique_ptr& pMsg, SnaccInvokeContext& ctx, std::string& strResponse) override { return ENetUC_Settings_ManagerROSE::OnInvoke(pMsg, &m_endpoint, this, ctx, strResponse); } @@ -839,7 +839,7 @@ class SettingsClientModule : public ENetUC_Settings_ManagerROSEInterface, public } // Uses the generated settings dispatcher for inbound results/events on the client. - long Dispatch(SNACC::ROSEMessage* pMsg, SnaccInvokeContext& ctx, std::string& strResponse) override + long Dispatch(std::unique_ptr& pMsg, SnaccInvokeContext& ctx, std::string& strResponse) override { return ENetUC_Settings_ManagerROSE::OnInvoke(pMsg, &m_endpoint, this, ctx, strResponse); } @@ -955,7 +955,7 @@ class EventServiceModule : public ENetUC_Event_ManagerROSEInterface, public IRun } // Calls the generated dispatcher for inbound event-manager traffic. - long Dispatch(SNACC::ROSEMessage* pMsg, SnaccInvokeContext& ctx, std::string& strResponse) override + long Dispatch(std::unique_ptr& pMsg, SnaccInvokeContext& ctx, std::string& strResponse) override { return ENetUC_Event_ManagerROSE::OnInvoke(pMsg, &m_endpoint, this, ctx, strResponse); } @@ -1022,7 +1022,7 @@ class EventClientModule : public ENetUC_Event_ManagerROSEInterface, public IRunt } // Uses the generated event-manager dispatcher for inbound notifications. - long Dispatch(SNACC::ROSEMessage* pMsg, SnaccInvokeContext& ctx, std::string& strResponse) override + long Dispatch(std::unique_ptr& pMsg, SnaccInvokeContext& ctx, std::string& strResponse) override { return ENetUC_Event_ManagerROSE::OnInvoke(pMsg, &m_endpoint, this, ctx, strResponse); } From eba0fbffb7b04d2cbef55936a190767191fd28a0 Mon Sep 17 00:00:00 2001 From: Jan Fellner Date: Tue, 7 Jul 2026 09:48:39 +0200 Subject: [PATCH 06/10] UCAAS-1446 Harden outbound encode ownership and document ROSE APIs. Replace manual prevent-delete nulling in Send/EncodeResult/EncodeError/EncodeReject with RAII helpers, add living-documentation guidance, and align header and correctness-note docs with unique_ptr ownership. Co-authored-by: Cursor --- .cursor/rules/living-documentation.mdc | 50 +++ AGENTS.md | 1 + cpp-lib/include/SnaccROSEBase.h | 30 +- cpp-lib/include/SnaccROSEInterfaces.h | 8 +- cpp-lib/src/SnaccROSEBase.cpp | 360 ++++++++++++++------- cpp-lib/tests/runtime_correctness_notes.md | 13 +- 6 files changed, 328 insertions(+), 134 deletions(-) create mode 100644 .cursor/rules/living-documentation.mdc diff --git a/.cursor/rules/living-documentation.mdc b/.cursor/rules/living-documentation.mdc new file mode 100644 index 0000000..2387b69 --- /dev/null +++ b/.cursor/rules/living-documentation.mdc @@ -0,0 +1,50 @@ +--- +description: Document new types and members — purpose, usage, problem solved +alwaysApply: true +--- + +# Living Documentation for Types and Members + +Good code documents **why** it exists, not only **what** it does. When you add or materially change a type (class, struct, enum, type alias) or a non-obvious member, add brief comments at the definition site. + +## Required for new/changed definitions + +For each new type or group of related helpers, document: + +1. **Purpose** — what problem this solves in the subsystem. +2. **Usage** — who calls it, lifetime/ownership expectations, borrow vs own. +3. **Scope** — file-local helper vs public API; thread/context constraints if relevant. + +For non-obvious members (handles, caches, flags, borrowed pointers, `unique_ptr` sinks), add a comment **on the line above the member** unless the type-level comment already makes it obvious. Do not use trailing end-of-line comments for member documentation. + +## Style in this repo + +- **Public headers** (`cpp-lib/include/`, generated interfaces): use existing `/*! ... */` block style where siblings do. +- **Implementation-only helpers** (anonymous namespaces, `.cpp` RAII guards): `//` block above the type; `//` line above each non-obvious member. +- Match **line density** of the surrounding file — do not add blank lines after every line. +- Do not restate the identifier in prose (“`m_foo` is the foo”) unless it disambiguates ownership or invariants. +- **Document each type or member in place.** Do not defer to another definition with “same as”, “like”, “see”, or “mirrors” — if one changes, cross-references go stale. Repeat purpose, usage, and ownership at every site that needs it. + +## Examples + +```cpp +// Snapshot of inbound invoke metadata after envelope decode. +class InboundInvokeRejectContext { +public: + // Invoke ID from the decoded envelope (99999 = event). + AsnIntType m_invokeId = 0; +}; + +/*! Owned inbound response matched to a pending outbound invoke. */ +std::unique_ptr m_pAnswerMessage; +``` + +## When comments are not required + +- Trivial locals, loop indices, obvious parameters (`invokeID`, `strResponse`). +- Generated code unless the generator template is being changed. +- Renames with unchanged semantics. + +## Agent workflow + +When introducing structures or members as part of a fix or feature, **include documentation in the same change**. Do not defer “comment pass” to a follow-up unless the user explicitly asks for code-only first. diff --git a/AGENTS.md b/AGENTS.md index fd0f822..af7b54e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -97,6 +97,7 @@ Requirements and IDE-specific steps are in [ReadMe.md](ReadMe.md). CMake minimum - Prefer the smallest correct change; match surrounding code style. - Generated output shape is defined by ASN.1 inputs plus the relevant `compiler/back-ends/*-gen` implementation — read both before changing behavior. - When documentation and code disagree, verify against `samples/` and tests before updating docs. +- **Document new types and members** at the definition site (purpose, usage, ownership). See [.cursor/rules/living-documentation.mdc](.cursor/rules/living-documentation.mdc). ### Tests define the public API (hard constraint) diff --git a/cpp-lib/include/SnaccROSEBase.h b/cpp-lib/include/SnaccROSEBase.h index 6377d54..66943b3 100644 --- a/cpp-lib/include/SnaccROSEBase.h +++ b/cpp-lib/include/SnaccROSEBase.h @@ -57,7 +57,8 @@ class SnaccROSEPendingOperation const unsigned int m_uiOperationID{}; const std::string m_strOperationName; - /*! The answer message. */ + /*! Owned inbound response matched to a pending outbound invoke. + Transferred from CompletePendingOperation; destroyed with the pending op. */ std::unique_ptr m_pAnswerMessage; /*! Error code (one of the ROSE_ error codes. */ @@ -69,7 +70,8 @@ class SnaccROSEPendingOperation /*! Outbound invoke telemetry tracked alongside the pending completion state. */ std::shared_ptr m_pTelemetry; - /*! Async Operation completed. */ + /*! Async Operation completed. + pAnswerMessage transfers ownership of a decoded response when present. */ void CompleteOperation(long lRoseResult, std::unique_ptr pAnswerMessage = {}, size_t stResponseData = 0); void FinalizeTelemetry(long lFinalRoseResult, std::shared_ptr pctx); @@ -276,7 +278,7 @@ class SnaccROSEBase : public SnaccROSESender, public SnaccTelemetryCallback * Encodes the result or error from an OnInvoke request. Retrieves the result or error from the response * * invokeResult - the result of the OnInvoke method - * pInvoke - the original invoke + * invoke - the original inbound invoke envelope (borrowed from owned ROSEMessage) * ctx - contextual data for the invoke * strResponse - the encoded result to put it on the transport * pResult - the result object (Base type pointer, the caller of the invoke provides the proper type) @@ -289,7 +291,7 @@ class SnaccROSEBase : public SnaccROSESender, public SnaccTelemetryCallback * to the caller, for example to propagate connection-specific session data. * * lRoseResult - the current ROSE result code - * pResponseMsg - the raw response message that was received + * responseMsg - owned pending-op response; may be mutated for BER JSON logging * result - the decoded result payload in case a result response is received * error - the decoded error payload in case an error response is received * ctx - contextual data for the invoke @@ -311,9 +313,9 @@ class SnaccROSEBase : public SnaccROSESender, public SnaccTelemetryCallback std::shared_ptr CreateInvokeContext(const SnaccInvokeContextInit& init) override; /** - * Decodes an invoke and properly handles logging for it + * Decodes an invoke argument and optionally logs the full message (BER JSON). * - * pInvokeMessage - the invoke message as provided from the other side + * invokeMessage - inbound invoke ROSEMessage; borrow only for decode/logging * argument - the argument object (Base type pointer, the caller of the provides the proper type) */ long DecodeInvoke(SNACC::ROSEMessage& invokeMessage, SNACC::AsnType* argument) override; @@ -326,6 +328,8 @@ class SnaccROSEBase : public SnaccROSESender, public SnaccTelemetryCallback /*! The functions and events. The implementation of this functions is contained in the generated code from the esnacc. */ + /*! Generated dispatch entry; borrows pMsg for the synchronous handler call. + Ownership remains with OnInvokeMessage until it returns. */ virtual long OnInvoke(std::unique_ptr& pMsg, SnaccInvokeContext& ctx, std::string& strResponse) = 0; /* Function is called when a received data Packet cannot be decoded (invalid Rose Message) @@ -352,13 +356,13 @@ class SnaccROSEBase : public SnaccROSESender, public SnaccTelemetryCallback void OnInvokeProcessed(std::shared_ptr data) override; private: - /*! The ROSE component messages. - These are called from the OnROSEMessage. - Do not dete the parameters. The functions are called - before the CompleteOperation */ + /*! Inbound invoke/event dispatch; takes ownership of the decoded message. */ virtual void OnInvokeMessage(std::unique_ptr pMessage, unsigned long lMessageSize); + /*! Telemetry hook for matched inbound result; borrows arm of owned message. */ virtual void OnResultMessage(SNACC::ROSEResult& result, unsigned long lMessageSize); + /*! Telemetry hook for matched inbound error; borrows arm of owned message. */ virtual void OnErrorMessage(SNACC::ROSEError& error, unsigned long lMessageSize); + /*! Telemetry hook for inbound reject; borrows arm of owned message. */ virtual void OnRejectMessage(SNACC::ROSEReject& reject, unsigned long lMessageSize); // The central process wide telemetry callback @@ -385,9 +389,9 @@ class SnaccROSEBase : public SnaccROSESender, public SnaccTelemetryCallback SnaccROSEPendingOperation& AddPendingOperation(int invokeID, unsigned int uiOperationID, const char* szOperationName); void RemovePendingOperation(int invokeID); - /*! Pending Operation result has been received. - Attention: The pMessage Objekt will be taken over from the - SnaccROSEPendingOperation Object and deleted in the end. */ + /*! Matches an inbound response to a pending outbound invoke by invokeID. + Transfers pMessage into SnaccROSEPendingOperation when found; otherwise + destroys it when this function returns. */ bool CompletePendingOperation(int invokeID, std::unique_ptr pMessage, unsigned long ulMessageSize); bool GetPendingOperationTelemetryInfo(int invokeID, unsigned int& uiOperationID, std::string& strOperationName); static long GetRejectResultCode(const SNACC::ROSEReject* pReject); diff --git a/cpp-lib/include/SnaccROSEInterfaces.h b/cpp-lib/include/SnaccROSEInterfaces.h index ef63afc..0e7a976 100644 --- a/cpp-lib/include/SnaccROSEInterfaces.h +++ b/cpp-lib/include/SnaccROSEInterfaces.h @@ -222,10 +222,10 @@ class SnaccROSESender * Handles the response payload of the SendInvoke method. Retrieves the result or error from the response * * lRoseResult - the result of the SendInvoke method - * pResponseMsg - the response message as provided by the SendInvoke method + * responseMsg - owned pending-op response; may be mutated for BER JSON logging * result - the result object (Base type pointer, the caller of the invoke provides the proper type) * error - the error object (Base type pointer, the caller of the invoke provides the proper type) - * pCtx - contextual data for the invoke + * ctx - contextual data for the invoke */ virtual long HandleInvokeResult(long lRoseResult, SNACC::ROSEMessage& responseMsg, SNACC::AsnType* result, SNACC::AsnType* error, SnaccInvokeContext& ctx) = 0; @@ -233,7 +233,7 @@ class SnaccROSESender * Encodes the result or error from an OnInvoke request. Retrieves the result or error from the response * * invokeResult - the result of the OnInvoke method - * pInvoke - the original invoke + * invoke - the original inbound invoke envelope (borrowed from owned ROSEMessage) * ctx - contextual data for the invoke * strResponse - the encoded result to put it on the transport * result - the result object (Base type pointer, the caller of the invoke provides the proper type) @@ -244,7 +244,7 @@ class SnaccROSESender /** * Decodes an invoke and properly handles logging for it * - * pInvokeMessage - the invoke message as provided from the other side + * invokeMessage - inbound invoke ROSEMessage; borrow only for decode/logging * argument - the argument object (Base type pointer, the caller of the provides the proper type) */ virtual long DecodeInvoke(SNACC::ROSEMessage& invokeMessage, SNACC::AsnType* argument) = 0; diff --git a/cpp-lib/src/SnaccROSEBase.cpp b/cpp-lib/src/SnaccROSEBase.cpp index f7c22ec..1d26a40 100644 --- a/cpp-lib/src/SnaccROSEBase.cpp +++ b/cpp-lib/src/SnaccROSEBase.cpp @@ -26,13 +26,21 @@ namespace return ""; } + // Snapshot of inbound invoke metadata taken immediately after a successful + // ROSE envelope decode. Decode catch blocks must not read ROSEMessage after + // std::move into dispatch; this struct drives targeted mistypedArgument rejects + // and decode-failure telemetry instead. class InboundInvokeRejectContext { public: + // Invoke ID from the decoded envelope (99999 = event, no reject). AsnIntType m_invokeId = 0; + // Registered operation id for telemetry and reject encoding. unsigned int m_uiOperationID = 0; + // Copied lookup name; stable storage for reject/telemetry strings. std::string m_strOperationName; + // Returns a snapshot when message is a decoded invoke; nullopt otherwise. static std::optional TryFrom(const SNACC::ROSEMessage& message) { if (message.choiceId != ROSEMessage::invokeCid || !message.invoke) @@ -58,6 +66,8 @@ namespace } }; + // Telemetry and optional reject outcome produced by inbound decode failure + // handling. Filled by HandleInboundRoseDecodeFailure for OnInvokeProcessed. struct InboundDecodeFailureResult { unsigned int uiOperationID = 0; @@ -66,6 +76,9 @@ namespace long lTelemetryResult = ROSE_RE_DECODE_FAILED; }; + // Shared inbound decode-failure path for OnBinaryDataBlock and + // OnBinaryDataBlockResult. Optionally sends mistypedArgument when rejectCtx + // is present; response entry point passes bSendReject=false. InboundDecodeFailureResult HandleInboundRoseDecodeFailure( SnaccROSEBase& rose, const std::optional& rejectCtx, @@ -109,6 +122,182 @@ namespace result.lTelemetryResult = lRejectResult; return result; } + + // Binds a stack-owned ROSEReject into a ROSEMessage for EncodeReject without + // transferring ownership. Detaches in ~dtor so ~ROSEMessage never deletes the + // caller's reject (including on encode exceptions). + class RoseEncodeRejectBorrow final + { + public: + RoseEncodeRejectBorrow(SNACC::ROSEMessage& message, SNACC::ROSEReject& reject) + { + message.choiceId = ROSEMessage::rejectCid; + message.reject = &reject; + m_pMessage = &message; + } + + ~RoseEncodeRejectBorrow() noexcept + { + if (m_pMessage) + m_pMessage->reject = nullptr; + } + + RoseEncodeRejectBorrow(const RoseEncodeRejectBorrow&) = delete; + RoseEncodeRejectBorrow& operator=(const RoseEncodeRejectBorrow&) = delete; + + private: + // ROSEMessage whose reject arm we borrow into for encoding. + SNACC::ROSEMessage* m_pMessage = nullptr; + }; + + // Builds a stack ROSEResult inside a ROSEMessage for outbound EncodeResult. + // Owns encode-side allocations; borrows caller AsnType payload. Detach in + // ~dtor prevents ~ROSEMessage from deleting stack envelopes or caller values. + class RoseEncodeResultEnvelope final + { + public: + RoseEncodeResultEnvelope(unsigned int uiInvokeID, SNACC::AsnType* pResult, const wchar_t* szSessionID) + { + m_result.invokeID = uiInvokeID; + m_result.result = new ROSEResultSeq; + m_result.result->resultValue = 0; + m_result.result->result.value = pResult; + if (szSessionID) + m_result.sessionID = new UTF8String(szSessionID); + m_message.choiceId = ROSEMessage::resultCid; + m_message.result = &m_result; + } + + ~RoseEncodeResultEnvelope() noexcept + { + detach(); + } + + SNACC::ROSEMessage& message() noexcept + { + return m_message; + } + + RoseEncodeResultEnvelope(const RoseEncodeResultEnvelope&) = delete; + RoseEncodeResultEnvelope& operator=(const RoseEncodeResultEnvelope&) = delete; + + private: + void detach() noexcept + { + if (m_message.result == &m_result) + m_message.result = nullptr; + if (m_result.result) + m_result.result->result.value = nullptr; + } + + // Stack envelope; owns ROSEResultSeq and optional sessionID. + SNACC::ROSEResult m_result; + // Encode view; borrows m_result until detach(). + SNACC::ROSEMessage m_message; + }; + + // Builds a stack ROSEError inside a ROSEMessage for outbound EncodeError. + // Owns encode-side allocations (AsnAny wrapper, optional sessionID); borrows + // caller AsnType payload. Detach in ~dtor prevents ~ROSEMessage from deleting + // stack envelopes or caller values (including on encode exceptions). + class RoseEncodeErrorEnvelope final + { + public: + RoseEncodeErrorEnvelope(unsigned int uiInvokeID, SNACC::AsnType* pError, const wchar_t* szSessionID) + { + m_error.invokedID = uiInvokeID; + m_error.error_value = 0; + m_error.error = new AsnAny(); + m_error.error->value = pError; + if (szSessionID) + m_error.sessionID = new UTF8String(szSessionID); + m_message.choiceId = ROSEMessage::errorCid; + m_message.error = &m_error; + } + + ~RoseEncodeErrorEnvelope() noexcept + { + detach(); + } + + SNACC::ROSEMessage& message() noexcept + { + return m_message; + } + + RoseEncodeErrorEnvelope(const RoseEncodeErrorEnvelope&) = delete; + RoseEncodeErrorEnvelope& operator=(const RoseEncodeErrorEnvelope&) = delete; + + private: + void detach() noexcept + { + if (m_message.error == &m_error) + m_message.error = nullptr; + if (m_error.error) + m_error.error->value = nullptr; + } + + // Stack envelope; owns AsnAny wrapper and optional sessionID. + SNACC::ROSEError m_error; + // Encode view; borrows m_error until detach(). + SNACC::ROSEMessage m_message; + }; + + // Binds caller-owned ROSEInvoke into a stack ROSEMessage for Send encoding. + // Detaches invoke pointer in ~dtor so ~ROSEMessage does not delete pInvoke. + class ScopedEncodeInvokeBorrow final + { + public: + ScopedEncodeInvokeBorrow(SNACC::ROSEMessage& message, SNACC::ROSEInvoke& invoke) + : m_message(message) + { + m_message.choiceId = ROSEMessage::invokeCid; + m_message.invoke = &invoke; + } + + ~ScopedEncodeInvokeBorrow() noexcept + { + m_message.invoke = nullptr; + } + + ScopedEncodeInvokeBorrow(const ScopedEncodeInvokeBorrow&) = delete; + ScopedEncodeInvokeBorrow& operator=(const ScopedEncodeInvokeBorrow&) = delete; + + private: + // Outbound invoke ROSEMessage being encoded. + SNACC::ROSEMessage& m_message; + }; + + // JSON Send requires operationName on the invoke; adds a temporary name only + // for encoding when the caller did not set one, and removes it on scope exit. + class ScopedInvokeOperationName final + { + public: + ScopedInvokeOperationName(SNACC::ROSEInvoke& invoke, const char* szOperationName) + { + if (!invoke.operationName && szOperationName) + { + m_pInvoke = &invoke; + invoke.operationName = UTF8String::CreateNewFromASCII(szOperationName); + } + } + + ~ScopedInvokeOperationName() noexcept + { + if (m_pInvoke && m_pInvoke->operationName) + { + delete m_pInvoke->operationName; + m_pInvoke->operationName = nullptr; + } + } + + ScopedInvokeOperationName(const ScopedInvokeOperationName&) = delete; + ScopedInvokeOperationName& operator=(const ScopedInvokeOperationName&) = delete; + + private: + // Set only when this scope allocated operationName on the invoke. + SNACC::ROSEInvoke* m_pInvoke = nullptr; + }; } // namespace SnaccInvokeContextInit::SnaccInvokeContextInit(SnaccInvokeDirection direction, SNACC::ROSEInvoke* pInvoke, const char* szOperationName /*= nullptr*/) @@ -976,8 +1165,7 @@ long SnaccROSEBase::EncodeReject(SNACC::ROSEReject* preject, std::string& strRes long lRoseResult = ROSE_NOERROR; ROSEMessage rejectMsg; - rejectMsg.choiceId = ROSEMessage::rejectCid; - rejectMsg.reject = preject; + const RoseEncodeRejectBorrow rejectBorrow(rejectMsg, *preject); // encode now. if (m_eTransportEncoding == SNACC::TransportEncoding::BER) @@ -1018,9 +1206,6 @@ long SnaccROSEBase::EncodeReject(SNACC::ROSEReject* preject, std::string& strRes throw std::runtime_error("invalid m_eTransportEncoding"); } - // prevent delete of preject... - rejectMsg.reject = 0; - return lRoseResult; } @@ -1259,8 +1444,7 @@ long SnaccROSEBase::Send(SNACC::ROSEInvoke* pInvoke, const char* szOperationName *pstRequestData = 0; ROSEMessage invokeMsg; - invokeMsg.choiceId = ROSEMessage::invokeCid; - invokeMsg.invoke = pInvoke; + const ScopedEncodeInvokeBorrow invokeBorrow(invokeMsg, *pInvoke); if (m_eTransportEncoding == SNACC::TransportEncoding::BER) { @@ -1274,9 +1458,7 @@ long SnaccROSEBase::Send(SNACC::ROSEInvoke* pInvoke, const char* szOperationName } else if (m_eTransportEncoding == SNACC::TransportEncoding::JSON || m_eTransportEncoding == SNACC::TransportEncoding::JSON_NO_HEADING) { - // The mobiles currently rely on the operationName so we need to fill it if it is missing here - if (!pInvoke->operationName) - pInvoke->operationName = UTF8String::CreateNewFromASCII(szOperationName); + const ScopedInvokeOperationName operationName(*pInvoke, szOperationName); std::string strData = GetEncoded(m_eTransportEncoding, &invokeMsg); @@ -1300,9 +1482,6 @@ long SnaccROSEBase::Send(SNACC::ROSEInvoke* pInvoke, const char* szOperationName throw std::runtime_error("invalid m_eTransportEncoding"); } - // prevent autodelete of pInvoke - invokeMsg.invoke = NULL; - return lRoseResult; } @@ -1510,65 +1689,45 @@ long SnaccROSEBase::DecodeResponse(const SNACC::ROSEMessage& response, SNACC::RO long SnaccROSEBase::EncodeResult(unsigned int uiInvokeID, SNACC::AsnType* pResult, std::string& strResponse, const wchar_t* szSessionID /*= nullptr*/) { long lRoseResult = ROSE_NOERROR; + RoseEncodeResultEnvelope encode(uiInvokeID, pResult, szSessionID); - ROSEResult result; - result.invokeID = uiInvokeID; - result.result = new ROSEResultSeq; - result.result->resultValue = 0; - result.result->result.value = pResult; - if (szSessionID) - result.sessionID = new UTF8String(szSessionID); - + if (m_eTransportEncoding == SNACC::TransportEncoding::BER) { - ROSEMessage ResultMsg; - ResultMsg.choiceId = ROSEMessage::resultCid; - ResultMsg.result = &result; - - // encode now. - if (m_eTransportEncoding == SNACC::TransportEncoding::BER) - { - AsnBuf OutBuf; - AsnLen BytesEncoded = ResultMsg.BEnc(OutBuf); - - OutBuf.ResetMode(); - OutBuf.GetSeg(strResponse, BytesEncoded); + AsnBuf OutBuf; + AsnLen BytesEncoded = encode.message().BEnc(OutBuf); - LogTransportData(true, m_eTransportEncoding, nullptr, strResponse.c_str(), BytesEncoded, &ResultMsg, nullptr); - } - else if (m_eTransportEncoding == SNACC::TransportEncoding::JSON || m_eTransportEncoding == SNACC::TransportEncoding::JSON_NO_HEADING) - { - auto value = ResultMsg.JEnc(); + OutBuf.ResetMode(); + OutBuf.GetSeg(strResponse, BytesEncoded); - int logLevel = (int)GetLogLevel(true); - if (logLevel & (int)EAsnLogLevel::JSON || logLevel & (int)EAsnLogLevel::JSON_ALWAYS_PRETTY_PRINTED) - strResponse = getPrettyPrinted(value); - else - { - SJson::FastWriter writer; - strResponse = writer.write(value); - } + LogTransportData(true, m_eTransportEncoding, nullptr, strResponse.c_str(), BytesEncoded, &encode.message(), nullptr); + } + else if (m_eTransportEncoding == SNACC::TransportEncoding::JSON || m_eTransportEncoding == SNACC::TransportEncoding::JSON_NO_HEADING) + { + auto value = encode.message().JEnc(); - std::string strPrefix; - if (m_eTransportEncoding == SNACC::TransportEncoding::JSON) - lRoseResult = GetJsonLengthPrefix(strResponse, strPrefix); - if (lRoseResult == ROSE_NOERROR) - { - LogTransportData(true, m_eTransportEncoding, nullptr, strResponse.c_str(), strResponse.length(), &ResultMsg, nullptr); - if (!strPrefix.empty()) - strResponse.insert(0, strPrefix); - } - } + int logLevel = (int)GetLogLevel(true); + if (logLevel & (int)EAsnLogLevel::JSON || logLevel & (int)EAsnLogLevel::JSON_ALWAYS_PRETTY_PRINTED) + strResponse = getPrettyPrinted(value); else { - throw std::runtime_error("invalid m_eTransportEncoding"); + SJson::FastWriter writer; + strResponse = writer.write(value); } - // prevent delete of presult... - ResultMsg.result = nullptr; + std::string strPrefix; + if (m_eTransportEncoding == SNACC::TransportEncoding::JSON) + lRoseResult = GetJsonLengthPrefix(strResponse, strPrefix); + if (lRoseResult == ROSE_NOERROR) + { + LogTransportData(true, m_eTransportEncoding, nullptr, strResponse.c_str(), strResponse.length(), &encode.message(), nullptr); + if (!strPrefix.empty()) + strResponse.insert(0, strPrefix); + } + } + else + { + throw std::runtime_error("invalid m_eTransportEncoding"); } - - // prevent delete of value... - result.result->result.value = nullptr; return lRoseResult; } @@ -1576,64 +1735,45 @@ long SnaccROSEBase::EncodeResult(unsigned int uiInvokeID, SNACC::AsnType* pResul long SnaccROSEBase::EncodeError(unsigned int uiInvokeID, SNACC::AsnType* pError, std::string& strResponse, const wchar_t* szSessionID /*= nullptr*/) { long lRoseResult = ROSE_NOERROR; + RoseEncodeErrorEnvelope encode(uiInvokeID, pError, szSessionID); - ROSEError error; - error.invokedID = uiInvokeID; - error.error_value = 0; - error.error = new AsnAny(); - error.error->value = pError; - if (szSessionID) - error.sessionID = new UTF8String(szSessionID); - + if (m_eTransportEncoding == SNACC::TransportEncoding::BER) { - ROSEMessage errorMsg; - errorMsg.choiceId = ROSEMessage::errorCid; - errorMsg.error = &error; + AsnBuf OutBuf; + AsnLen BytesEncoded = encode.message().BEnc(OutBuf); - // encode now. - if (m_eTransportEncoding == SNACC::TransportEncoding::BER) - { - AsnBuf OutBuf; - AsnLen BytesEncoded = errorMsg.BEnc(OutBuf); + OutBuf.ResetMode(); + OutBuf.GetSeg(strResponse, BytesEncoded); - OutBuf.ResetMode(); - OutBuf.GetSeg(strResponse, BytesEncoded); + LogTransportData(true, m_eTransportEncoding, nullptr, strResponse.c_str(), strResponse.length(), &encode.message(), nullptr); + } + else if (m_eTransportEncoding == SNACC::TransportEncoding::JSON || m_eTransportEncoding == SNACC::TransportEncoding::JSON_NO_HEADING) + { + auto value = encode.message().JEnc(); - LogTransportData(true, m_eTransportEncoding, nullptr, strResponse.c_str(), strResponse.length(), &errorMsg, nullptr); - } - else if (m_eTransportEncoding == SNACC::TransportEncoding::JSON || m_eTransportEncoding == SNACC::TransportEncoding::JSON_NO_HEADING) + int logLevel = (int)GetLogLevel(true); + if (logLevel & (int)EAsnLogLevel::JSON || logLevel & (int)EAsnLogLevel::JSON_ALWAYS_PRETTY_PRINTED) + strResponse = getPrettyPrinted(value); + else { - auto value = errorMsg.JEnc(); - - int logLevel = (int)GetLogLevel(true); - if (logLevel & (int)EAsnLogLevel::JSON || logLevel & (int)EAsnLogLevel::JSON_ALWAYS_PRETTY_PRINTED) - strResponse = getPrettyPrinted(value); - else - { - SJson::FastWriter writer; - strResponse = writer.write(value); - } - - std::string strPrefix; - if (m_eTransportEncoding == SNACC::TransportEncoding::JSON) - lRoseResult = GetJsonLengthPrefix(strResponse, strPrefix); - if (lRoseResult == ROSE_NOERROR) - { - LogTransportData(true, m_eTransportEncoding, nullptr, strResponse.c_str(), strResponse.length(), &errorMsg, nullptr); - if (!strPrefix.empty()) - strResponse.insert(0, strPrefix); - } + SJson::FastWriter writer; + strResponse = writer.write(value); } - else + + std::string strPrefix; + if (m_eTransportEncoding == SNACC::TransportEncoding::JSON) + lRoseResult = GetJsonLengthPrefix(strResponse, strPrefix); + if (lRoseResult == ROSE_NOERROR) { - throw std::runtime_error("invalid m_eTransportEncoding"); + LogTransportData(true, m_eTransportEncoding, nullptr, strResponse.c_str(), strResponse.length(), &encode.message(), nullptr); + if (!strPrefix.empty()) + strResponse.insert(0, strPrefix); } - // prevent delete of perror... - errorMsg.error = nullptr; } - - // prevent delete of value... - error.error->value = nullptr; + else + { + throw std::runtime_error("invalid m_eTransportEncoding"); + } return lRoseResult; } diff --git a/cpp-lib/tests/runtime_correctness_notes.md b/cpp-lib/tests/runtime_correctness_notes.md index 1267c3f..8a162b6 100644 --- a/cpp-lib/tests/runtime_correctness_notes.md +++ b/cpp-lib/tests/runtime_correctness_notes.md @@ -37,7 +37,8 @@ Primary reference points: | Response payload decode after a valid reply envelope | Caller sees decode failure, telemetry still looks like remote result/error | Telemetry must primarily reflect the caller-visible outcome | | Inbound decode failures and ROSE rejects | Enforced: garbage wire is silent; targeted reject only after envelope decode | See section 5 (implemented on `feature/UCAAS-1446-rose-invoke-context-runtime-tests`) | | `OnBinaryDataBlockResult()` decode-error hooks | `OnRoseDecodeError()` and `bAlreadyTransportLogged` parity with `OnBinaryDataBlock()` | Implemented; covered by `PublicApiSmokeTest.OnBinaryDataBlockResultDecodeErrorsInvokeHook*` | -| `ROSEMessage` ownership on inbound decode | `unique_ptr` at decode sites; `std::move` into `OnROSEMessage()` | See section 6 | +| `ROSEMessage` ownership on inbound decode | `unique_ptr` at decode sites; `std::move` through dispatch | See section 6 (implemented) | +| Outbound encode / `Send()` ownership | Manual `new` + `// prevent delete` nulling on happy path | See section 7 — exception safety and fragility, not steady-state leaks on happy path | ## 1. `StopProcessing()` Shutdown Contract @@ -358,10 +359,8 @@ Hook and logging parity between the two paths is required. **Reject parity is no — the response path must not fabricate server-side rejects when a reply cannot be decoded. -**Cleanup still open:** `OnBinaryDataBlockResult()` catch blocks still contain -legacy reject branches gated on `bRoseEnvelopeDecoded && invoke`. They do not fire -for normal garbage or malformed replies, but should be removed for strict asymmetry -(see leftovers below). +**Cleanup done:** legacy reject branches were removed from `OnBinaryDataBlockResult()` +decode catches; that path is telemetry-only on decode failure. ### Tests that enforce this @@ -394,8 +393,8 @@ If dispatch throws, the outer `catch` uses the snapshot for targeted | --- | --- | | 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 | `OnROSEMessage()` — destroyed on return | -| Matched result/error/reject | `CompletePendingOperation()` via `release()` → `SnaccROSEPendingOperation::m_pAnswerMessage` | +| 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 | From d367f1194fbc016362192018ea53e31bd5675d14 Mon Sep 17 00:00:00 2001 From: Jan Fellner Date: Tue, 7 Jul 2026 10:06:59 +0200 Subject: [PATCH 07/10] UCAAS-1446 Deduplicate inbound decode failures as private class methods. Move shared OnBinaryDataBlock decode-failure handling onto SnaccROSEBase and sync runtime_correctness_notes with implemented semantics. Co-authored-by: Cursor --- cpp-lib/include/SnaccROSEBase.h | 30 ++ cpp-lib/src/SnaccROSEBase.cpp | 441 ++++++++++----------- cpp-lib/tests/runtime_correctness_notes.md | 390 +++++++++--------- 3 files changed, 425 insertions(+), 436 deletions(-) diff --git a/cpp-lib/include/SnaccROSEBase.h b/cpp-lib/include/SnaccROSEBase.h index 66943b3..d1ddb9b 100644 --- a/cpp-lib/include/SnaccROSEBase.h +++ b/cpp-lib/include/SnaccROSEBase.h @@ -33,6 +33,7 @@ namespace SNACC class InvokeProblem; class ROSEAuthRequest; class ROSEAuthResult; + class SnaccException; } // namespace SNACC /*! The SnapcROSEPendingOperation class is used to implement the function calls. @@ -365,6 +366,35 @@ class SnaccROSEBase : public SnaccROSESender, public SnaccTelemetryCallback /*! Telemetry hook for inbound reject; borrows arm of owned message. */ virtual void OnRejectMessage(SNACC::ROSEReject& reject, unsigned long lMessageSize); + // Snapshot of inbound invoke metadata taken immediately after a successful + // ROSE envelope decode. + struct InboundInvokeRejectContext + { + long m_invokeId = 0; + unsigned int m_uiOperationID = 0; + std::string m_strOperationName; + + static std::optional TryFrom(const SNACC::ROSEMessage& message); + bool CanSendReject() const; + const char* OperationNameCStr() const; + }; + + struct InboundDecodeFailureResult + { + unsigned int uiOperationID = 0; + const char* szOperationName = nullptr; + SnaccTelemetryData::Outcome outcome = SnaccTelemetryData::Outcome::UNHANDLED; + long lTelemetryResult = ROSE_RE_DECODE_FAILED; + }; + + void EmitInboundDecodeFailureTelemetry(unsigned long ulMessageSize, unsigned int uiOperationID, const char* szOperationName, SnaccTelemetryData::Outcome outcome, long lTelemetryResult); + void EmitInboundGenericDecodeFailureTelemetry(unsigned long ulMessageSize); + InboundDecodeFailureResult HandleInboundRoseDecodeFailure(const std::optional& rejectCtx, bool bSendReject, const std::string& strRejectDetails); + void HandleInboundEnvelopeSnaccDecodeFailure(SNACC::TransportEncoding transportEncoding, SNACC::TransportEncoding hookEncoding, const char* lpBytes, unsigned long ulMessageSize, bool& bLogTransportData, const SNACC::SnaccException& ex, const std::optional& rejectCtx, bool bSendReject, bool bRejectDetailsFromPrettyJson, const char* szMethod); + void HandleInboundJsonParseDecodeFailure(SNACC::TransportEncoding transportEncoding, const char* lpBytes, unsigned long ulMessageSize, bool& bLogTransportData, const std::string& parseError, const char* szMethod); + void HandleInboundUnknownEncodingDecodeFailure(const char* lpBytes, unsigned long ulMessageSize, bool& bLogTransportData); + void HandleInboundOuterDecodeFailure(unsigned long ulMessageSize, const char* szException, const char* szMethod, std::optional errorCode = std::nullopt); + // The central process wide telemetry callback static inline SnaccTelemetryCallback* m_pTelemetryCallback{}; diff --git a/cpp-lib/src/SnaccROSEBase.cpp b/cpp-lib/src/SnaccROSEBase.cpp index 1d26a40..732f4b6 100644 --- a/cpp-lib/src/SnaccROSEBase.cpp +++ b/cpp-lib/src/SnaccROSEBase.cpp @@ -9,6 +9,8 @@ #include #include +std::string getPrettyPrinted(const SJson::Value& value); + using namespace SNACC; namespace @@ -26,103 +28,6 @@ namespace return ""; } - // Snapshot of inbound invoke metadata taken immediately after a successful - // ROSE envelope decode. Decode catch blocks must not read ROSEMessage after - // std::move into dispatch; this struct drives targeted mistypedArgument rejects - // and decode-failure telemetry instead. - class InboundInvokeRejectContext - { - public: - // Invoke ID from the decoded envelope (99999 = event, no reject). - AsnIntType m_invokeId = 0; - // Registered operation id for telemetry and reject encoding. - unsigned int m_uiOperationID = 0; - // Copied lookup name; stable storage for reject/telemetry strings. - std::string m_strOperationName; - - // Returns a snapshot when message is a decoded invoke; nullopt otherwise. - static std::optional TryFrom(const SNACC::ROSEMessage& message) - { - if (message.choiceId != ROSEMessage::invokeCid || !message.invoke) - return std::nullopt; - const auto* pInvoke = message.invoke; - InboundInvokeRejectContext ctx; - ctx.m_invokeId = (AsnIntType)pInvoke->invokeID; - ctx.m_uiOperationID = pInvoke->operationID; - const char* szName = SnaccRoseOperationLookup::LookUpName(ctx.m_uiOperationID); - if (szName) - ctx.m_strOperationName = szName; - return ctx; - } - - bool CanSendReject() const - { - return m_invokeId != 99999; - } - - const char* OperationNameCStr() const - { - return m_strOperationName.empty() ? nullptr : m_strOperationName.c_str(); - } - }; - - // Telemetry and optional reject outcome produced by inbound decode failure - // handling. Filled by HandleInboundRoseDecodeFailure for OnInvokeProcessed. - struct InboundDecodeFailureResult - { - unsigned int uiOperationID = 0; - const char* szOperationName = nullptr; - SnaccTelemetryData::Outcome outcome = SnaccTelemetryData::Outcome::UNHANDLED; - long lTelemetryResult = ROSE_RE_DECODE_FAILED; - }; - - // Shared inbound decode-failure path for OnBinaryDataBlock and - // OnBinaryDataBlockResult. Optionally sends mistypedArgument when rejectCtx - // is present; response entry point passes bSendReject=false. - InboundDecodeFailureResult HandleInboundRoseDecodeFailure( - SnaccROSEBase& rose, - const std::optional& rejectCtx, - bool bSendReject, - const std::string& strRejectDetails) - { - InboundDecodeFailureResult result; - if (!rejectCtx) - return result; - result.uiOperationID = rejectCtx->m_uiOperationID; - result.szOperationName = rejectCtx->OperationNameCStr(); - if (!bSendReject || !rejectCtx->CanSendReject()) - return result; - - ROSEReject reject; - if (rejectCtx->m_invokeId) - { - reject.invokedID.choiceId = ROSERejectChoice::invokedIDCid; - reject.invokedID.invokedID = new AsnInt(rejectCtx->m_invokeId); - } - else - { - reject.invokedID.choiceId = ROSERejectChoice::invokednullCid; - reject.invokedID.invokednull = new AsnNull; - } - - reject.reject = new RejectProblem(); - reject.reject->choiceId = RejectProblem::invokeProblemCid; - reject.reject->invokeProblem = new InvokeProblem(SNACC::InvokeProblem::mistypedArgument); - reject.details = UTF8String::CreateNewFromASCII(strRejectDetails.c_str()); - - auto pRejectCtx = SnaccInvokeContext::Create( - SnaccInvokeContextInit(SnaccInvokeDirection::INBOUND, nullptr, rejectCtx->OperationNameCStr())); - const long lRejectResult = rose.SendRejectEx(&reject, *pRejectCtx); - if (lRejectResult == ROSE_NOERROR) - { - result.outcome = SnaccTelemetryData::Outcome::REJECT; - result.lTelemetryResult = ROSE_REJECT_MISTYPEDARGUMENT; - } - else - result.lTelemetryResult = lRejectResult; - return result; - } - // Binds a stack-owned ROSEReject into a ROSEMessage for EncodeReject without // transferring ownership. Detaches in ~dtor so ~ROSEMessage never deletes the // caller's reject (including on encode exceptions). @@ -374,6 +279,156 @@ std::string getPrettyPrinted(const SJson::Value& value) return SJson::writeString(wbuilder, value); } +std::optional SnaccROSEBase::InboundInvokeRejectContext::TryFrom(const SNACC::ROSEMessage& message) +{ + if (message.choiceId != ROSEMessage::invokeCid || !message.invoke) + return std::nullopt; + const auto* pInvoke = message.invoke; + InboundInvokeRejectContext ctx; + ctx.m_invokeId = pInvoke->invokeID; + ctx.m_uiOperationID = pInvoke->operationID; + const char* szName = SnaccRoseOperationLookup::LookUpName(ctx.m_uiOperationID); + if (szName) + ctx.m_strOperationName = szName; + return ctx; +} + +bool SnaccROSEBase::InboundInvokeRejectContext::CanSendReject() const +{ + return m_invokeId != 99999; +} + +const char* SnaccROSEBase::InboundInvokeRejectContext::OperationNameCStr() const +{ + return m_strOperationName.empty() ? nullptr : m_strOperationName.c_str(); +} + +void SnaccROSEBase::EmitInboundDecodeFailureTelemetry(unsigned long ulMessageSize, unsigned int uiOperationID, const char* szOperationName, SnaccTelemetryData::Outcome outcome, long lTelemetryResult) +{ + OnInvokeProcessed(SnaccTelemetryData::CreateFinalized( + SnaccTelemetryData::Direction::INBOUND, + uiOperationID, + szOperationName, + ulMessageSize, + outcome, + SnaccTelemetryData::Stage::INBOUND_DECODE, + SnaccTelemetryData::Reason::DECODE_FAILED, + lTelemetryResult)); +} + +void SnaccROSEBase::EmitInboundGenericDecodeFailureTelemetry(unsigned long ulMessageSize) +{ + EmitInboundDecodeFailureTelemetry(ulMessageSize, 0, nullptr, SnaccTelemetryData::Outcome::UNHANDLED, ROSE_RE_DECODE_FAILED); +} + +SnaccROSEBase::InboundDecodeFailureResult SnaccROSEBase::HandleInboundRoseDecodeFailure(const std::optional& rejectCtx, bool bSendReject, const std::string& strRejectDetails) +{ + InboundDecodeFailureResult result; + if (!rejectCtx) + return result; + result.uiOperationID = rejectCtx->m_uiOperationID; + result.szOperationName = rejectCtx->OperationNameCStr(); + if (!bSendReject || !rejectCtx->CanSendReject()) + return result; + + ROSEReject reject; + if (rejectCtx->m_invokeId) + { + reject.invokedID.choiceId = ROSERejectChoice::invokedIDCid; + reject.invokedID.invokedID = new AsnInt(rejectCtx->m_invokeId); + } + else + { + reject.invokedID.choiceId = ROSERejectChoice::invokednullCid; + reject.invokedID.invokednull = new AsnNull; + } + + reject.reject = new RejectProblem(); + reject.reject->choiceId = RejectProblem::invokeProblemCid; + reject.reject->invokeProblem = new InvokeProblem(SNACC::InvokeProblem::mistypedArgument); + reject.details = UTF8String::CreateNewFromASCII(strRejectDetails.c_str()); + + auto pRejectCtx = SnaccInvokeContext::Create( + SnaccInvokeContextInit(SnaccInvokeDirection::INBOUND, nullptr, rejectCtx->OperationNameCStr())); + const long lRejectResult = SendRejectEx(&reject, *pRejectCtx); + if (lRejectResult == ROSE_NOERROR) + { + result.outcome = SnaccTelemetryData::Outcome::REJECT; + result.lTelemetryResult = ROSE_REJECT_MISTYPEDARGUMENT; + } + else + result.lTelemetryResult = lRejectResult; + return result; +} + +void SnaccROSEBase::HandleInboundEnvelopeSnaccDecodeFailure( + SNACC::TransportEncoding transportEncoding, + SNACC::TransportEncoding hookEncoding, + const char* lpBytes, + unsigned long ulMessageSize, + bool& bLogTransportData, + const SNACC::SnaccException& ex, + const std::optional& rejectCtx, + bool bSendReject, + bool bRejectDetailsFromPrettyJson, + const char* szMethod) +{ + if (bLogTransportData) + bLogTransportData = LogTransportData(false, transportEncoding, nullptr, lpBytes, ulMessageSize, nullptr, nullptr); + + SJson::Value error; + error["exception"] = ex.what(); + error["method"] = szMethod; + error["error"] = (int)ex.m_errorCode; + const std::string strError = getPrettyPrinted(error); + PrintJSONToLog(false, true, nullptr, strError.c_str(), strError.length()); + OnRoseDecodeError(bLogTransportData, hookEncoding, lpBytes, ulMessageSize, ex.what()); + + const std::string rejectDetails = bRejectDetailsFromPrettyJson ? strError : ex.what(); + const auto failure = HandleInboundRoseDecodeFailure(rejectCtx, bSendReject, rejectDetails); + EmitInboundDecodeFailureTelemetry(ulMessageSize, failure.uiOperationID, failure.szOperationName, failure.outcome, failure.lTelemetryResult); +} + +void SnaccROSEBase::HandleInboundJsonParseDecodeFailure( + SNACC::TransportEncoding transportEncoding, + const char* lpBytes, + unsigned long ulMessageSize, + bool& bLogTransportData, + const std::string& parseError, + const char* szMethod) +{ + if (bLogTransportData) + bLogTransportData = LogTransportData(false, transportEncoding, nullptr, lpBytes, ulMessageSize, nullptr, nullptr); + + SJson::Value error; + error["exception"] = parseError; + error["method"] = szMethod; + const std::string strError = getPrettyPrinted(error); + PrintJSONToLog(false, true, nullptr, strError.c_str(), strError.length()); + OnRoseDecodeError(bLogTransportData, transportEncoding, lpBytes, ulMessageSize, parseError); + EmitInboundGenericDecodeFailureTelemetry(ulMessageSize); +} + +void SnaccROSEBase::HandleInboundUnknownEncodingDecodeFailure(const char* lpBytes, unsigned long ulMessageSize, bool& bLogTransportData) +{ + if (bLogTransportData) + bLogTransportData = LogTransportData(false, SNACC::TransportEncoding::BER, nullptr, lpBytes, ulMessageSize, nullptr, nullptr); + OnRoseDecodeError(bLogTransportData, SNACC::TransportEncoding::BER, lpBytes, ulMessageSize, "unknown encoding"); + EmitInboundGenericDecodeFailureTelemetry(ulMessageSize); +} + +void SnaccROSEBase::HandleInboundOuterDecodeFailure(unsigned long ulMessageSize, const char* szException, const char* szMethod, std::optional errorCode) +{ + SJson::Value error; + error["exception"] = szException; + error["method"] = szMethod; + if (errorCode) + error["error"] = *errorCode; + const std::string strError = getPrettyPrinted(error); + PrintJSONToLog(false, true, nullptr, strError.c_str(), strError.length()); + EmitInboundGenericDecodeFailureTelemetry(ulMessageSize); +} + /* * Searches in haystack for needle but searches not until the null byte in haystack but until reaching limit */ @@ -776,19 +831,17 @@ bool SnaccROSEBase::OnBinaryDataBlockResult(const char* lpBytes, unsigned long l } catch (const SnaccException& ex) { - if (bLogTransportData) - bLogTransportData = LogTransportData(false, m_eTransportEncoding, nullptr, lpBytes, lSize, nullptr, nullptr); - - SJson::Value error; - error["exception"] = ex.what(); - error["method"] = __FUNCTION__; - error["error"] = (int)ex.m_errorCode; - std::string strError = getPrettyPrinted(error); - PrintJSONToLog(false, true, nullptr, strError.c_str(), strError.length()); - OnRoseDecodeError(bLogTransportData, SNACC::TransportEncoding::BER, lpBytes, lSize, ex.what()); - - const auto failure = HandleInboundRoseDecodeFailure(*this, rejectCtx, false, ex.what()); - OnInvokeProcessed(SnaccTelemetryData::CreateFinalized(SnaccTelemetryData::Direction::INBOUND, failure.uiOperationID, failure.szOperationName, lSize, failure.outcome, SnaccTelemetryData::Stage::INBOUND_DECODE, SnaccTelemetryData::Reason::DECODE_FAILED, failure.lTelemetryResult)); + HandleInboundEnvelopeSnaccDecodeFailure( + m_eTransportEncoding, + SNACC::TransportEncoding::BER, + lpBytes, + lSize, + bLogTransportData, + ex, + rejectCtx, + false, + false, + __FUNCTION__); return true; } break; @@ -816,79 +869,50 @@ bool SnaccROSEBase::OnBinaryDataBlockResult(const char* lpBytes, unsigned long l } catch (const SnaccException& ex) { - if (bLogTransportData) - bLogTransportData = LogTransportData(false, m_eTransportEncoding, nullptr, lpBytes, lSize, nullptr, nullptr); - - SJson::Value error; - error["exception"] = ex.what(); - error["method"] = __FUNCTION__; - error["error"] = (int)ex.m_errorCode; - std::string strError = getPrettyPrinted(error); - PrintJSONToLog(false, true, nullptr, strError.c_str(), strError.length()); - OnRoseDecodeError(bLogTransportData, m_eTransportEncoding, lpBytes, lSize, ex.what()); - - const auto failure = HandleInboundRoseDecodeFailure(*this, rejectCtx, false, strError); - OnInvokeProcessed(SnaccTelemetryData::CreateFinalized(SnaccTelemetryData::Direction::INBOUND, failure.uiOperationID, failure.szOperationName, lSize, failure.outcome, SnaccTelemetryData::Stage::INBOUND_DECODE, SnaccTelemetryData::Reason::DECODE_FAILED, failure.lTelemetryResult)); + HandleInboundEnvelopeSnaccDecodeFailure( + m_eTransportEncoding, + m_eTransportEncoding, + lpBytes, + lSize, + bLogTransportData, + ex, + rejectCtx, + false, + true, + __FUNCTION__); return true; } } else { - if (bLogTransportData) - bLogTransportData = LogTransportData(false, m_eTransportEncoding, nullptr, lpBytes, lSize, nullptr, nullptr); - -#ifdef _DEBUG - std::string strPayLoad; - strPayLoad.assign(lpBytes, lSize); -#endif - SJson::Value error; - error["exception"] = reader.getFormattedErrorMessages(); - error["where"] = __FUNCTION__; - std::string strError = getPrettyPrinted(error); - PrintJSONToLog(false, true, nullptr, strError.c_str(), strError.length()); - OnRoseDecodeError(bLogTransportData, m_eTransportEncoding, lpBytes, lSize, reader.getFormattedErrorMessages()); - OnInvokeProcessed(SnaccTelemetryData::CreateFinalized(SnaccTelemetryData::Direction::INBOUND, 0, nullptr, lSize, SnaccTelemetryData::Outcome::UNHANDLED, SnaccTelemetryData::Stage::INBOUND_DECODE, SnaccTelemetryData::Reason::DECODE_FAILED, ROSE_RE_DECODE_FAILED)); + HandleInboundJsonParseDecodeFailure( + m_eTransportEncoding, + lpBytes, + lSize, + bLogTransportData, + reader.getFormattedErrorMessages(), + __FUNCTION__); } break; } default: { - if (bLogTransportData) - bLogTransportData = LogTransportData(false, SNACC::TransportEncoding::BER, nullptr, lpBytes, lSize, nullptr, nullptr); - OnRoseDecodeError(bLogTransportData, SNACC::TransportEncoding::BER, lpBytes, lSize, "unknown encoding"); - OnInvokeProcessed(SnaccTelemetryData::CreateFinalized(SnaccTelemetryData::Direction::INBOUND, 0, nullptr, lSize, SnaccTelemetryData::Outcome::UNHANDLED, SnaccTelemetryData::Stage::INBOUND_DECODE, SnaccTelemetryData::Reason::DECODE_FAILED, ROSE_RE_DECODE_FAILED)); + HandleInboundUnknownEncodingDecodeFailure(lpBytes, lSize, bLogTransportData); break; } - break; } } catch (const SnaccException& ex) { - SJson::Value error; - error["exception"] = ex.what(); - error["method"] = __FUNCTION__; - error["error"] = (int)ex.m_errorCode; - std::string strError = getPrettyPrinted(error); - PrintJSONToLog(false, true, nullptr, strError.c_str(), strError.length()); - OnInvokeProcessed(SnaccTelemetryData::CreateFinalized(SnaccTelemetryData::Direction::INBOUND, 0, nullptr, lSize, SnaccTelemetryData::Outcome::UNHANDLED, SnaccTelemetryData::Stage::INBOUND_DECODE, SnaccTelemetryData::Reason::DECODE_FAILED, ROSE_RE_DECODE_FAILED)); + HandleInboundOuterDecodeFailure(lSize, ex.what(), __FUNCTION__, static_cast(ex.m_errorCode)); } catch (const std::exception& ex) { - SJson::Value error; - error["exception"] = ex.what(); - error["method"] = __FUNCTION__; - std::string strError = getPrettyPrinted(error); - PrintJSONToLog(false, true, nullptr, strError.c_str(), strError.length()); - OnInvokeProcessed(SnaccTelemetryData::CreateFinalized(SnaccTelemetryData::Direction::INBOUND, 0, nullptr, lSize, SnaccTelemetryData::Outcome::UNHANDLED, SnaccTelemetryData::Stage::INBOUND_DECODE, SnaccTelemetryData::Reason::DECODE_FAILED, ROSE_RE_DECODE_FAILED)); + HandleInboundOuterDecodeFailure(lSize, ex.what(), __FUNCTION__); } catch (...) { - SJson::Value error; - error["exception"] = "..."; - error["method"] = __FUNCTION__; - std::string strError = getPrettyPrinted(error); - PrintJSONToLog(false, true, nullptr, strError.c_str(), strError.length()); - OnInvokeProcessed(SnaccTelemetryData::CreateFinalized(SnaccTelemetryData::Direction::INBOUND, 0, nullptr, lSize, SnaccTelemetryData::Outcome::UNHANDLED, SnaccTelemetryData::Stage::INBOUND_DECODE, SnaccTelemetryData::Reason::DECODE_FAILED, ROSE_RE_DECODE_FAILED)); + HandleInboundOuterDecodeFailure(lSize, "...", __FUNCTION__); } return bReturn; } @@ -995,20 +1019,17 @@ void SnaccROSEBase::OnBinaryDataBlock(const char* lpBytes, unsigned long ulSize, } catch (const SnaccException& ex) { - if (bLogTransportData) - bLogTransportData = LogTransportData(false, m_eTransportEncoding, nullptr, lpBytes, ulSize, nullptr, nullptr); - - SJson::Value error; - error["exception"] = ex.what(); - error["method"] = __FUNCTION__; - error["error"] = (int)ex.m_errorCode; - std::string strError = getPrettyPrinted(error); - PrintJSONToLog(false, true, nullptr, strError.c_str(), strError.length()); - - OnRoseDecodeError(bLogTransportData, SNACC::TransportEncoding::BER, lpBytes, ulSize, ex.what()); - - const auto failure = HandleInboundRoseDecodeFailure(*this, rejectCtx, true, ex.what()); - OnInvokeProcessed(SnaccTelemetryData::CreateFinalized(SnaccTelemetryData::Direction::INBOUND, failure.uiOperationID, failure.szOperationName, ulSize, failure.outcome, SnaccTelemetryData::Stage::INBOUND_DECODE, SnaccTelemetryData::Reason::DECODE_FAILED, failure.lTelemetryResult)); + HandleInboundEnvelopeSnaccDecodeFailure( + m_eTransportEncoding, + SNACC::TransportEncoding::BER, + lpBytes, + ulSize, + bLogTransportData, + ex, + rejectCtx, + true, + false, + __FUNCTION__); } break; } @@ -1034,76 +1055,50 @@ void SnaccROSEBase::OnBinaryDataBlock(const char* lpBytes, unsigned long ulSize, } catch (const SnaccException& ex) { - if (bLogTransportData) - bLogTransportData = LogTransportData(false, m_eTransportEncoding, nullptr, lpBytes, ulSize, nullptr, nullptr); - - SJson::Value error; - error["exception"] = ex.what(); - error["method"] = __FUNCTION__; - error["error"] = (int)ex.m_errorCode; - std::string strError = getPrettyPrinted(error); - PrintJSONToLog(false, true, nullptr, strError.c_str(), strError.length()); - - OnRoseDecodeError(bLogTransportData, m_eTransportEncoding, lpBytes, ulSize, ex.what()); - - const auto failure = HandleInboundRoseDecodeFailure(*this, rejectCtx, true, strError); - OnInvokeProcessed(SnaccTelemetryData::CreateFinalized(SnaccTelemetryData::Direction::INBOUND, failure.uiOperationID, failure.szOperationName, ulSize, failure.outcome, SnaccTelemetryData::Stage::INBOUND_DECODE, SnaccTelemetryData::Reason::DECODE_FAILED, failure.lTelemetryResult)); + HandleInboundEnvelopeSnaccDecodeFailure( + m_eTransportEncoding, + m_eTransportEncoding, + lpBytes, + ulSize, + bLogTransportData, + ex, + rejectCtx, + true, + true, + __FUNCTION__); } } else { - if (bLogTransportData) - bLogTransportData = LogTransportData(false, m_eTransportEncoding, nullptr, lpBytes, ulSize, nullptr, nullptr); - - SJson::Value error; - error["exception"] = reader.getFormattedErrorMessages(); - error["method"] = __FUNCTION__; - std::string strError = getPrettyPrinted(error); - PrintJSONToLog(false, true, nullptr, strError.c_str(), strError.length()); - - OnRoseDecodeError(bLogTransportData, m_eTransportEncoding, lpBytes, ulSize, reader.getFormattedErrorMessages()); - OnInvokeProcessed(SnaccTelemetryData::CreateFinalized(SnaccTelemetryData::Direction::INBOUND, 0, nullptr, ulSize, SnaccTelemetryData::Outcome::UNHANDLED, SnaccTelemetryData::Stage::INBOUND_DECODE, SnaccTelemetryData::Reason::DECODE_FAILED, ROSE_RE_DECODE_FAILED)); + HandleInboundJsonParseDecodeFailure( + m_eTransportEncoding, + lpBytes, + ulSize, + bLogTransportData, + reader.getFormattedErrorMessages(), + __FUNCTION__); } break; } default: { // if we don't know the encoding, we need to log it binary to ensure proper readability in the logs (ensure payload is hex converted) - if (bLogTransportData) - bLogTransportData = LogTransportData(false, SNACC::TransportEncoding::BER, nullptr, lpBytes, ulSize, nullptr, nullptr); - OnRoseDecodeError(bLogTransportData, SNACC::TransportEncoding::BER, lpBytes, ulSize, "unknown encoding"); - OnInvokeProcessed(SnaccTelemetryData::CreateFinalized(SnaccTelemetryData::Direction::INBOUND, 0, nullptr, ulSize, SnaccTelemetryData::Outcome::UNHANDLED, SnaccTelemetryData::Stage::INBOUND_DECODE, SnaccTelemetryData::Reason::DECODE_FAILED, ROSE_RE_DECODE_FAILED)); + HandleInboundUnknownEncodingDecodeFailure(lpBytes, ulSize, bLogTransportData); break; } } } catch (const SnaccException& ex) { - SJson::Value error; - error["exception"] = ex.what(); - error["method"] = __FUNCTION__; - error["error"] = (int)ex.m_errorCode; - std::string strError = getPrettyPrinted(error); - PrintJSONToLog(false, true, nullptr, strError.c_str(), strError.length()); - OnInvokeProcessed(SnaccTelemetryData::CreateFinalized(SnaccTelemetryData::Direction::INBOUND, 0, nullptr, ulSize, SnaccTelemetryData::Outcome::UNHANDLED, SnaccTelemetryData::Stage::INBOUND_DECODE, SnaccTelemetryData::Reason::DECODE_FAILED, ROSE_RE_DECODE_FAILED)); + HandleInboundOuterDecodeFailure(ulSize, ex.what(), __FUNCTION__, static_cast(ex.m_errorCode)); } catch (const std::exception& ex) { - SJson::Value error; - error["exception"] = ex.what(); - error["method"] = __FUNCTION__; - std::string strError = getPrettyPrinted(error); - PrintJSONToLog(false, true, nullptr, strError.c_str(), strError.length()); - OnInvokeProcessed(SnaccTelemetryData::CreateFinalized(SnaccTelemetryData::Direction::INBOUND, 0, nullptr, ulSize, SnaccTelemetryData::Outcome::UNHANDLED, SnaccTelemetryData::Stage::INBOUND_DECODE, SnaccTelemetryData::Reason::DECODE_FAILED, ROSE_RE_DECODE_FAILED)); + HandleInboundOuterDecodeFailure(ulSize, ex.what(), __FUNCTION__); } catch (...) { - SJson::Value error; - error["exception"] = L"..."; - error["method"] = __FUNCTION__; - std::string strError = getPrettyPrinted(error); - PrintJSONToLog(false, true, nullptr, strError.c_str(), strError.length()); - OnInvokeProcessed(SnaccTelemetryData::CreateFinalized(SnaccTelemetryData::Direction::INBOUND, 0, nullptr, ulSize, SnaccTelemetryData::Outcome::UNHANDLED, SnaccTelemetryData::Stage::INBOUND_DECODE, SnaccTelemetryData::Reason::DECODE_FAILED, ROSE_RE_DECODE_FAILED)); + HandleInboundOuterDecodeFailure(ulSize, "...", __FUNCTION__); } } diff --git a/cpp-lib/tests/runtime_correctness_notes.md b/cpp-lib/tests/runtime_correctness_notes.md index 8a162b6..39a9414 100644 --- a/cpp-lib/tests/runtime_correctness_notes.md +++ b/cpp-lib/tests/runtime_correctness_notes.md @@ -6,7 +6,7 @@ entry_for: - C++ runtime behavior - runtime correctness tests - ROSE telemetry and shutdown semantics -purpose: Record intended C++ runtime semantics before tests or implementation are tightened. +purpose: Record intended C++ runtime semantics and implementation status for ROSE correctness areas exercised by cpp-lib runtime tests. read_when: - Changing cpp-lib runtime behavior, telemetry, shutdown, or decode-error handling - Adding or reviewing runtime correctness tests @@ -18,9 +18,9 @@ related_docs: # Runtime Correctness Notes This note records the intended semantics for selected `cpp-lib` runtime behaviors -before tightening tests or changing the implementation. 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. +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.h` @@ -30,18 +30,20 @@ Primary reference points: ## Summary -| Area | Current Behavior | Intended Behavior | -| --- | --- | --- | -| `StopProcessing()` | Completes pending operations, but does not block new work in practice | Shutdown must refuse new outbound work and stop inbound invoke/event dispatch | -| `iTimeout == 0` telemetry | Reported as `Outcome::UNHANDLED` with `WAIT_SKIPPED` | Treated as a successful fire-and-forget dispatch, not as a failure | -| Response payload decode after a valid reply envelope | Caller sees decode failure, telemetry still looks like remote result/error | Telemetry must primarily reflect the caller-visible outcome | -| Inbound decode failures and ROSE rejects | Enforced: garbage wire is silent; targeted reject only after envelope decode | See section 5 (implemented on `feature/UCAAS-1446-rose-invoke-context-runtime-tests`) | -| `OnBinaryDataBlockResult()` decode-error hooks | `OnRoseDecodeError()` and `bAlreadyTransportLogged` parity with `OnBinaryDataBlock()` | Implemented; covered by `PublicApiSmokeTest.OnBinaryDataBlockResultDecodeErrorsInvokeHook*` | -| `ROSEMessage` ownership on inbound decode | `unique_ptr` at decode sites; `std::move` through dispatch | See section 6 (implemented) | -| Outbound encode / `Send()` ownership | Manual `new` + `// prevent delete` nulling on happy path | See section 7 — exception safety and fragility, not steady-state leaks on happy path | +| 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` | ## 1. `StopProcessing()` Shutdown Contract +### Status: implemented + ### Public contract `SnaccROSEBase` documents shutdown as a hard stop: @@ -54,53 +56,6 @@ Primary reference points: void StopProcessing(bool bStop = true); ``` -### Current behavior - -The implementation only toggles `m_bProcessingAllowed` and completes pending -operations. In the current tree, the flag is not enforced by the send or receive -paths. - -```330:338:cpp-lib/src/SnaccROSEBase.cpp -void SnaccROSEBase::StopProcessing(bool bStop /*= true*/) -{ - { - std::lock_guard guard(m_InternalProtectMutex); - m_bProcessingAllowed = bStop ? false : true; - } - - CompleteAllPendingOperations(); -} -``` - -```346:352:cpp-lib/src/SnaccROSEBase.cpp -void SnaccROSEBase::CompleteAllPendingOperations() -{ - std::lock_guard guard(m_InternalProtectMutex); - - for (auto it = m_PendingOperations.begin(); it != m_PendingOperations.end(); it++) - it->second->CompleteOperation(ROSE_TE_SHUTDOWN, NULL); -} -``` - -`SendInvoke()` does not check `m_bProcessingAllowed` before creating a pending -operation, sending an invoke, or waiting for a response: - -```1500:1506:cpp-lib/src/SnaccROSEBase.cpp - auto& pendingOP = AddPendingOperation(pinvoke->invokeID, pinvoke->operationID, szResolvedOperationName); - - size_t stRequestData = 0; - long lRoseResult = Send(pinvoke, szOperationName, ctx, &stRequestData); - pendingOP.m_pTelemetry = SnaccTelemetryData::Create(SnaccTelemetryData::Direction::OUTBOUND, pinvoke->operationID, szResolvedOperationName, stRequestData, chronoCreated); - - if (lRoseResult == 0) -``` - -### Why this is inconsistent - -- The public header promises that new calls are blocked after shutdown. -- The implementation only completes already pending work. -- Callers and operators cannot rely on shutdown to be a real safety boundary. - ### Intended behavior Treat `StopProcessing(true)` as a real runtime shutdown gate: @@ -111,198 +66,168 @@ Treat `StopProcessing(true)` as a real runtime shutdown gate: handlers while shutdown is active. 4. Late inbound responses that arrive after pending operations were force- completed may be ignored, but they must not resurrect completed work. -5. `StopProcessing(false)` may re-enable the runtime if that is part of the - supported API, but the behavior should be explicit and documented as a - restart of processing, not an incidental side effect. - -### Tests that should enforce this later - -- `SendInvoke()` after shutdown returns `ROSE_TE_SHUTDOWN` without creating a - new pending operation. -- `SendEvent()` after shutdown returns `ROSE_TE_SHUTDOWN`. -- An inbound invoke fed through `OnBinaryDataBlock()` during shutdown does not - reach application handlers. -- Existing pending operations complete with `ROSE_TE_SHUTDOWN`. -- If restart remains supported, `StopProcessing(false)` has one explicit test - proving the intended resumed behavior. - -### Likely code paths to change - -- `SnaccROSEBase::SendInvoke()` -- `SnaccROSEBase::SendEvent()` -- potentially `SnaccROSEBase::Send()` as the shared outbound choke point -- `SnaccROSEBase::OnBinaryDataBlock()` -- `SnaccROSEBase::OnBinaryDataBlockResult()` -- possibly `SnaccROSEBase::OnROSEMessage()` for central inbound gating +5. `StopProcessing(false)` re-enables processing; callers must treat that as an + explicit restart, not an incidental side effect. -## 2. Fire-And-Forget Invoke Telemetry (`iTimeout == 0`) +### Implementation -### Current behavior +`StopProcessing(true)` clears `m_bProcessingAllowed` and completes all pending +operations with `ROSE_TE_SHUTDOWN`: -When the caller explicitly chooses not to wait, `SendInvoke()` treats the call -as locally successful and stores `ROSE_NOERROR`: +```605:628:cpp-lib/src/SnaccROSEBase.cpp +void SnaccROSEBase::StopProcessing(bool bStop /*= true*/) +{ + { + std::lock_guard guard(m_InternalProtectMutex); + m_bProcessingAllowed = bStop ? false : true; + } -```1529:1533:cpp-lib/src/SnaccROSEBase.cpp - else - { - lRoseResult = ROSE_NOERROR; - pendingOP.m_lRoseResult = lRoseResult; - } + if (bStop) + CompleteAllPendingOperations(); +} +... + for (auto it = m_PendingOperations.begin(); it != m_PendingOperations.end(); it++) + it->second->CompleteOperation(ROSE_TE_SHUTDOWN); ``` -Later, telemetry finalization sees "no answer message" plus `ROSE_NOERROR` and -records the lifecycle as `Outcome::UNHANDLED` with `Reason::WAIT_SKIPPED`. +Outbound choke points check `IsProcessingAllowed()` before creating pending +operations or sending: -```299:306:cpp-lib/src/SnaccROSEBase.cpp - if (m_lRoseResult != ROSE_NOERROR) +```1604:1610:cpp-lib/src/SnaccROSEBase.cpp + if (!IsProcessingAllowed()) { - m_pTelemetry->finalize(SnaccTelemetryData::Outcome::UNHANDLED, GetOutboundUnhandledStageFromResult(m_lRoseResult), GetUnhandledReasonFromResult(m_lRoseResult), m_lRoseResult, std::nullopt, std::move(pctx)); - return; + auto telemetry = SnaccTelemetryData::Create(...); + telemetry->finalize(..., SnaccTelemetryData::Reason::SHUTDOWN, ROSE_TE_SHUTDOWN, ...); + OnInvokeProcessed(telemetry); + return ROSE_TE_SHUTDOWN; } - - m_pTelemetry->finalize(SnaccTelemetryData::Outcome::UNHANDLED, SnaccTelemetryData::Stage::OUTBOUND_WAIT, SnaccTelemetryData::Reason::WAIT_SKIPPED, m_lRoseResult, std::nullopt, std::move(pctx)); ``` -### Why this is inconsistent +`SendEvent()` uses the same gate and returns `ROSE_TE_SHUTDOWN` without sending. -- `WAIT_SKIPPED` is intentional caller behavior, not a failure condition. -- `Outcome::UNHANDLED` is otherwise used for abnormal paths such as transport, - timeout, shutdown, invalid response, and decode failure. -- A caller intentionally using fire-and-forget currently looks like a failure in - telemetry aggregations. - -### Intended behavior +Inbound invoke/event dispatch is blocked in `OnInvokeMessage()`: -Fire-and-forget should be treated as a successful local dispatch of an invoke -whose remote outcome is intentionally unknown to this runtime instance. +```1324:1325:cpp-lib/src/SnaccROSEBase.cpp + if (!IsProcessingAllowed()) + lResult = ROSE_TE_SHUTDOWN; +``` -Preferred model: +Wire data may still be decoded on the receive path; handlers are not reached +while shutdown is active. -1. Introduce a dedicated successful outcome such as `DISPATCHED` or `PENDING` - for "sent, not awaited". -2. Keep `Reason::WAIT_SKIPPED` to preserve the explicit cause. -3. Keep `Stage::OUTBOUND_WAIT` or rename it later if a more precise stage model - is introduced. +### Tests that enforce this -If the enum surface cannot be changed immediately, the minimum semantic rule -should still be: +- `PublicApiRuntimeTest.StopProcessingBlocksNewOutboundInvokesAndEvents` +- `PublicApiRuntimeTest.StopProcessingBlocksInboundDispatchUntilReEnabled` +- `LifecycleRuntimeTest.StopProcessingCompletesPendingInvokeWithShutdownBer` +- `LifecycleRuntimeTest.StopProcessingCompletesPendingInvokeWithShutdownJson` +- `LifecycleRuntimeTest.PendingInvokeCanRecoverAfterShutdownOnNextFixtureSetupBer` +- `LifecycleRuntimeTest.PendingInvokeCanRecoverAfterShutdownOnNextFixtureSetupJson` -- do not classify `WAIT_SKIPPED` under the same top-level failure bucket used - for true unhandled faults. +## 2. Fire-And-Forget Invoke Telemetry (`iTimeout == 0`) -### Tests that should enforce this later +### Status: implemented -- `SendInvoke(..., iTimeout = 0)` produces a non-failure telemetry outcome. -- The telemetry reason remains `WAIT_SKIPPED`. -- The runtime reports local send success and does not fabricate a remote result. +### Intended behavior -### Likely code paths to change +Fire-and-forget is a successful local dispatch of an invoke whose remote outcome +is intentionally unknown to this runtime instance: -- `SnaccROSEPendingOperation::FinalizeTelemetry()` -- `SnaccTelemetryData::Outcome` in `cpp-lib/include/SnaccTelemetry.h` -- any debug-text helpers in `cpp-lib/src/SnaccTelemetry.cpp` +1. `Outcome::DISPATCHED` for "sent, not awaited". +2. `Reason::WAIT_SKIPPED` preserves the explicit cause. +3. `Stage::OUTBOUND_WAIT` is acceptable for now; a finer stage taxonomy is + deferred until async invokes that complete via callback reshape outbound + lifecycle telemetry anyway. -## 3. Outbound Telemetry After Response Payload Decode Failure +`WAIT_SKIPPED` must not be classified under the same top-level failure bucket as +transport errors, timeouts, shutdown, invalid responses, or decode failures. -### Current behavior +### Implementation -`HandleInvokeResult()` can turn a reply that had a valid envelope into a final -caller-visible decode failure when the embedded result or error payload cannot -be decoded. +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: -```1745:1747:cpp-lib/src/SnaccROSEBase.cpp - lRoseResult = DecodeResponse(pResponseMsg, &pResult, &pError, ctx); - - if (lRoseResult == ROSE_NOERROR) +```581:581:cpp-lib/src/SnaccROSEBase.cpp + m_pTelemetry->finalize(SnaccTelemetryData::Outcome::DISPATCHED, SnaccTelemetryData::Stage::OUTBOUND_WAIT, SnaccTelemetryData::Reason::WAIT_SKIPPED, m_lRoseResult, std::nullopt, std::move(pctx)); ``` -```1787:1796:cpp-lib/src/SnaccROSEBase.cpp - catch (const SnaccException& ex) - { - SJson::Value err; - err["exception"] = ex.what(); - err["method"] = __FUNCTION__; - err["error"] = (int)ex.m_errorCode; - std::string strError = getPrettyPrinted(err); - PrintJSONToLog(false, true, nullptr, strError.c_str(), strError.length()); - - lRoseResult = ROSE_RE_DECODE_FAILED; - } -``` +`SnaccTelemetryData::Outcome::DISPATCHED` and its debug text are defined in +`cpp-lib/include/SnaccTelemetry.h` and `cpp-lib/src/SnaccTelemetry.cpp`. -But outbound telemetry finalization uses the received envelope kind stored in -`m_pAnswerMessage`, not the final caller-visible result returned after payload -decode: +### Tests that enforce this -```280:292:cpp-lib/src/SnaccROSEBase.cpp - if (m_pAnswerMessage) - { - switch (m_pAnswerMessage->choiceId) - { - case ROSEMessage::resultCid: - m_pTelemetry->finalize(SnaccTelemetryData::Outcome::RESULT, SnaccTelemetryData::Stage::OUTBOUND_WAIT, SnaccTelemetryData::Reason::REMOTE_RESULT, m_lRoseResult, m_stResponseData, pctx); - break; - case ROSEMessage::errorCid: - m_pTelemetry->finalize(SnaccTelemetryData::Outcome::ERR, SnaccTelemetryData::Stage::OUTBOUND_WAIT, SnaccTelemetryData::Reason::REMOTE_ERROR, m_lRoseResult, m_stResponseData, pctx); - break; -``` +- `TelemetryRuntimeTest.WaitSkippedTelemetryBer` +- `TelemetryRuntimeTest.WaitSkippedTelemetryJson` -### Why this is inconsistent +## 3. Outbound Telemetry After Response Payload Decode Failure -- The public caller gets a decode failure. -- Telemetry still says the operation completed as a clean remote result or - remote error. -- This makes telemetry disagree with the API result that application code sees. +### Status: implemented ### Intended behavior -For outbound invoke telemetry, the final caller-visible result must be the -authoritative classification. - -That means: +For outbound invoke telemetry, the final caller-visible result is the +authoritative classification: 1. If the response envelope was received but payload decode fails, telemetry - should finalize as `Outcome::UNHANDLED`. -2. The reason should be `DECODE_FAILED`. -3. The result code should be `ROSE_RE_DECODE_FAILED`. -4. If later needed, the runtime may keep a secondary field for "remote envelope - kind received", but that must not override the primary outcome. + finalizes as `Outcome::UNHANDLED`. +2. The reason is `DECODE_FAILED`. +3. The result code is `ROSE_RE_DECODE_FAILED`. +4. Envelope kind (`result` vs `error`) must not override the primary outcome. -### Tests that should enforce this later +### Implementation -- A malformed result payload yields `ROSE_RE_DECODE_FAILED` to the caller and - telemetry `UNHANDLED + DECODE_FAILED`. -- A malformed error payload yields the same shape. -- The telemetry record must no longer be `REMOTE_RESULT` or `REMOTE_ERROR` in - those cases. +`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: -### Likely code paths to change +```549:552:cpp-lib/src/SnaccROSEBase.cpp + 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. -- `SnaccROSEBase::HandleInvokeResult()` -- `SnaccROSEPendingOperation::FinalizeTelemetry()` -- potentially `SnaccROSEPendingOperation::CompleteOperation()` if more state is - needed than envelope kind plus raw result code +### Tests that enforce this + +- `TelemetryRuntimeTest.ResultPayloadDecodeFailureTelemetryBer` +- `TelemetryRuntimeTest.ResultPayloadDecodeFailureTelemetryJson` +- `TelemetryRuntimeTest.ErrorPayloadDecodeFailureTelemetryBer` +- `TelemetryRuntimeTest.ErrorPayloadDecodeFailureTelemetryJson` ## 4. `OnBinaryDataBlockResult()` Decode-Error Hook and Logging Parity ### Status: implemented -Both inbound entry points now call `OnRoseDecodeError()` for comparable decode +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`. + ### Tests that enforce this - `PublicApiSmokeTest.OnBinaryDataBlockResultDecodeErrorsInvokeHookBer` - `PublicApiSmokeTest.OnBinaryDataBlockResultDecodeErrorsInvokeHookJson` -### Remaining maintenance note - -Hook and logging behavior is duplicated between `OnBinaryDataBlock()` and -`OnBinaryDataBlockResult()`. A shared helper would reduce future drift, but -parity itself is no longer an open gap. - ## 5. Inbound Decode Layers, Reject Policy, and BER vs JSON ### Why BER and JSON are not symmetric at the wire layer @@ -359,8 +284,8 @@ Hook and logging parity between the two paths is required. **Reject parity is no — the response path must not fabricate server-side rejects when a reply cannot be decoded. -**Cleanup done:** legacy reject branches were removed from `OnBinaryDataBlockResult()` -decode catches; that path is telemetry-only on decode failure. +Legacy reject branches were removed from `OnBinaryDataBlockResult()` decode +catches; that path is telemetry-only on decode failure. ### Tests that enforce this @@ -378,7 +303,9 @@ decode catches; that path is telemetry-only on decode failure. ## 6. `ROSEMessage` Ownership on Inbound Decode Paths -### Contract (implemented) +### Status: implemented + +### Contract Inbound decode paths allocate with `std::make_unique()`. After a successful envelope decode (`BDec` / `JDec`), reject-relevant invoke fields are @@ -414,17 +341,54 @@ Reject policy in decode `catch` blocks: - `SnaccROSEBase::CompletePendingOperation()` - `SnaccROSEPendingOperation::CompleteOperation()` +## 7. Outbound Encode and `Send()` Ownership + +### Status: implemented + +### Problem + +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. + +### Contract + +1. Caller retains ownership of invoke arguments, result/error payloads, and reject + objects passed into `Send()`, `EncodeResult()`, `EncodeError()`, and + `EncodeReject()`. +2. Stack `ROSEMessage` envelopes used for encoding must detach borrowed arms in + all exit paths, including encode exceptions. +3. `ScopedInvokeOperationName` may allocate a temporary `operationName` on the + caller invoke for JSON encoding only; it removes that allocation on scope exit. + +### Implementation + +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. + +### Tests that exercise this + +- Happy-path outbound invoke/response flows across the runtime test suite +- `InvokeContextRuntimeTest.OutboundEncodeFailureKeepsCallerContextJson` (encode + failure must not corrupt caller-owned invoke context) + ## Recommended Follow-Up Order -1. Fix and test the shutdown contract first, because it affects public API - guarantees and can change how the rest of the runtime is exercised. -2. Fix fire-and-forget telemetry classification so dashboards stop reporting - intentional behavior as a failure. -3. Fix response-payload decode telemetry so caller-visible failures and - telemetry agree. -4. ~~Remove legacy reject branches from `OnBinaryDataBlockResult()` decode catches~~ (done) - and fix the BER branch there (reject object missing `invokeProblem` if that - path ever fired). -5. Optionally extract shared decode-failure helpers between - `OnBinaryDataBlock()` and `OnBinaryDataBlockResult()` to avoid drift. -6. Commit branch updates; push and open PR for UCAAS-1446. +1. ~~Shutdown contract~~ (done) +2. ~~Fire-and-forget telemetry classification~~ (done) +3. ~~Response-payload decode telemetry~~ (done) +4. ~~Inbound decode reject policy and `OnBinaryDataBlockResult()` reject cleanup~~ (done) +5. ~~Inbound and outbound `ROSEMessage` ownership~~ (done) +6. Push branch and open PR for UCAAS-1446. +7. ~~Extract shared decode-failure helpers between `OnBinaryDataBlock()` and `OnBinaryDataBlockResult()`~~ (done) From e255635b7e7e7efd503f2ec507316913c270d61d Mon Sep 17 00:00:00 2001 From: Jan Fellner Date: Tue, 7 Jul 2026 10:07:47 +0200 Subject: [PATCH 08/10] Bump version to 7.0.6 (release 07.07.2026). Co-authored-by: Cursor --- version.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/version.h b/version.h index 225f4f0..357e845 100644 --- a/version.h +++ b/version.h @@ -1,8 +1,8 @@ #ifndef VERSION_H #define VERSION_H -#define VERSION "7.0.5" -#define VERSION_RC 7, 0, 5 -#define RELDATE "03.07.2026" +#define VERSION "7.0.6" +#define VERSION_RC 7, 0, 6 +#define RELDATE "07.07.2026" #endif // VERSION_H From 9da9fcd9bf591f6386385ab2977429f071c3957e Mon Sep 17 00:00:00 2001 From: Jan Fellner Date: Tue, 7 Jul 2026 15:09:01 +0200 Subject: [PATCH 09/10] UCAAS-1446 Restore const-correctness on ROSE stub/runtime APIs. Use const references and pointers for borrow-only envelopes and payloads, with documented in-place BER JSON logging grafts that restore the owned message. --- cpp-lib/include/SnaccROSEBase.h | 30 ++--- cpp-lib/include/SnaccROSEInterfaces.h | 12 +- cpp-lib/src/SnaccROSEBase.cpp | 127 ++++++++++-------- .../test_support/sample_runtime_harness.h | 4 +- 4 files changed, 93 insertions(+), 80 deletions(-) diff --git a/cpp-lib/include/SnaccROSEBase.h b/cpp-lib/include/SnaccROSEBase.h index d1ddb9b..9865179 100644 --- a/cpp-lib/include/SnaccROSEBase.h +++ b/cpp-lib/include/SnaccROSEBase.h @@ -215,7 +215,7 @@ class SnaccROSEBase : public SnaccROSESender, public SnaccTelemetryCallback * strResponse - the encoded response data to send via the transport layer * szSessionID - the SessionID (this propery is filled by subclassing from the concrete class in case we are handling multiple clients via one connection) */ - virtual long EncodeResult(unsigned int uiInvokeID, SNACC::AsnType* pResult, std::string& strResponse, const wchar_t* szSessionID = nullptr) override; + virtual long EncodeResult(unsigned int uiInvokeID, const SNACC::AsnType* pResult, std::string& strResponse, const wchar_t* szSessionID = nullptr) override; /* Send a Reject Message. */ long EncodeReject(SNACC::ROSEReject* preject, std::string& strResponse); @@ -242,7 +242,7 @@ class SnaccROSEBase : public SnaccROSESender, public SnaccTelemetryCallback * strResponse - the encoded response data to send via the transport layer * szSessionID - the SessionID (this propery is filled by subclassing from the concrete class in case we are handling multiple clients via one connection) */ - virtual long EncodeError(unsigned int uiInvokeID, SNACC::AsnType* pError, std::string& strResponse, const wchar_t* szSessionID = nullptr) override; + virtual long EncodeError(unsigned int uiInvokeID, const SNACC::AsnType* pError, std::string& strResponse, const wchar_t* szSessionID = nullptr) override; /*! Increment invoke counter Override from SnaccRoseSender*/ @@ -285,19 +285,19 @@ class SnaccROSEBase : public SnaccROSESender, public SnaccTelemetryCallback * pResult - the result object (Base type pointer, the caller of the invoke provides the proper type) * pError - the error object (Base type pointer, the caller of the invoke provides the proper type) */ - virtual long HandleOnInvokeResult(SNACC::InvokeResult invokeResult, SNACC::ROSEInvoke& invoke, SnaccInvokeContext& ctx, std::string& strResponse, SNACC::AsnType* pResult, SNACC::AsnType* pError) override; + virtual long HandleOnInvokeResult(SNACC::InvokeResult invokeResult, const SNACC::ROSEInvoke& invoke, SnaccInvokeContext& ctx, std::string& strResponse, SNACC::AsnType* pResult, SNACC::AsnType* pError) override; /** * Allows implementers to customize the decoded invoke response before it is handed back * to the caller, for example to propagate connection-specific session data. * * lRoseResult - the current ROSE result code - * responseMsg - owned pending-op response; may be mutated for BER JSON logging - * result - the decoded result payload in case a result response is received - * error - the decoded error payload in case an error response is received + * responseMsg - owned pending-op response; read-only for callers and overrides + * result - the result object (Base type pointer, the caller of the invoke provides the proper type) + * error - the error object (Base type pointer, the caller of the invoke provides the proper type) * ctx - contextual data for the invoke */ - virtual long HandleInvokeResult(long lRoseResult, SNACC::ROSEMessage& responseMsg, SNACC::AsnType* result, SNACC::AsnType* error, SnaccInvokeContext& ctx) override; + virtual long HandleInvokeResult(long lRoseResult, const SNACC::ROSEMessage& responseMsg, SNACC::AsnType* result, SNACC::AsnType* error, SnaccInvokeContext& ctx) override; /** * An event (invoke without result) that is send to the other side. Should only be called by the ROSE stub itself generated files @@ -319,7 +319,7 @@ class SnaccROSEBase : public SnaccROSESender, public SnaccTelemetryCallback * invokeMessage - inbound invoke ROSEMessage; borrow only for decode/logging * argument - the argument object (Base type pointer, the caller of the provides the proper type) */ - long DecodeInvoke(SNACC::ROSEMessage& invokeMessage, SNACC::AsnType* argument) override; + long DecodeInvoke(const SNACC::ROSEMessage& invokeMessage, SNACC::AsnType* argument) override; protected: // Get the length prefix for a given strJson payload @@ -349,7 +349,7 @@ class SnaccROSEBase : public SnaccROSESender, public SnaccTelemetryCallback responses move into `CompletePendingOperation()`. returns true if the message was processed. set bAllowInvokes to false, if invokes are not processed. */ - virtual bool OnROSEMessage(std::unique_ptr pmessage, bool bAllowInvokes, unsigned long ulMessageSize); + virtual bool OnROSEMessage(std::unique_ptr pMessage, bool bAllowInvokes, unsigned long ulMessageSize); /* * This callback provides telemetry data for processed ROSE messages. @@ -360,11 +360,11 @@ class SnaccROSEBase : public SnaccROSESender, public SnaccTelemetryCallback /*! Inbound invoke/event dispatch; takes ownership of the decoded message. */ virtual void OnInvokeMessage(std::unique_ptr pMessage, unsigned long lMessageSize); /*! Telemetry hook for matched inbound result; borrows arm of owned message. */ - virtual void OnResultMessage(SNACC::ROSEResult& result, unsigned long lMessageSize); + virtual void OnResultMessage(const SNACC::ROSEResult& result, unsigned long lMessageSize); /*! Telemetry hook for matched inbound error; borrows arm of owned message. */ - virtual void OnErrorMessage(SNACC::ROSEError& error, unsigned long lMessageSize); + virtual void OnErrorMessage(const SNACC::ROSEError& error, unsigned long lMessageSize); /*! Telemetry hook for inbound reject; borrows arm of owned message. */ - virtual void OnRejectMessage(SNACC::ROSEReject& reject, unsigned long lMessageSize); + virtual void OnRejectMessage(const SNACC::ROSEReject& reject, unsigned long lMessageSize); // Snapshot of inbound invoke metadata taken immediately after a successful // ROSE envelope decode. @@ -407,7 +407,7 @@ class SnaccROSEBase : public SnaccROSESender, public SnaccTelemetryCallback /*! The counter for the InvokeIds */ long m_lInvokeCounter{}; /*! The guard for m_bProcessingAllowed and SnaccROSEPendingOperationMap */ - std::mutex m_InternalProtectMutex; + mutable std::mutex m_InternalProtectMutex; /*! If set, the open file where we append log data to. The pointer is created with ConfigureFileLogging() */ FILE* m_pAsnLogFile{}; /*! true if the logfile already contains data, false if not */ @@ -428,7 +428,7 @@ class SnaccROSEBase : public SnaccROSESender, public SnaccTelemetryCallback static long GetRejectResultCode(const SNACC::ROSEReject* pReject, SnaccInvokeContext& ctx); static long DecodeResponse(const SNACC::ROSEMessage& response, SNACC::ROSEResult*& pResult, SNACC::ROSEError*& pError, SnaccInvokeContext& ctx); void CompleteAllPendingOperations(); - bool IsProcessingAllowed(); + bool IsProcessingAllowed() const; SnaccROSEPendingOperationMap m_PendingOperations; @@ -449,7 +449,7 @@ class SnaccROSEBase : public SnaccROSESender, public SnaccTelemetryCallback int GetJsonHeaderLen(const char* lpBytes, unsigned long iLength); // Encodes a value based on the transportencoding and loglevel - std::string GetEncoded(const SNACC::TransportEncoding encoding, SNACC::AsnType* pValue, unsigned long* pUlSize = nullptr); + std::string GetEncoded(const SNACC::TransportEncoding encoding, const SNACC::AsnType* pValue, unsigned long* pUlSize = nullptr); /** * The combined method that takes care about encoding and sending an event or invoke diff --git a/cpp-lib/include/SnaccROSEInterfaces.h b/cpp-lib/include/SnaccROSEInterfaces.h index 0e7a976..c4c2a2d 100644 --- a/cpp-lib/include/SnaccROSEInterfaces.h +++ b/cpp-lib/include/SnaccROSEInterfaces.h @@ -222,12 +222,12 @@ class SnaccROSESender * Handles the response payload of the SendInvoke method. Retrieves the result or error from the response * * lRoseResult - the result of the SendInvoke method - * responseMsg - owned pending-op response; may be mutated for BER JSON logging + * responseMsg - owned pending-op response; read-only for callers and overrides * result - the result object (Base type pointer, the caller of the invoke provides the proper type) * error - the error object (Base type pointer, the caller of the invoke provides the proper type) * ctx - contextual data for the invoke */ - virtual long HandleInvokeResult(long lRoseResult, SNACC::ROSEMessage& responseMsg, SNACC::AsnType* result, SNACC::AsnType* error, SnaccInvokeContext& ctx) = 0; + virtual long HandleInvokeResult(long lRoseResult, const SNACC::ROSEMessage& responseMsg, SNACC::AsnType* result, SNACC::AsnType* error, SnaccInvokeContext& ctx) = 0; /** * Encodes the result or error from an OnInvoke request. Retrieves the result or error from the response @@ -239,7 +239,7 @@ class SnaccROSESender * result - the result object (Base type pointer, the caller of the invoke provides the proper type) * error - the error object (Base type pointer, the caller of the invoke provides the proper type) */ - virtual long HandleOnInvokeResult(SNACC::InvokeResult invokeResult, SNACC::ROSEInvoke& invoke, SnaccInvokeContext& ctx, std::string& strResponse, SNACC::AsnType* pResult, SNACC::AsnType* pError) = 0; + virtual long HandleOnInvokeResult(SNACC::InvokeResult invokeResult, const SNACC::ROSEInvoke& invoke, SnaccInvokeContext& ctx, std::string& strResponse, SNACC::AsnType* pResult, SNACC::AsnType* pError) = 0; /** * Decodes an invoke and properly handles logging for it @@ -247,7 +247,7 @@ class SnaccROSESender * invokeMessage - inbound invoke ROSEMessage; borrow only for decode/logging * argument - the argument object (Base type pointer, the caller of the provides the proper type) */ - virtual long DecodeInvoke(SNACC::ROSEMessage& invokeMessage, SNACC::AsnType* argument) = 0; + virtual long DecodeInvoke(const SNACC::ROSEMessage& invokeMessage, SNACC::AsnType* argument) = 0; /** An event (invoke without result) that is send to the other side. Should only be called by the ROSE stub itself generated files * @@ -266,7 +266,7 @@ class SnaccROSESender * strResponse - the encoded response data to send via the transport layer * szSessionID - the SessionID (this propery is filled by subclassing from the concrete class in case we are handling multiple clients via one connection) */ - virtual long EncodeResult(unsigned int uiInvokeID, SNACC::AsnType* pResult, std::string& strResponse, const wchar_t* szSessionID = nullptr) = 0; + virtual long EncodeResult(unsigned int uiInvokeID, const SNACC::AsnType* pResult, std::string& strResponse, const wchar_t* szSessionID = nullptr) = 0; /* * Encodes an error as repsonse to an invoke @@ -276,7 +276,7 @@ class SnaccROSESender * strResponse - the encoded response data to send via the transport layer * szSessionID - the SessionID (this propery is filled by subclassing from the concrete class in case we are handling multiple clients via one connection) */ - virtual long EncodeError(unsigned int uiInvokeID, SNACC::AsnType* pError, std::string& strResponse, const wchar_t* szSessionID = nullptr) = 0; + virtual long EncodeError(unsigned int uiInvokeID, const SNACC::AsnType* pError, std::string& strResponse, const wchar_t* szSessionID = nullptr) = 0; }; class SnaccScopedInvokeMessage diff --git a/cpp-lib/src/SnaccROSEBase.cpp b/cpp-lib/src/SnaccROSEBase.cpp index 732f4b6..df794e2 100644 --- a/cpp-lib/src/SnaccROSEBase.cpp +++ b/cpp-lib/src/SnaccROSEBase.cpp @@ -61,12 +61,12 @@ namespace class RoseEncodeResultEnvelope final { public: - RoseEncodeResultEnvelope(unsigned int uiInvokeID, SNACC::AsnType* pResult, const wchar_t* szSessionID) + RoseEncodeResultEnvelope(unsigned int uiInvokeID, const AsnType* pResult, const wchar_t* szSessionID) { m_result.invokeID = uiInvokeID; m_result.result = new ROSEResultSeq; m_result.result->resultValue = 0; - m_result.result->result.value = pResult; + m_result.result->result.value = const_cast(pResult); if (szSessionID) m_result.sessionID = new UTF8String(szSessionID); m_message.choiceId = ROSEMessage::resultCid; @@ -108,12 +108,12 @@ namespace class RoseEncodeErrorEnvelope final { public: - RoseEncodeErrorEnvelope(unsigned int uiInvokeID, SNACC::AsnType* pError, const wchar_t* szSessionID) + RoseEncodeErrorEnvelope(unsigned int uiInvokeID, const AsnType* pError, const wchar_t* szSessionID) { m_error.invokedID = uiInvokeID; m_error.error_value = 0; m_error.error = new AsnAny(); - m_error.error->value = pError; + m_error.error->value = const_cast(pError); if (szSessionID) m_error.sessionID = new UTF8String(szSessionID); m_message.choiceId = ROSEMessage::errorCid; @@ -682,7 +682,7 @@ void SnaccROSEBase::CompleteAllPendingOperations() it->second->CompleteOperation(ROSE_TE_SHUTDOWN); } -bool SnaccROSEBase::IsProcessingAllowed() +bool SnaccROSEBase::IsProcessingAllowed() const { std::lock_guard guard(m_InternalProtectMutex); return m_bProcessingAllowed; @@ -817,17 +817,17 @@ bool SnaccROSEBase::OnBinaryDataBlockResult(const char* lpBytes, unsigned long l case SNACC::TransportEncoding::BER: { AsnBuf buffer((const char*)lpBytes, lSize); - auto pmessage = std::make_unique(); + auto pMessage = std::make_unique(); AsnLen bytesDecoded = 0; std::optional rejectCtx; try { - pmessage->BDec(buffer, bytesDecoded); - rejectCtx = InboundInvokeRejectContext::TryFrom(*pmessage); + pMessage->BDec(buffer, bytesDecoded); + rejectCtx = InboundInvokeRejectContext::TryFrom(*pMessage); if (bLogTransportData) LogTransportData(false, m_eTransportEncoding, nullptr, lpBytes, lSize, nullptr, nullptr); - bReturn = OnROSEMessage(std::move(pmessage), false, lSize); + bReturn = OnROSEMessage(std::move(pMessage), false, lSize); } catch (const SnaccException& ex) { @@ -854,18 +854,18 @@ bool SnaccROSEBase::OnBinaryDataBlockResult(const char* lpBytes, unsigned long l SJson::Reader reader; if (reader.parse((const char*)lpBytes + iHeaderLen, (const char*)lpBytes + lSize, value)) { - auto pmessage = std::make_unique(); + auto pMessage = std::make_unique(); std::optional rejectCtx; try { - if (!pmessage->JDec(value)) + if (!pMessage->JDec(value)) throw InvalidTagException("ROSEMessage", "decode failed: ROSEMessage", STACK_ENTRY); - rejectCtx = InboundInvokeRejectContext::TryFrom(*pmessage); + rejectCtx = InboundInvokeRejectContext::TryFrom(*pMessage); if (bLogTransportData) - LogTransportData(false, m_eTransportEncoding, nullptr, lpBytes, lSize, pmessage.get(), &value); + LogTransportData(false, m_eTransportEncoding, nullptr, lpBytes, lSize, pMessage.get(), &value); - bReturn = OnROSEMessage(std::move(pmessage), false, lSize); + bReturn = OnROSEMessage(std::move(pMessage), false, lSize); } catch (const SnaccException& ex) { @@ -932,7 +932,7 @@ int SnaccROSEBase::GetJsonHeaderLen(const char* lpBytes, unsigned long iLength) return iLen; } -std::string SnaccROSEBase::GetEncoded(const SNACC::TransportEncoding encoding, AsnType* pValue, unsigned long* pUlSize /* = nullptr */) +std::string SnaccROSEBase::GetEncoded(const SNACC::TransportEncoding encoding, const AsnType* pValue, unsigned long* pUlSize /* = nullptr */) { std::string strData; if (pUlSize) @@ -1005,17 +1005,17 @@ void SnaccROSEBase::OnBinaryDataBlock(const char* lpBytes, unsigned long ulSize, case SNACC::TransportEncoding::BER: { AsnBuf buffer((const char*)lpBytes, ulSize); - auto pmessage = std::make_unique(); + auto pMessage = std::make_unique(); AsnLen bytesDecoded = 0; std::optional rejectCtx; try { - pmessage->BDec(buffer, bytesDecoded); - rejectCtx = InboundInvokeRejectContext::TryFrom(*pmessage); + pMessage->BDec(buffer, bytesDecoded); + rejectCtx = InboundInvokeRejectContext::TryFrom(*pMessage); if (bLogTransportData) LogTransportData(false, SNACC::TransportEncoding::BER, nullptr, lpBytes, ulSize, nullptr, nullptr); - OnROSEMessage(std::move(pmessage), true, ulSize); + OnROSEMessage(std::move(pMessage), true, ulSize); } catch (const SnaccException& ex) { @@ -1042,16 +1042,16 @@ void SnaccROSEBase::OnBinaryDataBlock(const char* lpBytes, unsigned long ulSize, SJson::Reader reader; if (reader.parse((const char*)lpBytes + iHeaderLen, (const char*)lpBytes + ulSize, value)) { - auto pmessage = std::make_unique(); + auto pMessage = std::make_unique(); std::optional rejectCtx; try { - if (!pmessage->JDec(value)) + if (!pMessage->JDec(value)) throw InvalidTagException("ROSEMessage", "decode failed: ROSEMessage", STACK_ENTRY); - rejectCtx = InboundInvokeRejectContext::TryFrom(*pmessage); + rejectCtx = InboundInvokeRejectContext::TryFrom(*pMessage); if (bLogTransportData) - LogTransportData(false, m_eTransportEncoding, nullptr, lpBytes, ulSize, pmessage.get(), &value); - OnROSEMessage(std::move(pmessage), true, ulSize); + LogTransportData(false, m_eTransportEncoding, nullptr, lpBytes, ulSize, pMessage.get(), &value); + OnROSEMessage(std::move(pMessage), true, ulSize); } catch (const SnaccException& ex) { @@ -1396,7 +1396,7 @@ void SnaccROSEBase::OnInvokeMessage(std::unique_ptr pMessage OnInvokeProcessed(telemetry); } -void SnaccROSEBase::OnResultMessage(SNACC::ROSEResult& result, unsigned long ulMessageSize) +void SnaccROSEBase::OnResultMessage(const SNACC::ROSEResult& result, unsigned long ulMessageSize) { unsigned int uiOperationID = 0; std::string strOperationName; @@ -1404,7 +1404,7 @@ void SnaccROSEBase::OnResultMessage(SNACC::ROSEResult& result, unsigned long ulM OnInvokeProcessed(SnaccTelemetryData::CreateFinalized(SnaccTelemetryData::Direction::INBOUND, uiOperationID, strOperationName.c_str(), ulMessageSize, SnaccTelemetryData::Outcome::RESULT, SnaccTelemetryData::Stage::INBOUND_RESPONSE, SnaccTelemetryData::Reason::REMOTE_RESULT, ROSE_NOERROR)); } -void SnaccROSEBase::OnErrorMessage(SNACC::ROSEError& error, unsigned long ulMessageSize) +void SnaccROSEBase::OnErrorMessage(const SNACC::ROSEError& error, unsigned long ulMessageSize) { unsigned int uiOperationID = 0; std::string strOperationName; @@ -1412,7 +1412,7 @@ void SnaccROSEBase::OnErrorMessage(SNACC::ROSEError& error, unsigned long ulMess OnInvokeProcessed(SnaccTelemetryData::CreateFinalized(SnaccTelemetryData::Direction::INBOUND, uiOperationID, strOperationName.c_str(), ulMessageSize, SnaccTelemetryData::Outcome::ERR, SnaccTelemetryData::Stage::INBOUND_RESPONSE, SnaccTelemetryData::Reason::REMOTE_ERROR, ROSE_ERROR_VALUE)); } -void SnaccROSEBase::OnRejectMessage(SNACC::ROSEReject& reject, unsigned long ulMessageSize) +void SnaccROSEBase::OnRejectMessage(const SNACC::ROSEReject& reject, unsigned long ulMessageSize) { unsigned int uiOperationID = 0; std::string strOperationName; @@ -1681,7 +1681,7 @@ long SnaccROSEBase::DecodeResponse(const SNACC::ROSEMessage& response, SNACC::RO return lRoseResult; } -long SnaccROSEBase::EncodeResult(unsigned int uiInvokeID, SNACC::AsnType* pResult, std::string& strResponse, const wchar_t* szSessionID /*= nullptr*/) +long SnaccROSEBase::EncodeResult(unsigned int uiInvokeID, const SNACC::AsnType* pResult, std::string& strResponse, const wchar_t* szSessionID /*= nullptr*/) { long lRoseResult = ROSE_NOERROR; RoseEncodeResultEnvelope encode(uiInvokeID, pResult, szSessionID); @@ -1727,7 +1727,7 @@ long SnaccROSEBase::EncodeResult(unsigned int uiInvokeID, SNACC::AsnType* pResul return lRoseResult; } -long SnaccROSEBase::EncodeError(unsigned int uiInvokeID, SNACC::AsnType* pError, std::string& strResponse, const wchar_t* szSessionID /*= nullptr*/) +long SnaccROSEBase::EncodeError(unsigned int uiInvokeID, const SNACC::AsnType* pError, std::string& strResponse, const wchar_t* szSessionID /*= nullptr*/) { long lRoseResult = ROSE_NOERROR; RoseEncodeErrorEnvelope encode(uiInvokeID, pError, szSessionID); @@ -1800,7 +1800,7 @@ SNACC::TransportEncoding SnaccROSEBase::GetTransportEncoding() const return m_eTransportEncoding; } -long SnaccROSEBase::HandleInvokeResult(long lRoseResult, SNACC::ROSEMessage& responseMsg, SNACC::AsnType* result, SNACC::AsnType* error, SnaccInvokeContext& ctx) +long SnaccROSEBase::HandleInvokeResult(long lRoseResult, const SNACC::ROSEMessage& responseMsg, SNACC::AsnType* result, SNACC::AsnType* error, SnaccInvokeContext& ctx) { // In case of transport errors, we hand that value back as we have no response to parse if (ISROSE_TE(lRoseResult)) @@ -1829,19 +1829,23 @@ long SnaccROSEBase::HandleInvokeResult(long lRoseResult, SNACC::ROSEMessage& res int logLevel = (int)GetLogLevel(false); if (logLevel & ((int)SNACC::EAsnLogLevel::JSON | (int)SNACC::EAsnLogLevel::JSON_ALWAYS_PRETTY_PRINTED)) { + // LogTransportData needs the decoded payload inside the ROSE tree for JEnc(). + // Graft temporarily, then restore below — the owned message is unchanged afterward. + // In-place graft avoids copying ROSEMessage and duplicating the BER anyBuf. + auto& logMsg = const_cast(responseMsg); // Backup the original response object inside the response message - ROSEResultSeq* pOriginalResponse = responseMsg.result->result; + ROSEResultSeq* pOriginalResponse = logMsg.result->result; // Set the decoded result object into the response to be able to fully log the whole message - responseMsg.result->result = new ROSEResultSeq(); - responseMsg.result->result->result.value = result; - LogTransportData(false, SNACC::TransportEncoding::BER, nullptr, nullptr, 0, &responseMsg, nullptr); + logMsg.result->result = new ROSEResultSeq(); + logMsg.result->result->result.value = result; + LogTransportData(false, SNACC::TransportEncoding::BER, nullptr, nullptr, 0, &logMsg, nullptr); // As we hand back the result object to the outer world (function argument) we need to set it to NULL to prevent deletion if we discard the inserted object - responseMsg.result->result->result.value = NULL; + logMsg.result->result->result.value = NULL; // Delete the inserted result object and reset to the original response object - delete responseMsg.result->result; - responseMsg.result->result = pOriginalResponse; + delete logMsg.result->result; + logMsg.result->result = pOriginalResponse; } } else if (pResult->result->result.jsonBuf) @@ -1883,19 +1887,23 @@ long SnaccROSEBase::HandleInvokeResult(long lRoseResult, SNACC::ROSEMessage& res int logLevel = (int)GetLogLevel(false); if (logLevel & ((int)SNACC::EAsnLogLevel::JSON | (int)SNACC::EAsnLogLevel::JSON_ALWAYS_PRETTY_PRINTED)) { + // LogTransportData needs the decoded payload inside the ROSE tree for JEnc(). + // Graft temporarily, then restore below — the owned message is unchanged afterward. + // In-place graft avoids copying ROSEMessage and duplicating the BER anyBuf. + auto& logMsg = const_cast(responseMsg); // Backup the original error object inside the response message - AsnAny* pOriginalError = responseMsg.error->error; + AsnAny* pOriginalError = logMsg.error->error; // Set the decoded error object into the response to be able to fully log the whole message - responseMsg.error->error = new AsnAny(); - responseMsg.error->error->value = error; - LogTransportData(false, SNACC::TransportEncoding::BER, nullptr, nullptr, 0, &responseMsg, nullptr); + logMsg.error->error = new AsnAny(); + logMsg.error->error->value = error; + LogTransportData(false, SNACC::TransportEncoding::BER, nullptr, nullptr, 0, &logMsg, nullptr); // As we hand back the error object to the outer world (function argument) we need to set it to NULL to prevent deletion if we discard the inserted object - responseMsg.error->error->value = NULL; + logMsg.error->error->value = NULL; // Delete the inserted error object and reset to the original response object - delete responseMsg.error->error; - responseMsg.error->error = pOriginalError; + delete logMsg.error->error; + logMsg.error->error = pOriginalError; } } else if (pError->error->jsonBuf) @@ -1922,7 +1930,7 @@ long SnaccROSEBase::HandleInvokeResult(long lRoseResult, SNACC::ROSEMessage& res return lRoseResult; } -long SnaccROSEBase::HandleOnInvokeResult(SNACC::InvokeResult invokeResult, SNACC::ROSEInvoke& invoke, SnaccInvokeContext& ctx, std::string& strResponse, SNACC::AsnType* pResult, SNACC::AsnType* pError) +long SnaccROSEBase::HandleOnInvokeResult(SNACC::InvokeResult invokeResult, const SNACC::ROSEInvoke& invoke, SnaccInvokeContext& ctx, std::string& strResponse, SNACC::AsnType* pResult, SNACC::AsnType* pError) { if ((AsnIntType)invoke.invokeID == 99999) { @@ -1943,9 +1951,9 @@ long SnaccROSEBase::HandleOnInvokeResult(SNACC::InvokeResult invokeResult, SNACC } } -long SnaccROSEBase::DecodeInvoke(SNACC::ROSEMessage& message, SNACC::AsnType* argument) +long SnaccROSEBase::DecodeInvoke(const SNACC::ROSEMessage& invokeMessage, SNACC::AsnType* argument) { - auto& invoke = *message.invoke; + auto& invoke = *invokeMessage.invoke; if (!invoke.argument) { if (argument->mayBeEmpty()) @@ -1971,30 +1979,35 @@ long SnaccROSEBase::DecodeInvoke(SNACC::ROSEMessage& message, SNACC::AsnType* ar int logLevel = (int)GetLogLevel(false); if (logLevel & ((int)SNACC::EAsnLogLevel::JSON | (int)SNACC::EAsnLogLevel::JSON_ALWAYS_PRETTY_PRINTED)) { + // LogTransportData needs the decoded payload inside the ROSE tree for JEnc(). + // Graft temporarily, then restore below — the owned message is unchanged afterward. + // In-place graft avoids copying ROSEMessage and duplicating the BER anyBuf. + auto& logMsg = const_cast(invokeMessage); + auto& logInvoke = *logMsg.invoke; // Backup the original response object inside the response message - AsnAny* pOriginalArgument = invoke.argument; + AsnAny* pOriginalArgument = logInvoke.argument; // Set the decoded result object into the response to be able to fully log the whole message - invoke.argument = new AsnAny(); - invoke.argument->value = argument; + logInvoke.argument = new AsnAny(); + logInvoke.argument->value = argument; // Get the name of the called operation for logging std::string strOperationName; const char* szOperationName = nullptr; - if (invoke.operationName) + if (logInvoke.operationName) { - strOperationName = invoke.operationName->getUTF8(); + strOperationName = logInvoke.operationName->getUTF8(); szOperationName = strOperationName.c_str(); } if (!szOperationName) - szOperationName = SnaccRoseOperationLookup::LookUpName(invoke.operationID); - LogTransportData(false, SNACC::TransportEncoding::BER, szOperationName, nullptr, 0, &message, nullptr); + szOperationName = SnaccRoseOperationLookup::LookUpName(logInvoke.operationID); + LogTransportData(false, SNACC::TransportEncoding::BER, szOperationName, nullptr, 0, &logMsg, nullptr); // As we hand back the result object to the outer world (function argument) we need to set it to NULL to prevent deletion if we discard the inserted object - invoke.argument->value = NULL; + logInvoke.argument->value = NULL; // Delete the inserted result object and reset to the original response object - delete invoke.argument; - invoke.argument = pOriginalArgument; + delete logInvoke.argument; + logInvoke.argument = pOriginalArgument; } } else if (invoke.argument->jsonBuf) diff --git a/cpp-lib/tests/test_support/sample_runtime_harness.h b/cpp-lib/tests/test_support/sample_runtime_harness.h index 8e7b0ae..0e1b89f 100644 --- a/cpp-lib/tests/test_support/sample_runtime_harness.h +++ b/cpp-lib/tests/test_support/sample_runtime_harness.h @@ -555,13 +555,13 @@ class RuntimeEndpoint : public SnaccROSEBase } // Ensures outbound result payloads inherit the session id of this connection. - long EncodeResult(unsigned int uiInvokeID, SNACC::AsnType* pResult, std::string& strResponse, const wchar_t* szSessionID = nullptr) override + long EncodeResult(unsigned int uiInvokeID, const SNACC::AsnType* pResult, std::string& strResponse, const wchar_t* szSessionID = nullptr) override { return SnaccROSEBase::EncodeResult(uiInvokeID, pResult, strResponse, szSessionID ? szSessionID : m_wstrSessionId.c_str()); } // Ensures outbound error payloads inherit the session id of this connection. - long EncodeError(unsigned int uiInvokeID, SNACC::AsnType* pError, std::string& strResponse, const wchar_t* szSessionID = nullptr) override + long EncodeError(unsigned int uiInvokeID, const SNACC::AsnType* pError, std::string& strResponse, const wchar_t* szSessionID = nullptr) override { return SnaccROSEBase::EncodeError(uiInvokeID, pError, strResponse, szSessionID ? szSessionID : m_wstrSessionId.c_str()); } From 0096ff98779ed7dc56f20240a13c103d8702ec75 Mon Sep 17 00:00:00 2001 From: Jan Fellner Date: Tue, 7 Jul 2026 15:27:53 +0200 Subject: [PATCH 10/10] UCAAS-1446 Standardize ROSE API pointer parameter naming. Use p-prefix names for AsnType and ROSE invoke/reject pointers across runtime interfaces and implementations. --- cpp-lib/include/SnaccROSEBase.h | 26 +++--- cpp-lib/include/SnaccROSEInterfaces.h | 22 ++--- cpp-lib/src/SnaccROSEBase.cpp | 80 +++++++++---------- .../test_support/sample_runtime_harness.h | 12 +-- 4 files changed, 70 insertions(+), 70 deletions(-) diff --git a/cpp-lib/include/SnaccROSEBase.h b/cpp-lib/include/SnaccROSEBase.h index 9865179..09da43c 100644 --- a/cpp-lib/include/SnaccROSEBase.h +++ b/cpp-lib/include/SnaccROSEBase.h @@ -218,8 +218,8 @@ class SnaccROSEBase : public SnaccROSESender, public SnaccTelemetryCallback virtual long EncodeResult(unsigned int uiInvokeID, const SNACC::AsnType* pResult, std::string& strResponse, const wchar_t* szSessionID = nullptr) override; /* Send a Reject Message. */ - long EncodeReject(SNACC::ROSEReject* preject, std::string& strResponse); - long SendRejectEx(SNACC::ROSEReject* preject, SnaccInvokeContext& ctx); + long EncodeReject(SNACC::ROSEReject* pReject, std::string& strResponse); + long SendRejectEx(SNACC::ROSEReject* pReject, SnaccInvokeContext& ctx); /* * Encodes a reject as repsonse to an invoke @@ -265,15 +265,15 @@ class SnaccROSEBase : public SnaccROSESender, public SnaccTelemetryCallback /** * An invoke that is send to the other side. Should only be called by the ROSE stub itself generated files * - * pinvoke - the invoke payload (it is put into a ROSEMessage in the function) - * result - decoded result payload in case a result response is received - * error - decoded error payload in case an error response is received + * pInvoke - the invoke payload (it is put into a ROSEMessage in the function) + * pResult - decoded result payload in case a result response is received + * pError - decoded error payload in case an error response is received * szOperationName - the operationName (for logging purposes) * iTimeout - the timeout in milliseconds (-1 uses default m_lMaxInvokeWait, 0 returns immediately without waiting for the result) * pCtx - contextual data for the invoke. The caller may keep another shared reference * to inspect changes after the call. */ - virtual long SendInvoke(SNACC::ROSEInvoke* pinvoke, SNACC::AsnType* result, SNACC::AsnType* error, const char* szOperationName, int iTimeout = -1, std::shared_ptr pCtx = {}) override; + virtual long SendInvoke(SNACC::ROSEInvoke* pInvoke, SNACC::AsnType* pResult, SNACC::AsnType* pError, const char* szOperationName, int iTimeout = -1, std::shared_ptr pCtx = {}) override; /** * Encodes the result or error from an OnInvoke request. Retrieves the result or error from the response @@ -293,20 +293,20 @@ class SnaccROSEBase : public SnaccROSESender, public SnaccTelemetryCallback * * lRoseResult - the current ROSE result code * responseMsg - owned pending-op response; read-only for callers and overrides - * result - the result object (Base type pointer, the caller of the invoke provides the proper type) - * error - the error object (Base type pointer, the caller of the invoke provides the proper type) + * pResult - the result object (Base type pointer, the caller of the invoke provides the proper type) + * pError - the error object (Base type pointer, the caller of the invoke provides the proper type) * ctx - contextual data for the invoke */ - virtual long HandleInvokeResult(long lRoseResult, const SNACC::ROSEMessage& responseMsg, SNACC::AsnType* result, SNACC::AsnType* error, SnaccInvokeContext& ctx) override; + virtual long HandleInvokeResult(long lRoseResult, const SNACC::ROSEMessage& responseMsg, SNACC::AsnType* pResult, SNACC::AsnType* pError, SnaccInvokeContext& ctx) override; /** * An event (invoke without result) that is send to the other side. Should only be called by the ROSE stub itself generated files * - * pinvoke - the invoke payload (it is put into a ROSEMessage in the function) + * pInvoke - the invoke payload (it is put into a ROSEMessage in the function) * pCtx - contextual data for the invoke. The caller may keep another shared reference * to inspect changes after the call. */ - virtual long SendEvent(SNACC::ROSEInvoke* pinvoke, const char* szOperationName, std::shared_ptr pCtx = {}) override; + virtual long SendEvent(SNACC::ROSEInvoke* pInvoke, const char* szOperationName, std::shared_ptr pCtx = {}) override; /** * Creates the invoke context for inbound and outbound invoke lifecycles. @@ -317,9 +317,9 @@ class SnaccROSEBase : public SnaccROSESender, public SnaccTelemetryCallback * Decodes an invoke argument and optionally logs the full message (BER JSON). * * invokeMessage - inbound invoke ROSEMessage; borrow only for decode/logging - * argument - the argument object (Base type pointer, the caller of the provides the proper type) + * pArgument - the argument object (Base type pointer, the caller of the provides the proper type) */ - long DecodeInvoke(const SNACC::ROSEMessage& invokeMessage, SNACC::AsnType* argument) override; + long DecodeInvoke(const SNACC::ROSEMessage& invokeMessage, SNACC::AsnType* pArgument) override; protected: // Get the length prefix for a given strJson payload diff --git a/cpp-lib/include/SnaccROSEInterfaces.h b/cpp-lib/include/SnaccROSEInterfaces.h index c4c2a2d..45a9ea7 100644 --- a/cpp-lib/include/SnaccROSEInterfaces.h +++ b/cpp-lib/include/SnaccROSEInterfaces.h @@ -209,25 +209,25 @@ class SnaccROSESender /** An invoke that is send to the other side. Should only be called by the ROSE stub itself generated files * * pInvoke - the invoke payload (it is put into a ROSEMessage in the function) - * result - decoded result payload in case a result response is received - * error - decoded error payload in case an error response is received + * pResult - decoded result payload in case a result response is received + * pError - decoded error payload in case an error response is received * szOperationName - the operationName (for logging purposes) * iTimeout - the timeout in milliseconds (-1 uses default m_lMaxInvokeWait, 0 returns immediately without waiting for the result) * pCtx - contextual data for the invoke. The caller may keep another shared reference * to inspect changes after the call. */ - virtual long SendInvoke(SNACC::ROSEInvoke* pInvoke, SNACC::AsnType* result, SNACC::AsnType* error, const char* szOperationName, int iTimeout = -1, std::shared_ptr pCtx = {}) = 0; + virtual long SendInvoke(SNACC::ROSEInvoke* pInvoke, SNACC::AsnType* pResult, SNACC::AsnType* pError, const char* szOperationName, int iTimeout = -1, std::shared_ptr pCtx = {}) = 0; /** * Handles the response payload of the SendInvoke method. Retrieves the result or error from the response * * lRoseResult - the result of the SendInvoke method * responseMsg - owned pending-op response; read-only for callers and overrides - * result - the result object (Base type pointer, the caller of the invoke provides the proper type) - * error - the error object (Base type pointer, the caller of the invoke provides the proper type) + * pResult - the result object (Base type pointer, the caller of the invoke provides the proper type) + * pError - the error object (Base type pointer, the caller of the invoke provides the proper type) * ctx - contextual data for the invoke */ - virtual long HandleInvokeResult(long lRoseResult, const SNACC::ROSEMessage& responseMsg, SNACC::AsnType* result, SNACC::AsnType* error, SnaccInvokeContext& ctx) = 0; + virtual long HandleInvokeResult(long lRoseResult, const SNACC::ROSEMessage& responseMsg, SNACC::AsnType* pResult, SNACC::AsnType* pError, SnaccInvokeContext& ctx) = 0; /** * Encodes the result or error from an OnInvoke request. Retrieves the result or error from the response @@ -236,8 +236,8 @@ class SnaccROSESender * invoke - the original inbound invoke envelope (borrowed from owned ROSEMessage) * ctx - contextual data for the invoke * strResponse - the encoded result to put it on the transport - * result - the result object (Base type pointer, the caller of the invoke provides the proper type) - * error - the error object (Base type pointer, the caller of the invoke provides the proper type) + * pResult - the result object (Base type pointer, the caller of the invoke provides the proper type) + * pError - the error object (Base type pointer, the caller of the invoke provides the proper type) */ virtual long HandleOnInvokeResult(SNACC::InvokeResult invokeResult, const SNACC::ROSEInvoke& invoke, SnaccInvokeContext& ctx, std::string& strResponse, SNACC::AsnType* pResult, SNACC::AsnType* pError) = 0; @@ -245,9 +245,9 @@ class SnaccROSESender * Decodes an invoke and properly handles logging for it * * invokeMessage - inbound invoke ROSEMessage; borrow only for decode/logging - * argument - the argument object (Base type pointer, the caller of the provides the proper type) + * pArgument - the argument object (Base type pointer, the caller of the provides the proper type) */ - virtual long DecodeInvoke(const SNACC::ROSEMessage& invokeMessage, SNACC::AsnType* argument) = 0; + virtual long DecodeInvoke(const SNACC::ROSEMessage& invokeMessage, SNACC::AsnType* pArgument) = 0; /** An event (invoke without result) that is send to the other side. Should only be called by the ROSE stub itself generated files * @@ -282,7 +282,7 @@ class SnaccROSESender class SnaccScopedInvokeMessage { public: - explicit SnaccScopedInvokeMessage(long invokeID, unsigned int uiOperationID, SNACC::AsnType* argument); + explicit SnaccScopedInvokeMessage(long invokeID, unsigned int uiOperationID, SNACC::AsnType* pArgument); SnaccScopedInvokeMessage(const SnaccScopedInvokeMessage&) = delete; SnaccScopedInvokeMessage& operator=(const SnaccScopedInvokeMessage&) = delete; SnaccScopedInvokeMessage(SnaccScopedInvokeMessage&&) = delete; diff --git a/cpp-lib/src/SnaccROSEBase.cpp b/cpp-lib/src/SnaccROSEBase.cpp index df794e2..61103dc 100644 --- a/cpp-lib/src/SnaccROSEBase.cpp +++ b/cpp-lib/src/SnaccROSEBase.cpp @@ -222,13 +222,13 @@ std::shared_ptr SnaccInvokeContext::Clone() const return std::shared_ptr(new SnaccInvokeContext(*this)); } -SnaccScopedInvokeMessage::SnaccScopedInvokeMessage(long invokeID, unsigned int uiOperationID, SNACC::AsnType* argument) +SnaccScopedInvokeMessage::SnaccScopedInvokeMessage(long invokeID, unsigned int uiOperationID, SNACC::AsnType* pArgument) : m_pInvoke(new SNACC::ROSEInvoke()) { m_pInvoke->invokeID = invokeID; m_pInvoke->operationID = uiOperationID; m_pInvoke->argument = new AsnAny; - m_pInvoke->argument->value = argument; + m_pInvoke->argument->value = pArgument; } SnaccScopedInvokeMessage::~SnaccScopedInvokeMessage() @@ -1155,12 +1155,12 @@ bool SnaccROSEBase::OnROSEMessage(std::unique_ptr pMessage, return bProcessed; } -long SnaccROSEBase::EncodeReject(SNACC::ROSEReject* preject, std::string& strResponse) +long SnaccROSEBase::EncodeReject(SNACC::ROSEReject* pReject, std::string& strResponse) { long lRoseResult = ROSE_NOERROR; ROSEMessage rejectMsg; - const RoseEncodeRejectBorrow rejectBorrow(rejectMsg, *preject); + const RoseEncodeRejectBorrow rejectBorrow(rejectMsg, *pReject); // encode now. if (m_eTransportEncoding == SNACC::TransportEncoding::BER) @@ -1204,10 +1204,10 @@ long SnaccROSEBase::EncodeReject(SNACC::ROSEReject* preject, std::string& strRes return lRoseResult; } -long SnaccROSEBase::SendRejectEx(SNACC::ROSEReject* preject, SnaccInvokeContext& ctx) +long SnaccROSEBase::SendRejectEx(SNACC::ROSEReject* pReject, SnaccInvokeContext& ctx) { std::string strResponse; - auto lResult = EncodeReject(preject, strResponse); + auto lResult = EncodeReject(pReject, strResponse); if (lResult) return lResult; @@ -1480,19 +1480,19 @@ long SnaccROSEBase::Send(SNACC::ROSEInvoke* pInvoke, const char* szOperationName return lRoseResult; } -long SnaccROSEBase::SendEvent(SNACC::ROSEInvoke* pinvoke, const char* szOperationName, std::shared_ptr pCtx /*= {}*/) +long SnaccROSEBase::SendEvent(SNACC::ROSEInvoke* pInvoke, const char* szOperationName, std::shared_ptr pCtx /*= {}*/) { const auto chronoCreated = std::chrono::steady_clock::now(); - const char* szResolvedOperationName = SnaccRoseOperationLookup::LookUpName(pinvoke->operationID); + const char* szResolvedOperationName = SnaccRoseOperationLookup::LookUpName(pInvoke->operationID); if (!pCtx) { - SnaccInvokeContextInit init(SnaccInvokeDirection::OUTBOUND, pinvoke, szResolvedOperationName ? szResolvedOperationName : szOperationName); + SnaccInvokeContextInit init(SnaccInvokeDirection::OUTBOUND, pInvoke, szResolvedOperationName ? szResolvedOperationName : szOperationName); pCtx = CreateInvokeContext(init); } auto& ctx = *pCtx; size_t stRequestData = 0; - const long lRoseResult = IsProcessingAllowed() ? Send(pinvoke, szOperationName, ctx, &stRequestData) : ROSE_TE_SHUTDOWN; - auto telemetry = SnaccTelemetryData::Create(SnaccTelemetryData::Direction::OUTBOUND, pinvoke->operationID, szResolvedOperationName, stRequestData, chronoCreated); + const long lRoseResult = IsProcessingAllowed() ? Send(pInvoke, szOperationName, ctx, &stRequestData) : ROSE_TE_SHUTDOWN; + auto telemetry = SnaccTelemetryData::Create(SnaccTelemetryData::Direction::OUTBOUND, pInvoke->operationID, szResolvedOperationName, stRequestData, chronoCreated); telemetry->finalize(lRoseResult == ROSE_NOERROR ? SnaccTelemetryData::Outcome::EVENT : SnaccTelemetryData::Outcome::UNHANDLED, SnaccTelemetryData::Stage::OUTBOUND_SEND, lRoseResult == ROSE_NOERROR ? SnaccTelemetryData::Reason::LOCAL_EVENT : GetUnhandledReasonFromResult(lRoseResult), lRoseResult, std::nullopt, pCtx); OnInvokeProcessed(telemetry); return lRoseResult; @@ -1583,32 +1583,32 @@ bool SnaccROSEBase::LogTransportData(const bool bOutbound, const SNACC::Transpor return bTransportDataWasLogged; } -long SnaccROSEBase::SendInvoke(SNACC::ROSEInvoke* pinvoke, SNACC::AsnType* result, SNACC::AsnType* error, const char* szOperationName /*= nullptr*/, int iTimeout /*= -1*/, std::shared_ptr pCtx /*= {}*/) +long SnaccROSEBase::SendInvoke(SNACC::ROSEInvoke* pInvoke, SNACC::AsnType* pResult, SNACC::AsnType* pError, const char* szOperationName /*= nullptr*/, int iTimeout /*= -1*/, std::shared_ptr pCtx /*= {}*/) { const auto chronoCreated = std::chrono::steady_clock::now(); - const char* szResolvedOperationName = SnaccRoseOperationLookup::LookUpName(pinvoke->operationID); + const char* szResolvedOperationName = SnaccRoseOperationLookup::LookUpName(pInvoke->operationID); // Ensure that we always have a ctx if (!pCtx) { - SnaccInvokeContextInit init(SnaccInvokeDirection::OUTBOUND, pinvoke, szResolvedOperationName ? szResolvedOperationName : szOperationName); + SnaccInvokeContextInit init(SnaccInvokeDirection::OUTBOUND, pInvoke, szResolvedOperationName ? szResolvedOperationName : szOperationName); pCtx = CreateInvokeContext(init); } auto& ctx = *pCtx; if (!IsProcessingAllowed()) { - auto telemetry = SnaccTelemetryData::Create(SnaccTelemetryData::Direction::OUTBOUND, pinvoke->operationID, szResolvedOperationName, 0, chronoCreated); + auto telemetry = SnaccTelemetryData::Create(SnaccTelemetryData::Direction::OUTBOUND, pInvoke->operationID, szResolvedOperationName, 0, chronoCreated); telemetry->finalize(SnaccTelemetryData::Outcome::UNHANDLED, SnaccTelemetryData::Stage::OUTBOUND_SEND, SnaccTelemetryData::Reason::SHUTDOWN, ROSE_TE_SHUTDOWN, std::nullopt, std::move(pCtx)); OnInvokeProcessed(telemetry); return ROSE_TE_SHUTDOWN; } - auto& pendingOP = AddPendingOperation(pinvoke->invokeID, pinvoke->operationID, szResolvedOperationName); + auto& pendingOP = AddPendingOperation(pInvoke->invokeID, pInvoke->operationID, szResolvedOperationName); size_t stRequestData = 0; - long lRoseResult = Send(pinvoke, szOperationName, ctx, &stRequestData); - pendingOP.m_pTelemetry = SnaccTelemetryData::Create(SnaccTelemetryData::Direction::OUTBOUND, pinvoke->operationID, szResolvedOperationName, stRequestData, chronoCreated); + long lRoseResult = Send(pInvoke, szOperationName, ctx, &stRequestData); + pendingOP.m_pTelemetry = SnaccTelemetryData::Create(SnaccTelemetryData::Direction::OUTBOUND, pInvoke->operationID, szResolvedOperationName, stRequestData, chronoCreated); if (lRoseResult == 0) { @@ -1643,7 +1643,7 @@ long SnaccROSEBase::SendInvoke(SNACC::ROSEInvoke* pinvoke, SNACC::AsnType* resul pendingOP.m_lRoseResult = lRoseResult; if (pendingOP.m_pAnswerMessage) - lRoseResult = HandleInvokeResult(lRoseResult, *pendingOP.m_pAnswerMessage, result, error, ctx); + lRoseResult = HandleInvokeResult(lRoseResult, *pendingOP.m_pAnswerMessage, pResult, pError, ctx); pendingOP.FinalizeTelemetry(lRoseResult, pCtx); OnInvokeProcessed(pendingOP.m_pTelemetry); @@ -1800,26 +1800,26 @@ SNACC::TransportEncoding SnaccROSEBase::GetTransportEncoding() const return m_eTransportEncoding; } -long SnaccROSEBase::HandleInvokeResult(long lRoseResult, const SNACC::ROSEMessage& responseMsg, SNACC::AsnType* result, SNACC::AsnType* error, SnaccInvokeContext& ctx) +long SnaccROSEBase::HandleInvokeResult(long lRoseResult, const SNACC::ROSEMessage& responseMsg, SNACC::AsnType* pResult, SNACC::AsnType* pError, SnaccInvokeContext& ctx) { // In case of transport errors, we hand that value back as we have no response to parse if (ISROSE_TE(lRoseResult)) return lRoseResult; - SNACC::ROSEError* pError = nullptr; - SNACC::ROSEResult* pResult = nullptr; - lRoseResult = DecodeResponse(responseMsg, pResult, pError, ctx); + SNACC::ROSEError* pRoseError = nullptr; + SNACC::ROSEResult* pRoseResult = nullptr; + lRoseResult = DecodeResponse(responseMsg, pRoseResult, pRoseError, ctx); if (lRoseResult == ROSE_NOERROR) { - if (pResult && pResult->result && result) + if (pRoseResult && pRoseResult->result && pResult) { AsnLen len; try { - if (pResult->result->result.anyBuf) + if (pRoseResult->result->result.anyBuf) { - result->BDec(*pResult->result->result.anyBuf, len); + pResult->BDec(*pRoseResult->result->result.anyBuf, len); // Special to log the *full* ROSE Message in json // While for JSON Transport we can simply decode the full message on BER we need to decode the envelop and the payload // @@ -1838,7 +1838,7 @@ long SnaccROSEBase::HandleInvokeResult(long lRoseResult, const SNACC::ROSEMessag // Set the decoded result object into the response to be able to fully log the whole message logMsg.result->result = new ROSEResultSeq(); - logMsg.result->result->result.value = result; + logMsg.result->result->result.value = pResult; LogTransportData(false, SNACC::TransportEncoding::BER, nullptr, nullptr, 0, &logMsg, nullptr); // As we hand back the result object to the outer world (function argument) we need to set it to NULL to prevent deletion if we discard the inserted object logMsg.result->result->result.value = NULL; @@ -1848,9 +1848,9 @@ long SnaccROSEBase::HandleInvokeResult(long lRoseResult, const SNACC::ROSEMessag logMsg.result->result = pOriginalResponse; } } - else if (pResult->result->result.jsonBuf) + else if (pRoseResult->result->result.jsonBuf) { - if (!result->JDec(*pResult->result->result.jsonBuf)) + if (!pResult->JDec(*pRoseResult->result->result.jsonBuf)) lRoseResult = ROSE_RE_DECODE_FAILED; // No logging here as the full object has already been logged in OnBinaryDataBlock } @@ -1870,14 +1870,14 @@ long SnaccROSEBase::HandleInvokeResult(long lRoseResult, const SNACC::ROSEMessag } else if (lRoseResult == ROSE_ERROR_VALUE) { - if (pError && pError->error && error) + if (pRoseError && pRoseError->error && pError) { AsnLen len; try { - if (pError->error->anyBuf) + if (pRoseError->error->anyBuf) { - error->BDec(*pError->error->anyBuf, len); + pError->BDec(*pRoseError->error->anyBuf, len); // Special to log the *full* ROSE Message in json // While for JSON Transport we can simply decode the full message on BER we need to decode the envelop and the payload // @@ -1896,7 +1896,7 @@ long SnaccROSEBase::HandleInvokeResult(long lRoseResult, const SNACC::ROSEMessag // Set the decoded error object into the response to be able to fully log the whole message logMsg.error->error = new AsnAny(); - logMsg.error->error->value = error; + logMsg.error->error->value = pError; LogTransportData(false, SNACC::TransportEncoding::BER, nullptr, nullptr, 0, &logMsg, nullptr); // As we hand back the error object to the outer world (function argument) we need to set it to NULL to prevent deletion if we discard the inserted object logMsg.error->error->value = NULL; @@ -1906,9 +1906,9 @@ long SnaccROSEBase::HandleInvokeResult(long lRoseResult, const SNACC::ROSEMessag logMsg.error->error = pOriginalError; } } - else if (pError->error->jsonBuf) + else if (pRoseError->error->jsonBuf) { - if (!error->JDec(*pError->error->jsonBuf)) + if (!pError->JDec(*pRoseError->error->jsonBuf)) lRoseResult = ROSE_RE_DECODE_FAILED; // No logging here as the full object has already been logged in OnBinaryDataBlock } @@ -1951,12 +1951,12 @@ long SnaccROSEBase::HandleOnInvokeResult(SNACC::InvokeResult invokeResult, const } } -long SnaccROSEBase::DecodeInvoke(const SNACC::ROSEMessage& invokeMessage, SNACC::AsnType* argument) +long SnaccROSEBase::DecodeInvoke(const SNACC::ROSEMessage& invokeMessage, SNACC::AsnType* pArgument) { auto& invoke = *invokeMessage.invoke; if (!invoke.argument) { - if (argument->mayBeEmpty()) + if (pArgument->mayBeEmpty()) return ROSE_NOERROR; else return ROSE_REJECT_ARGUMENT_MISSING; @@ -1969,7 +1969,7 @@ long SnaccROSEBase::DecodeInvoke(const SNACC::ROSEMessage& invokeMessage, SNACC: if (invoke.argument->anyBuf) { AsnLen len = 0; - argument->BDec(*invoke.argument->anyBuf, len); + pArgument->BDec(*invoke.argument->anyBuf, len); // Special to log the *full* ROSE Message in json // While for JSON Transport we can simply decode the full message on BER we need to decode the envelop and the payload // @@ -1989,7 +1989,7 @@ long SnaccROSEBase::DecodeInvoke(const SNACC::ROSEMessage& invokeMessage, SNACC: // Set the decoded result object into the response to be able to fully log the whole message logInvoke.argument = new AsnAny(); - logInvoke.argument->value = argument; + logInvoke.argument->value = pArgument; // Get the name of the called operation for logging std::string strOperationName; @@ -2012,7 +2012,7 @@ long SnaccROSEBase::DecodeInvoke(const SNACC::ROSEMessage& invokeMessage, SNACC: } else if (invoke.argument->jsonBuf) { - argument->JDec(*invoke.argument->jsonBuf); + pArgument->JDec(*invoke.argument->jsonBuf); // No logging here as the full object has already been logged in OnBinaryDataBlock } else diff --git a/cpp-lib/tests/test_support/sample_runtime_harness.h b/cpp-lib/tests/test_support/sample_runtime_harness.h index 0e1b89f..f8c9ec0 100644 --- a/cpp-lib/tests/test_support/sample_runtime_harness.h +++ b/cpp-lib/tests/test_support/sample_runtime_harness.h @@ -541,17 +541,17 @@ class RuntimeEndpoint : public SnaccROSEBase } // Attaches the endpoint session id before the base runtime encodes an invoke. - long SendInvoke(SNACC::ROSEInvoke* pinvoke, SNACC::AsnType* result, SNACC::AsnType* error, const char* szOperationName, int iTimeout = -1, std::shared_ptr pCtx = {}) override + long SendInvoke(SNACC::ROSEInvoke* pInvoke, SNACC::AsnType* pResult, SNACC::AsnType* pError, const char* szOperationName, int iTimeout = -1, std::shared_ptr pCtx = {}) override { - AttachSessionId(pinvoke); - return SnaccROSEBase::SendInvoke(pinvoke, result, error, szOperationName, iTimeout, std::move(pCtx)); + AttachSessionId(pInvoke); + return SnaccROSEBase::SendInvoke(pInvoke, pResult, pError, szOperationName, iTimeout, std::move(pCtx)); } // Attaches the endpoint session id before the base runtime encodes an event. - long SendEvent(SNACC::ROSEInvoke* pinvoke, const char* szOperationName, std::shared_ptr pCtx = {}) override + long SendEvent(SNACC::ROSEInvoke* pInvoke, const char* szOperationName, std::shared_ptr pCtx = {}) override { - AttachSessionId(pinvoke); - return SnaccROSEBase::SendEvent(pinvoke, szOperationName, std::move(pCtx)); + AttachSessionId(pInvoke); + return SnaccROSEBase::SendEvent(pInvoke, szOperationName, std::move(pCtx)); } // Ensures outbound result payloads inherit the session id of this connection.