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 injectedwire_codec; no directrfl::jsoncall remains inhandler_helpers.hpp.[X]Every existing service test suite still passes unmodified (proves the migration is behaviour-preserving when the codec is configured forjson, 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'spublish_once(): replaced the manualrfl::json::write+ byte-copy withdefault_wire_codec().encode(hb).workflow_helpers.hpp: both thestep_completed_eventpublish (workflow_step_context::publish()) and theget_step_result_request=/=responseround-trip (check_step_idempotency()) now go through the default codec. Left untouched: the doc-comment example'srfl::json::write(result)and theresult_json=/=error_messagestring fields onstep_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'sOreImportWizard.cppfalse-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.hppandworkflow_helpers.hpp– migrated, see* Planabove.- No further bypassing
rfl::json::write=/=readcall sites found inores.servicebeyond those two files and the doc-comment mention inworkflow_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.