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.

  1. Handler (service/src/folder_feed_control_handler.hpp): the constructor gains curve_ctrl and auth_nats (both already in scope at the registrar's call site — application.cpp builds the controllers and svc_nats before registration). start() checks both permissions (synthetic::fx_spot_generation_configs:read and synthetic::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: enabled plus an enabled container — the same gate the FX auto-start walk applies, and the container-existence the IR configs' trigger-validated config_id already guarantees. Each row dispatches through make_default_feed_factory(): FX rows to feed_controller::add(feed, binding_mode), IR rows to curve_feed_controller::add(feed, &conflicting_source_name) inside try/catch, both mirroring the application.cpp auto-start walks. The FX factory path drops the old ctrl_->start() vintage check (vintage_data_missing skipped) — 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 by source_name on its own controller.
  2. 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.
  3. Registrar (service/src/registrar.cpp): both folder subscriptions construct the handler with ctrl, curve_ctrl, auth_nats, ctx, verifier.
  4. 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 via jwt_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_component rows, 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, the RATES_SPOT_FORWARD convention + resolution row, value rows, and the ir_curve_generation_config row. Asserts the single-pass request starts both kinds, per-kind counts, already-running on repeat, skipped for 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).

Emacs 29.3 (Org mode 9.6.15)