17 KiB
server-legacy-migration Specification
Purpose
Conversion of the legacy analytics collections (events, stats, active_entities, last_alive), written by
OpenVidu 3.9 and earlier, into the analytics model by the OpenVidu server, so that an upgraded deployment keeps its
history, closes the entities the legacy writer left open and exposes the progress to the dashboard.
Requirements
Requirement: Migration trigger at writer startup
Edition: CE and PRO. After renaming a legacy events collection and initializing the analytics collections, a writer
that finds no schema_meta _id: "migration" document and a legacy collection (legacy_events, stats,
active_entities or last_alive) SHALL write _id: "migration" with state: "pending" and start the migration in
the background; every later start resumes a migration that is not completed. Without legacy collections no
migration document SHALL be written.
Scenario: Legacy deployment upgraded
- GIVEN a database written by OpenVidu 3.9 or earlier and no
schema_meta_id: "schema"document - WHEN the writer starts
- THEN
legacy_eventsholds the legacy documents with their indexes, the analytics collections exist, the live write path accepts events immediately andschema_meta_id: "migration"hasstate: "pending",total,writerVersionandfromSchemaVersion: 0
Scenario: Fresh deployment
- GIVEN an empty database
- WHEN the writer starts
- THEN no
migrationdocument exists and nothing is renamed
Scenario: Legacy collection next to an analytics schema
- GIVEN a
schema_meta_id: "schema"document written by the analytics writer, nomigrationdocument and a legacylast_alivecollection - WHEN the writer starts
- THEN
schema_meta_id: "migration"is written withstate: "pending", whatever the recorded schema version
Scenario: Migration already completed
- GIVEN
schema_meta_id: "migration"withstate: "completed" - WHEN the writer restarts
- THEN the document is unchanged and no replay runs
Scenario: Migration disabled
- GIVEN
openvidu.analytics.migration.enabled: falseand a pending migration - WHEN the writer starts
- THEN the document stays
pending, a warning names the setting and the live path runs normally
Requirement: Migration state document
Edition: CE and PRO. schema_meta _id: "migration" SHALL carry state (pending | running |
completed | failed), processed, total, checkpoint { phase, events, stats }, startedAt,
finishedAt, error, writerVersion, nodeId, counts { events, stats, synthetic, skipped } and
legacyRetainedUntil, updated after every batch, so the dashboard can show progress without any other
access to the writer.
Scenario: Progress visible while running
- GIVEN a migration in progress
- WHEN
schema_meta_id: "migration"is read - THEN
stateisrunning,processedis at most one batch behind the documents applied,totalis the document count oflegacy_eventsplusstatsat start, andstartedAtandnodeIdare set
Scenario: Completed with counts
- GIVEN a migration that finished all phases
- WHEN the document is read
- THEN
stateiscompleted,finishedAtis set,countsreports replayed events, stats, synthetic closures and skipped documents, andlegacyRetainedUntilequals the latestopenvidu_expire_atof the legacy collections
Requirement: Single runner across nodes
Edition: CE and PRO. The migration SHALL run on at most one node at a time, elected with the Redis lock
analytics-migration-lock (TTL 60 s, refreshed after every batch; a process-local lock in a deployment without
Redis); a node that cannot obtain the lock SHALL serve the live path and retry the lock every minute, so it takes over
when the runner dies.
Scenario: Two nodes start together
- GIVEN a pending migration and two writer nodes starting at the same time
- WHEN both try to run it
- THEN exactly one runs, the other logs that another node holds the migration and both write live batches
Scenario: Runner node dies
- GIVEN a migration
runningon node A and node B waiting for the lock - WHEN A crashes and its lock expires
- THEN node B obtains the lock at its next attempt, without a restart, and resumes from the stored checkpoint
Requirement: Replay order and batching
Edition: CE and PRO. Legacy events SHALL be replayed in timestamp.seconds then timestamp.nanos order and stats in
time_stamp.seconds order, in batches of batch_size documents (fewer at the end) extended until the second of their
last document is complete, so that a batch never splits a second, read through a streaming cursor that holds one batch
in memory, with a pause after each batch so the throughput does not exceed rate documents per second.
Scenario: Throttle respected
- GIVEN
rate: 1000and 10 000 legacy events - WHEN the replay runs
- THEN it takes at least 10 s and live batches keep their interval during the run
Scenario: Batch keeps whole seconds
- GIVEN
batch_size: 500and 700 events with the sametimestamp.seconds - WHEN the batch containing that second is read
- THEN all 700 events are in it and the checkpoint moves past that second only after they are applied
Scenario: Unthrottled
- GIVEN
rate: 0 - WHEN the replay runs
- THEN no pause is inserted between batches
Scenario: Defaults
- GIVEN no
openvidu.analytics.migrationkeys - WHEN the replay runs
- THEN batches hold 500 documents, extended until the second of their last document is complete (fewer at the end), and the throughput stays at or below 2000 documents per second
Requirement: Legacy document decoding
Edition: CE and PRO. Each legacy document SHALL be decoded into the protocol message the live writer
receives: numbers stored as doubles converted to the field's integer type, enums accepted as the enum
name or its number, oneof wrappers keyed by Go names, absent fields treated as zero values and
ROOM_CREATED without room_id resolved from room.sid. A document that cannot be decoded SHALL be
skipped, counted and logged with its _id.
Scenario: Float numbers
- GIVEN a legacy
ROOM_CREATEDwithroom.creation_time: 1.728e9and anEGRESS_ENDEDwithegress.started_atas a double of nanoseconds - WHEN they are replayed
- THEN the room has
createdAtas aDateand the egressstartedAtanddurationMsare exact to the millisecond
Scenario: Enums as names or numbers
- GIVEN a legacy
PARTICIPANT_LEFTwithtype: "PARTICIPANT_LEFT",participant.kind: 4andparticipant.disconnect_reason: 1 - WHEN it is replayed
- THEN the participant document has
kind: "EGRESS"anddisconnectReason: "CLIENT_INITIATED"
Scenario: Oneof by Go name
- GIVEN a legacy
EGRESS_STARTEDwhose request is underegress.Request.RoomComposite - WHEN it is replayed
- THEN the egress document has
type: "room_composite"and a sanitizedrequest
Scenario: ROOM_CREATED without room_id
- GIVEN a legacy
ROOM_CREATEDwithroom.sidand noroom_id - WHEN it is replayed
- THEN one
roomsdocument with_idequal toroom.sidexists
Scenario: Undecodable document
- GIVEN a legacy document whose
typeis unknown - WHEN the batch is replayed
- THEN the document is skipped,
counts.skippedincreases, a warning carries its_idand the migration continues
Requirement: Raw event identity
Edition: CE and PRO. A replayed legacy event SHALL be stored as a raw event whose _id is the legacy
id field (AE_…) when present and otherwise AE_LEG_ followed by the legacy _id, so that a second
replay of the same document is a no-op; events of types not in events.store_types are projected but
not stored raw.
Scenario: Event with LiveKit id
- GIVEN a legacy event with
id: "AE_abc" - WHEN it is replayed twice
- THEN exactly one
eventsdocument with_id: "AE_abc"exists and counters moved once
Scenario: Event without LiveKit id
- GIVEN a legacy event with string
_id(md5) and noid - WHEN it is replayed
- THEN the raw event
_idisAE_LEG_<md5>
Requirement: Fixer events become synthetic closures
Edition: CE and PRO. A legacy close event inserted by the active-entities fixer (ObjectId _id, no
id) SHALL not be projected as an authentic event: it SHALL close its entity through the reconciler's
closure routine at the fake event's timestamp with closedBy: "reconciler" and record a synthetic raw
event (_id AE_SYN_ followed by the entity id). An authentic close event of the same entity SHALL override
the synthetic closure without counting it twice.
Scenario: Fake PARTICIPANT_LEFT
- GIVEN a legacy fake
PARTICIPANT_LEFTfor a participant whosePARTICIPANT_ACTIVEwas replayed - WHEN it is replayed
- THEN the participant has
status: "left",closedBy: "reconciler",disconnectReason: "RECONCILED",leftAtequal to the fake timestamp, and a raw event withsynthetic: trueandpayload.reconciler.reason: "legacy-fixer"exists
Scenario: Fake EGRESS_ENDED claims COMPLETE
- GIVEN a legacy fake
EGRESS_ENDEDwithegress.status: "EGRESS_COMPLETE" - WHEN it is replayed
- THEN the egress is closed with
closedBy: "reconciler"and the terminal status the reconciler uses for lost egresses, nevercomplete
Scenario: Authentic close after fake close
- GIVEN a fake
ROOM_ENDEDand an authenticROOM_ENDEDfor the same room - WHEN both are replayed in any order
- THEN the room has
closedBy: "event",endReasonfrom the authentic event andendedAtfrom it
Requirement: Live events during the migration
Edition: CE and PRO. Live events SHALL be processed normally while the migration runs, and an entity touched by both a replayed and a live event SHALL converge to the same document as if all events had arrived live. Until the migration completes, the live reconciler SHALL not close candidates of nodes that never wrote a heartbeat (legacy nodes), which the replay closes later.
Scenario: Room active across the upgrade
- GIVEN a room whose
ROOM_CREATEDandPARTICIPANT_ACTIVEare legacy and whosePARTICIPANT_LEFTandROOM_ENDEDarrive live during the replay - WHEN the migration completes
- THEN one
roomsdocument exists withstatus: "closed",closedBy: "event",participants.sessions: 1andparticipants.active: 0
Scenario: Live reconciler during the replay
- GIVEN a running migration, an open replayed legacy participant and a participant of a crashed node that wrote heartbeats
- WHEN the live reconciler runs
- THEN it closes the participant of the crashed node and leaves the legacy participant to the replay
Requirement: Checkpoint and resume
Edition: CE and PRO. The checkpoint (phase and the last fully applied second of each collection) SHALL
be persisted after every batch; a restart, a lost lock or a failure SHALL leave a checkpoint from which
the next run resumes without reapplying any document. A non-transient error, or a transient one that persists
through five attempts, SHALL set state: "failed" with a clear error, and the next writer start SHALL retry from
the checkpoint.
Scenario: Killed mid-run
- GIVEN a migration killed after 40 % of the events
- WHEN the writer restarts
- THEN the replay resumes after the checkpointed second, no
track_statssample is duplicated and the final documents equal those of an uninterrupted run
Scenario: Database error
- GIVEN a MongoDB outage during a batch
- WHEN the batch fails five attempts in a row, spaced 1, 2, 4 and 8 s
- THEN
stateisfailed,errornames the failure and the checkpoint, and the next writer start retries from the checkpoint
Scenario: Transient error with progress
- GIVEN a phase that fails with a network error four times, applies more batches and fails again
- WHEN it is retried
- THEN the attempt count starts again after the progress and the migration keeps
state: "running"
Requirement: Stats replay
Edition: CE and PRO. Legacy stats SHALL be replayed after the events through the live stats pipeline,
producing track_stats documents and the same participant, room and rollup deltas the live writer
produces, in the configured stats mode.
Scenario: Legacy stat document
- GIVEN a legacy stat with
kind: "UPSTREAM",time_stampas doubles andstreams[]RTP totals - WHEN it is replayed
- THEN a
track_statsdocument exists withtsas aDate,meta.kind: "up", normalized units and the participant'sbytes.upincludes its bytes
Requirement: Post-replay steps
Edition: CE and PRO. After the stats phase the migration SHALL run the reconciler until a run is not partial and
closes nothing (at most 20 runs), drop active_entities and last_alive, record legacyRetainedUntil and mark
the migration completed; legacy_events and stats SHALL never be deleted by the migration.
Scenario: Open entities after the replay
- GIVEN a participant whose legacy
PARTICIPANT_LEFTnever existed and whose room is absent from LiveKit - WHEN the post-replay reconciliation runs
- THEN the participant is
leftwithclosedBy: "reconciler"and a room still listed by LiveKit staysactive
Scenario: Legacy collections afterwards
- GIVEN a completed migration
- WHEN the database is inspected
- THEN
active_entitiesandlast_alivedo not exist,legacy_eventsandstatskeep their documents and TTL indexes, andlegacyRetainedUntilis set
Requirement: Legacy data kept for rollback
Edition: CE and PRO. The migration SHALL only read legacy_events and stats, never changing their documents or
indexes, so that a legacy server (OpenVidu 3.9 or earlier) can be restored on the legacy data that has not expired
by its TTL.
Scenario: Rollback after the migration
- GIVEN a completed migration
- WHEN the operator stops the server, drops the analytics collections, renames
legacy_eventsback toeventsand starts the legacy server - THEN
eventsandstatshold every legacy document whoseopenvidu_expire_athas not passed, with their original indexes
Requirement: Observability
Edition: CE and PRO. The writer SHALL expose openvidu_analytics_migration_state (gauge: 0 none, 1 pending,
2 running, 3 completed, 4 failed), openvidu_analytics_migration_processed_total{collection} and
openvidu_analytics_migration_batch_seconds (batch duration histogram) and SHALL
log progress (processed, total, rate, estimated remaining time) at least every 30 s while running and
one line per state transition.
Scenario: Metrics during the run
- GIVEN a running migration
- WHEN the metrics endpoint is scraped
- THEN
openvidu_analytics_migration_stateis 2 andopenvidu_analytics_migration_processed_totalgrows with the replay
Requirement: Migration invariants
Edition: CE and PRO. After completion the analytics collections SHALL satisfy: one rooms document per distinct
legacy room.sid, one participants document per distinct legacy participant_id, one egresses
document per egress_id, one ingress_sessions document per ingress.state.resource_id, no secret
field copied, and durations of event-closed entities within 1 s of the legacy start/end difference.
Scenario: Invariants on a recorded snapshot
- GIVEN a legacy snapshot recorded from the local deployment
- WHEN the migration completes and the invariant checks run
- THEN every check passes and a second migration of the legacy events of the same snapshot leaves every document unchanged
Requirement: Room-name search field on migrated egresses
Edition: CE and PRO. A migrated egress document SHALL carry roomNameLower, the lower-cased legacy room name, set by
the live egress projection that replays the legacy egress events; a legacy egress without a room name SHALL get
neither roomName nor roomNameLower. Node documents are not migrated.
Scenario: Legacy egress of a room
- GIVEN a legacy
EGRESS_STARTEDwhoseegress.room_nameisDemo-Room - WHEN it is replayed
- THEN the
egressesdocument hasroomName: "Demo-Room"androomNameLower: "demo-room"
Scenario: Egress closed by a fixer event
- GIVEN a legacy egress of room
Demo-Roomwhose only close event is a fakeEGRESS_ENDEDof the active-entities fixer - WHEN the migration completes
- THEN the egress is closed by the reconciler and keeps
roomNameLower: "demo-room"from its replayedEGRESS_STARTED
Scenario: Legacy web egress
- GIVEN a legacy web egress with empty
room_name - WHEN it is replayed
- THEN the
egressesdocument has neitherroomNamenorroomNameLower