Task: Folder cascade covers every asset class via the factory
Table of Contents
This page documents a task in the Feed lifecycle harmonization: asset-class-agnostic feed control-plane story. It captures the goal, current status, acceptance, and any notes or results.
Goal
The reported defect is fixed: a folder-scoped start/stop request is a message to the server carrying the folder; the server walks the folder subtree once and starts every feed under it — every asset class — dispatching through the factory. No client-side enumeration of config kinds.
Status
| Field | Value |
|---|---|
| State | DONE |
| Parent story | Feed lifecycle harmonization: asset-class-agnostic feed control-plane |
| Now | Nothing. |
| Waiting on | Nothing. |
| Next | Nothing. |
| Last touched | 2026-08-11 |
Acceptance
- The start/stop feeds under folder request starts (and stops) every feed kind under the subtree: a folder containing both FX and IR configs starts both kinds.
- The walk is server-side and single-pass: one subtree query, per-row dispatch through the factory. The IR pass is not a third copy of the build sequence.
- The cascade honors the same startability semantics as the rest of the control-plane (enabled; container state) and reports per-kind started/already-running/skipped counts.
- Permission checking is uniform across kinds.
Plan
folder_feed_control_handler is rewritten from an FX-only cascade into
the single server-side walk the goal describes: one subtree query, both
config families read, every row dispatched through
make_default_feed_factory() to the per-kind controller. The IR pass
is a per-row dispatch, not a second copy of the build sequence.
- Handler (
service/src/folder_feed_control_handler.hpp): the constructor gainscurve_ctrlandauth_nats(both already in scope at the registrar's call site — application.cpp builds the controllers andsvc_natsbefore registration).start()checks both permissions (synthetic::fx_spot_generation_configs:readandsynthetic::ir_curve_generation_configs:read), reads both config families plus their children (fx:gmm_component; ir:ir_curve_template_entry, process parameter values, the definitions catalogue), and walks each row in the subtree with uniform startability:enabledplus an enabled container — the same gate the FX auto-start walk applies, and the container-existence the IR configs' trigger-validatedconfig_idalready guarantees. Each row dispatches throughmake_default_feed_factory(): FX rows tofeed_controller::add(feed, binding_mode), IR rows tocurve_feed_controller::add(feed, &conflicting_source_name)inside try/catch, both mirroring the application.cpp auto-start walks. The FX factory path drops the oldctrl_->start()vintage check (vintage_data_missingskipped) — accepted: the cascade aligns with the auto-start path it replaces, and the vintage check stays in the client-supplied-params path until the control-plane task deletes it.stop()walks both families, stopping each bysource_nameon its own controller. - Protocol (
ores.marketdata.api/messaging/market_feed_config_protocol.hpp): the folder start/stop responses gain kind-agnostic per-kind counts —std::map<std::string, feed_kind_counts>keyed by the factory kind string, so the protocol never enumerates an asset class. The aggregate fields (started,already_running,skipped,stopped) stay, keeping ores.shell's printed summary and every existing consumer backward-safe. - Registrar (
service/src/registrar.cpp): both folder subscriptions construct the handler withctrl,curve_ctrl,auth_nats,ctx,verifier. - Tests (
service/tests/folder_feed_control_handler_tests.cpp): the first service-test handler integration test — real NATS, real DB (scoped_database_helper), and an HS256 JWT minted in-test viajwt_authenticator::create_hs256()carrying the test tenant and both permission strings (the pattern the refdata eventing tests use for NATS+DB, extended with the JWT seam). Seeds both config families under the test tenant — folder, containers, fx config +gmm_componentrows, and the IR stack:yield_curve_process_type(VASICEK), the four parameter definitions, a DEPOSIT template entry (SPOT→1M) with its tenor/instrument-code rows, theRATES_SPOT_FORWARDconvention + resolution row, value rows, and their_curve_generation_configrow. Asserts the single-pass request starts both kinds, per-kind counts, already-running on repeat,skippedfor disabled rows, stop counts, and permission denial without the IR permission.
Notes
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 |
|---|---|
| #1964 | [marketdata,synthetic,ores.sql] Folder cascade covers every asset class via the feed factory |
Review
| # | Comment summary | File | Decision | Notes |
|---|---|---|---|---|
| 1 | add() has no re-entrancy guard for the same source_name; a duplicate add emplace-collides, destroying a joinable thread (std::terminate) | curve_feed_controller.hpp | Fixed | 7b657a0543: add() returns false for an already-running source_name, as start() always did |
| 2 | FX factory.make()/ctrl_->add() not wrapped in try/catch; one malformed row propagates out of start() and the request times out | folder_feed_control_handler.hpp | Fixed | c7f9ad53b3: try/catch counts the row as skipped with a warn log, mirroring the IR pass |
| 3 | IR add() false return counted nowhere; totals under-count | folder_feed_control_handler.hpp | Fixed | c7f9ad53b3: conflicting-source-name out param; empty conflict (concurrent duplicate) counts as already_running, named conflict as skipped with warn |
| 4 | Trailing comment on the skipped field, against the comment convention | market_feed_config_protocol.hpp | Fixed | d3f2c370d1: comment moved above the field |
| 5 | FX cascade drops the vintage-price check the old ctrl_->start() path had | folder_feed_control_handler.hpp | Declined | Accepted in the Plan (lines 74-77): the cascade aligns with the auto-start path it replaces; the vintage check stays in the client-supplied-params path until task 5 (config-id-control-plane) deletes it |
| 6 | start_folder/stop_folder now require both FX and IR read permissions | folder_feed_control_handler.hpp | Declined | Uniform gate is the designed behavior: the cascade now touches both config families; the handler's doc comment states the gate |
| 7 | Folder-cascade walk duplicates the auto-start walk in application.cpp | folder_feed_control_handler.hpp | Declined | Story task 4 (uniform-auto-start) plans the unification of both walks |
Result
done on 2026-08-11. The folder start/stop request is now a single
server-side walk over the folder subtree: one subtree query, both
config families read, every row dispatched through
make_default_feed_factory() to the per-kind controller. A folder
containing both FX and IR configs starts both kinds, honoring the
same startability gate (enabled plus an enabled container) and
reporting per-kind started/already-running/skipped counts (start) and
stopped counts (stop) in the response protocol. Permission checking is
uniform: both synthetic::fx_spot_generation_configs:read and
synthetic::ir_curve_generation_configs:read are required. The IR
pass is a per-row dispatch, not a third copy of the build sequence.
Verified: full build green, database recreated (schema 0.0.25), rat
green — including the new integration tests
(folder_feed_control_handler_tests.cpp: both kinds start/stop with
per-kind counts, permission denial without the IR permission,
malformed folder id). Review round 1 addressed: four findings fixed
(controller re-entrancy guard, FX try/catch, IR add() counting,
comment placement) and three declined by design (vintage-check drop,
uniform permission gate, walk duplication) — see the Review table.
One review-round verification finding fixed on this branch: the IR cascade read the yield-curve parameter-definitions catalogue with the caller's tenant context, so real tenants resolved no definitions (commit a0f28bfd94 — the catalogue is system-tenant owned, mirroring the sibling fix on main).