Skip to content

Multi-controller foundation end-to-end via REST transport #353

@gilesknap

Description

@gilesknap

Parent

#351

What to build

Land the foundational multi-controller mechanics through the REST transport, end-to-end. REST is chosen as the tracer because it has the loosest naming rules and the smallest surface, so the foundation can be exercised without dragging EPICS/Tango/GraphQL specifics into this slice. After this slice, a single FastCS process can serve N controllers over REST with stable id-based routes, with subsequent slices adding the other transports atop the same foundation.

Scope:

  • Controller.id set exactly once by the launcher between __init__ and initialise(). Reading before set raises a clear runtime error; setting twice raises. Controller.__repr__ includes the id when set.
  • fastcs.yaml config schema: controllers: is a dict keyed by id, each value carrying a type: discriminator and a controller: options block. Single-class registration may omit type:. The launcher does not hard-code the filename.
  • launch() accepts a list of Controller classes. Type discriminator value defaults to __name__; optional type_name: ClassVar[str] overrides.
  • Uniform Transport.connect(controller_apis: list[ControllerAPI], loop) signature applied to every transport. Existing transports (EPICS CA, EPICS PVA, GraphQL, Tango) accept a list-of-one and behave as before — they will adopt true multi-controller in subsequent slices.
  • REST transport routes GET /{id}/{sub}/{attr} for every configured controller. One combined OpenAPI schema describes all controllers.
  • Per-transport REST id validator runs at connect() time; illegal characters fail fast with a clear startup error.
  • Logging: FastCS startup line lists controller ids; Controller.__repr__ surfaces id.
  • IPython shell context generalises the existing parallel-top-level-variables pattern (see src/fastcs/control_system.py:103) to parallel dicts: controllers: dict[id, Controller] and controller_apis: dict[id, ControllerAPI]. This satisfies user story 26 without introducing a Controller.api back-pointer, leaving ADR 0006 untouched.
  • Deep modules with their own unit tests:
    • D1: Controller registry / discriminator resolution (dynamic Pydantic model, single-class type inference, type_name overrides).
    • D2: PV-prefix derivation utility (path[0] verbatim, path[1:] snake-to-Pascal). Code lands here; EPICS adopts in EPICS CA multi-root softioc with id-based PV prefix #354.
    • D3-REST: REST id validator.
  • New tests/test_multi_controller.py with the lifecycle and api-path scenarios from the PRD's testing decisions (id available from initialise() onward; raises if read in __init__; set_id raises if called twice; controller_apis["X"].path == [id] for root, [id, sub] for sub-controllers).
  • tests/test_launch.py gains targeted cases for type discriminator resolution, single-class type inference, and duplicate-id rejection (handled by Pydantic's dict[str, ...]).
  • REST end-to-end test: two controllers wired into one process with distinct route prefixes and no clash.

User stories from #351 covered: 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 17, 19, 22, 24, 25, 26, 27, 33.

Acceptance criteria

  • Controller.id lifecycle implemented with the documented errors; __repr__ includes id when set
  • fastcs.yaml schema parses dict-keyed-by-id controllers: block with type: discriminator and single-class inference
  • Transport.connect() signature uniformly takes list[ControllerAPI] across all transports
  • REST transport serves GET /{id}/{sub}/{attr} for every configured controller with a combined OpenAPI schema
  • REST id validator fails fast with a clear startup error on illegal ids
  • FastCS startup log lists controller ids; IPython shell exposes parallel controllers and controller_apis dicts
  • D1, D2, D3-REST modules each ship with unit tests in this slice
  • tests/test_multi_controller.py covers the four PRD lifecycle/api-path scenarios via REST
  • tests/test_launch.py covers discriminator resolution, single-class inference, duplicate-id rejection
  • Existing test suite remains green; existing transports accept the new signature without regression

Blocked by

None - can start immediately

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestpythonPull requests that update Python code

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions