mirror of https://github.com/OpenVidu/openvidu.git
267 lines
8.8 KiB
JavaScript
267 lines
8.8 KiB
JavaScript
#!/usr/bin/env node
|
|
// Seeds the rooms data of the end-to-end suites from the contract fixtures of @openvidu-dashboard/typings: the
|
|
// collections and indexes of the contract, the fixture documents with their dates moved so the active room is live, a
|
|
// copy of the active room without stats for 100 s, a copy of the closed room as closed by the reconciler, two server
|
|
// nodes and the hourly rollup of the current hour, metadata on the active room, and egresses closed by the
|
|
// reconciler, without room and with credentials a leaking writer could have stored (the sentinel never reaches a
|
|
// response).
|
|
// Usage: node scripts/seed-rooms.mjs --uri <mongodb uri> --db <database>
|
|
import { readFileSync } from 'node:fs';
|
|
import { createRequire } from 'node:module';
|
|
import { parseArgs } from 'node:util';
|
|
|
|
import { BSON, MongoClient } from 'mongodb';
|
|
|
|
const require = createRequire(import.meta.url);
|
|
const YEAR_SECONDS = 365 * 86_400;
|
|
const SEEDED = ['rooms', 'participants', 'egresses', 'ingresses', 'ingress_sessions', 'events', 'track_stats'];
|
|
|
|
export const ACTIVE_ROOM = 'RM_bQ7tVnW2cErT';
|
|
export const CLOSED_ROOM = 'RM_aLkq3ZpD9wXy';
|
|
export const RECONCILED_ROOM = 'RM_rec0nciled01';
|
|
export const RECONCILED_EGRESS = 'EG_rec0nciled01';
|
|
export const WEB_EGRESS = 'EG_webN0Room01';
|
|
export const LEAKING_EGRESS = 'EG_leak1ng0001';
|
|
export const SENTINEL = 'E2E_SENTINEL_9f3c';
|
|
export const IDLE_INGRESS = 'IN_wh1pIdle0001';
|
|
export const FAILED_INGRESS = 'IN_ur1Failed001';
|
|
export const LEAKING_INGRESS = 'IN_leak1ng00001';
|
|
export const INGRESS_USER_INFO = 'e2euser:e2epass';
|
|
export const STALE_ROOM = 'RM_st4leRoom001';
|
|
|
|
function contract(file) {
|
|
return BSON.EJSON.parse(readFileSync(require.resolve(`@openvidu-dashboard/typings/${file}`), 'utf8'), {
|
|
relaxed: true
|
|
});
|
|
}
|
|
|
|
function shift(value, delta) {
|
|
if (value instanceof Date) {
|
|
return new Date(value.getTime() + delta);
|
|
}
|
|
|
|
if (Array.isArray(value)) {
|
|
return value.map((item) => shift(item, delta));
|
|
}
|
|
|
|
if (value !== null && typeof value === 'object') {
|
|
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, shift(item, delta)]));
|
|
}
|
|
|
|
return value;
|
|
}
|
|
|
|
async function createContractCollections(db) {
|
|
const existing = new Set((await db.listCollections({}, { nameOnly: true }).toArray()).map((info) => info.name));
|
|
|
|
for (const definition of contract('schema/collections.json')) {
|
|
if (existing.has(definition.name)) {
|
|
continue;
|
|
}
|
|
|
|
const { timeseries } = definition;
|
|
|
|
await db.createCollection(
|
|
definition.name,
|
|
timeseries === undefined
|
|
? {}
|
|
: {
|
|
timeseries: {
|
|
timeField: timeseries.timeField,
|
|
metaField: timeseries.metaField,
|
|
granularity: timeseries.granularity
|
|
},
|
|
expireAfterSeconds: YEAR_SECONDS
|
|
}
|
|
);
|
|
}
|
|
|
|
for (const index of contract('schema/indexes.json')) {
|
|
if (index.timeseriesFallbackOnly === true) {
|
|
continue;
|
|
}
|
|
|
|
const ttl = index.retentionClass === undefined ? index.options.expireAfterSeconds : YEAR_SECONDS;
|
|
|
|
await db.collection(index.collection).createIndex(Object.fromEntries(index.key), {
|
|
name: index.name,
|
|
...(index.options.unique === true ? { unique: true } : {}),
|
|
...(index.options.partialFilterExpression === undefined
|
|
? {}
|
|
: { partialFilterExpression: index.options.partialFilterExpression }),
|
|
...(ttl === undefined ? {} : { expireAfterSeconds: ttl })
|
|
});
|
|
}
|
|
}
|
|
|
|
/** Replaces the rooms collections of `db` with the seeded documents. */
|
|
export async function seedRooms(db, now = new Date()) {
|
|
await createContractCollections(db);
|
|
|
|
const rooms = contract('fixtures/rooms.json');
|
|
const active = rooms.find((room) => room._id === ACTIVE_ROOM);
|
|
const delta = now.getTime() - 60_000 - active.lastEventAt.getTime();
|
|
const documents = Object.fromEntries(
|
|
SEEDED.map((collection) => [collection, shift(contract(`fixtures/${collection}.json`), delta)])
|
|
);
|
|
const seededActive = documents.rooms.find((room) => room._id === ACTIVE_ROOM);
|
|
const closed = documents.rooms.find((room) => room._id === CLOSED_ROOM);
|
|
|
|
seededActive.lastStatsAt = new Date(now.getTime() - 10_000);
|
|
seededActive.config = { ...seededActive.config, metadata: JSON.stringify({ customer: 'acme', plan: 'pro' }) };
|
|
documents.rooms.push({
|
|
...seededActive,
|
|
_id: STALE_ROOM,
|
|
name: 'Stale-Room',
|
|
nameLower: 'stale-room',
|
|
createdAt: new Date(now.getTime() - 50 * 60_000),
|
|
lastStatsAt: new Date(now.getTime() - 100_000)
|
|
});
|
|
documents.rooms.push({
|
|
...closed,
|
|
_id: RECONCILED_ROOM,
|
|
name: 'Crashed-Node-Room',
|
|
nameLower: 'crashed-node-room',
|
|
endReason: 'RECONCILED',
|
|
closedBy: 'reconciler'
|
|
});
|
|
|
|
const complete = documents.egresses.find((egress) => egress.status === 'complete');
|
|
const roomless = { ...complete };
|
|
|
|
delete roomless.roomSid;
|
|
delete roomless.roomName;
|
|
|
|
documents.egresses.push(
|
|
{
|
|
...complete,
|
|
_id: RECONCILED_EGRESS,
|
|
status: 'failed',
|
|
closedBy: 'reconciler',
|
|
error: 'egress lost (reconciled)'
|
|
},
|
|
{
|
|
...roomless,
|
|
_id: WEB_EGRESS,
|
|
type: 'web',
|
|
sourceType: 'web',
|
|
request: { url: 'https://app.example.com/***' }
|
|
},
|
|
{
|
|
...complete,
|
|
_id: LEAKING_EGRESS,
|
|
request: {
|
|
...complete.request,
|
|
s3: { accessKey: SENTINEL, secret: SENTINEL },
|
|
token: SENTINEL,
|
|
webhooks: [{ signingKey: SENTINEL }]
|
|
}
|
|
}
|
|
);
|
|
|
|
const publishing = documents.ingresses.find((ingress) => ingress.status === 'publishing');
|
|
const idle = { ...publishing, _id: IDLE_INGRESS, name: 'whip-desk', nameLower: 'whip-desk', inputType: 'whip' };
|
|
|
|
delete idle.lastSessionAt;
|
|
documents.ingresses.push(
|
|
{ ...idle, status: 'inactive', reusable: false, sessions: 0, totalDurationMs: 0 },
|
|
{
|
|
...publishing,
|
|
_id: FAILED_INGRESS,
|
|
name: 'mirror',
|
|
nameLower: 'mirror',
|
|
inputType: 'url',
|
|
url: 'https://src.example.com/live.m3u8',
|
|
status: 'error',
|
|
reusable: false,
|
|
sessions: 1,
|
|
lastError: 'could not connect to source'
|
|
},
|
|
{
|
|
...publishing,
|
|
_id: LEAKING_INGRESS,
|
|
name: 'leaky-cam',
|
|
nameLower: 'leaky-cam',
|
|
status: 'inactive',
|
|
url: `rtmp://${INGRESS_USER_INFO}@ingress.example.com/live?token=${SENTINEL}`
|
|
}
|
|
);
|
|
documents.ingress_sessions.push({
|
|
...documents.ingress_sessions[0],
|
|
_id: 'url_f41led000001',
|
|
ingressId: FAILED_INGRESS,
|
|
inputType: 'url',
|
|
status: 'error',
|
|
error: 'could not connect to source'
|
|
});
|
|
documents.events.push(
|
|
...[FAILED_INGRESS, LEAKING_INGRESS].map((ingressId, index) => ({
|
|
_id: `AE_ingress_e2e_${String(index)}`,
|
|
type: 'INGRESS_CREATED',
|
|
ts: new Date(now.getTime() - 30 * 60_000),
|
|
ingressId,
|
|
nodeId: 'ND_e2e',
|
|
synthetic: false,
|
|
payload: { ingress: { stream_key: SENTINEL, url: `rtmp://${INGRESS_USER_INFO}@ingress.example.com/live` } }
|
|
}))
|
|
);
|
|
|
|
for (const collection of SEEDED) {
|
|
await db.collection(collection).deleteMany({});
|
|
await db.collection(collection).insertMany(documents[collection]);
|
|
}
|
|
|
|
await seedWriter(db, now);
|
|
}
|
|
|
|
/** Two server nodes, one seen 5 s ago and one 50 s ago, and the global hourly rollup of the current hour. */
|
|
async function seedWriter(db, now) {
|
|
const [alpha, beta] = contract('fixtures/nodes.json');
|
|
const hour = new Date(Math.floor(now.getTime() / 3_600_000) * 3_600_000);
|
|
|
|
await db.collection('nodes').deleteMany({});
|
|
await db.collection('nodes').insertMany([
|
|
{ ...alpha, lastSeenAt: new Date(now.getTime() - 5_000) },
|
|
{ ...beta, lastSeenAt: new Date(now.getTime() - 50_000) }
|
|
]);
|
|
await db.collection('analytics_hourly').deleteMany({});
|
|
await db.collection('analytics_hourly').insertOne({
|
|
_id: `global:${hour.toISOString().replace('.000Z', 'Z')}`,
|
|
scope: 'global',
|
|
bucket: hour,
|
|
rooms: { created: 3 },
|
|
participants: { joined: 12, minutes: 40 },
|
|
egress: { started: 1 },
|
|
ingress: { sessionsStarted: 1 }
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Moves the liveness signals of the seeded data to `now`, as a running writer would: node `alpha` seen 5 s ago and
|
|
* `beta` 50 s ago, the active room with stats 10 s old and the stale room 100 s old.
|
|
*/
|
|
export async function refreshLiveness(db, now = new Date()) {
|
|
const [alpha, beta] = contract('fixtures/nodes.json');
|
|
const ago = (ms) => new Date(now.getTime() - ms);
|
|
|
|
await db.collection('nodes').updateOne({ _id: alpha._id }, { $set: { lastSeenAt: ago(5_000) } });
|
|
await db.collection('nodes').updateOne({ _id: beta._id }, { $set: { lastSeenAt: ago(50_000) } });
|
|
await db.collection('rooms').updateOne({ _id: ACTIVE_ROOM }, { $set: { lastStatsAt: ago(10_000) } });
|
|
await db.collection('rooms').updateOne({ _id: STALE_ROOM }, { $set: { lastStatsAt: ago(100_000) } });
|
|
}
|
|
|
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
const { values } = parseArgs({ options: { uri: { type: 'string' }, db: { type: 'string' } } });
|
|
|
|
if (values.uri === undefined || values.db === undefined) {
|
|
console.error('Usage: node scripts/seed-rooms.mjs --uri <mongodb uri> --db <database>');
|
|
process.exit(1);
|
|
}
|
|
|
|
const client = await MongoClient.connect(values.uri);
|
|
|
|
await seedRooms(client.db(values.db));
|
|
await client.close();
|
|
console.log(`Seeded the rooms data into ${values.db}`);
|
|
}
|