Task: Migrate ores.service messaging handler_helpers to wire_codec

Table of Contents

This page documents a task in the Make NATS wire format configurable: JSON/MessagePack, decided once at startup story. It captures the goal, current status, acceptance, and any notes or results.

Goal

ores.service::messaging::handler_helpers' reply() and decode() are the two generic template functions almost every service handler in the codebase already calls to send a response and parse a request. Update them to use an injected wire_codec (from the previous task) instead of a hard-coded rfl::json::write=/=rfl::json::read call – this single change flips every service's request/response handling onto the configurable format in one pass, with no per-handler edits needed.

Also audit for service-to-service NATS calls that build/parse messages directly rather than through these two helpers (if any exist), and either migrate them too or document why they're out of scope.

Status

Field Value
State DONE
Parent story Make NATS wire format configurable: JSON/MessagePack, decided once at startup
Now Nothing.
Waiting on Nothing.
Next Nothing.
Last touched 2026-07-29

Acceptance

  • [X] reply()=/=decode() use the injected wire_codec; no direct rfl::json call remains in handler_helpers.hpp.
  • [X] Every existing service test suite still passes unmodified (proves the migration is behaviour-preserving when the codec is configured for json, today's default).
  • [X] Audit result for bypassing call sites recorded in Notes, with each one either migrated or explicitly deferred with a reason.

Plan

reply()=/=decode() in handler_helpers.hpp now call ores::nats::default_wire_codec().encode(resp)=/.decode<Req>(msg.data)= instead of hard-coded rfl::json::write=/=read.

The blocker: decode<Req>(msg) is called from ~250+ handler call sites across every service (ores.dq, ores.iam, ores.compute, ores.trading, …), none of which have a wire_codec or a nats::service::client reference in scope at the call site – threading one through would mean editing every one of those call sites, which is exactly what this task's "single change, no per-handler edits" framing rules out. Solution: a process-wide default wire_codec (ores::nats::default_wire_codec()=/ =set_default_wire_codec(), added to wire_codec.hpp=/.cpp= in ores.nats), set automatically by nats::service::client's constructor – every server process constructs exactly one client at startup from its resolved nats_options, so this requires zero edits at any of the ~250 call sites or any service's host.cpp. This is a deliberate, narrow exception to per-instance DI, justified because wire format is a single process-wide decided-once-at-startup value (per the write-up task's rejected-alternatives rationale), not a per-instance or per-message choice; documented as such directly on default_wire_codec()'s doc comment.

Stragglers migrated onto the same default codec:

  • heartbeat_publisher.hpp's publish_once(): replaced the manual rfl::json::write + byte-copy with default_wire_codec().encode(hb).
  • workflow_helpers.hpp: both the step_completed_event publish (workflow_step_context::publish()) and the get_step_result_request=/=response round-trip (check_step_idempotency()) now go through the default codec. Left untouched: the doc-comment example's rfl::json::write(result) and the result_json=/=error_message string fields on step_completed_event – these are an embedded JSON domain field (the step's own result payload, itself carried inside the wire message), not the NATS wire-format serialization, matching the write-up task's OreImportWizard.cpp false-positive precedent.

Verification: full-repo compass build --preset linux-clang-debug-make clean (100%, no errors, including all ~40 service handler headers that transitively include handler_helpers.hpp); full ctest --preset linux-clang-debug-make 74/74 suites passed, confirming the migration is behaviour-preserving with the json default.

Notes

Audit for call sites bypassing handler_helpers.hpp's reply()=/ =decode() (per the write-up task's blast-radius audit, doc/agile/versions/v0/sprint_24/nats-configurable-wire-format/task_write-up-wire-format-architecture.org):

  • heartbeat_publisher.hpp and workflow_helpers.hpp – migrated, see * Plan above.
  • No further bypassing rfl::json::write=/=read call sites found in ores.service beyond those two files and the doc-comment mention in workflow_helpers.hpp (an embedded-JSON-field example, not a wire-format call site).

Test Scenarios

Manual QA scenarios (scaffolded via compass add test_scenario, run through the QA Validation Runner panel) that verify this task. Link new ones here as they're created; the scenario doc itself links back via its "Verifies task" field.

Scenario State Notes
     

PRs

PR Title
#1748 [ores.service] Migrate handler_helpers to wire_codec

Review

# Comment summary File Decision Notes
1 No direct unit test for default_wire_codec()/set_default_wire_codec() domain_wire_codec_tests.cpp Fixed Added a test covering the pre-client json default and set_default_wire_codec() changing it, resetting to json afterwards
2 Global mutable state relies on startup-ordering (client ctor before worker threads) without a lock/atomic wire_codec.hpp Fixed Strengthened the doc comment to spell out the happens-before reliance explicitly; kept the plain (non-atomic) field, consistent with the design's single-client-per-process assumption noted by all three review passes as non-blocking

Result

handler_helpers.hpp's reply()=/=decode() choke point, plus the two straggler call sites identified by the write-up task (heartbeat_publisher.hpp, workflow_helpers.hpp), all now route through ores::nats::default_wire_codec() instead of hard-coded rfl::json. Because decode<Req>(msg) is called from ~250+ handler sites across every service with no client/codec reference in scope, the codec is a process-wide default set automatically by nats::service::client's constructor (one client per process, constructed from the resolved nats_options.format) rather than threaded through every call site – zero edits needed at any handler call site or any service's host.cpp. Full-repo build and the complete 74-suite ctest run both pass, confirming the migration is behaviour-preserving under today's json default.

Emacs 29.3 (Org mode 9.6.15)