diff --git a/openvidu-dashboard/openspec/changes/physical-media-nodes/.openspec.yaml b/openvidu-dashboard/openspec/changes/physical-media-nodes/.openspec.yaml new file mode 100644 index 000000000..4ccb2b41a --- /dev/null +++ b/openvidu-dashboard/openspec/changes/physical-media-nodes/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-11-04 diff --git a/openvidu-dashboard/openspec/changes/physical-media-nodes/design.md b/openvidu-dashboard/openspec/changes/physical-media-nodes/design.md new file mode 100644 index 000000000..4c5aa8e37 --- /dev/null +++ b/openvidu-dashboard/openspec/changes/physical-media-nodes/design.md @@ -0,0 +1,175 @@ +# Design + +## Context + +- **Node documents:** + - `analytics.Node{ID, Region, WithoutMediaStats}` is built in `openvidu/openvidu.go`. + - `store.Heartbeat` upserts one `nodes` document per process on every batch tick, and the document expires 30 days + after `lastSeenAt`. + - Node ids are generated per process. + - An edition adjusts the analytics through `openviduconfig` package variables before `openvidu.Start` + (`DefaultStatsDetail`, `MediaStats`). +- **Entity stamps:** + - Rooms take `nodeId` from `ROOM_CREATED`, and from the first event of any kind while it is unknown. + - Participant sessions take `nodeId` from their first event. + - `mediaStats` follows the same rule (`projection/room.go`, `participant.go`). +- **Rollups:** `projection/rollup.go` writes `global` and `node:`. The event node id is always the + writing process's (`pkg/telemetry/analyticsservice.go`). +- **Reconciler:** + - It adds the `participantsSum` of a room it closes to `node:`. + - Its daily unique identities group sessions by `nodeId`. + - It and the legacy migration project with `projectionOptions`, which carries no node fields. +- **Shutdown:** `writer.drain` writes the queues after the rooms close, within the 5 s shutdown budget. +- **Dashboard backend:** + - The status service counts every `nodes` document (`foldWriter`). + - The overview repository groups active rooms and sessions by `nodeId` for the edition extension + (`OverviewInputs`). + - The rooms list filters `nodeId` on `nodeId_status_createdAt_id`. +- **Dashboard frontend:** + - Three seams receive edition components: `OVERVIEW_NODES_CARD`, `ROOMS_NODE_FILTER` and `ANALYTICS_SCOPE_SELECT`. + - CE renders them locked without `per-node-breakdown`. + - The Node columns are CE columns reading `nodeId`. +- **Index maintenance:** `schema.Initialize` creates and updates the contract's indexes and never drops an index that + is absent from the contract. + +## Goals / Non-Goals + +**Goals:** +- A restart-stable machine identity on every room and session, for the whole entity retention. +- Per-node rollups that add up the restarts of one machine. +- A stopped server is distinguishable from a silent one. +- CE code and specs stay neutral: the label is an opaque string the server gives, and CE never shows per-node UI. + +**Non-Goals:** +- Deciding when a label exists or what it looks like (edition). +- Labelling egress, ingress, tracks or subscriptions: views reach tracks and subscriptions through their participant. +- Using `stoppedAt` in the reconciler's liveness check. + +## Decisions + +### D1. An opaque label set by the server edition + +`openviduconfig.MediaNode` (default empty) is the media node label. An edition sets it before the analytics start, like +`MediaStats`. `openvidu.Start` passes it as `analytics.Node.MediaNode`, and `nodeProjectionOptions` passes it to the +projector. There is no configuration key: the edition derives it from facts it already has. + +Rejected: +- The dashboard joining `nodeId` to `nodes`, for the reason `media-stats-availability` gave for `mediaStats`: node + documents expire after 30 days and entities live 90 days by default. +- A CE-defined label (hostname, IP). The hostname of a container is not the machine, and the right identity depends on + how an edition deploys its nodes. + +### D2. The stamp follows `nodeId` + +The label is set with the same expressions as `mediaStats`. An entity this process writes first gets its label. The +host's `ROOM_CREATED` (or the session's first event) sets `nodeId` and `mediaNode` together. So a room touched first by +another process's egress event ends with the host's label. + +### D3. Node scopes keyed by the label, none without one + +`scopes()` returns `global` plus `node: