openvibers/openvibe.host Commits

Protect releases a JSMPEG worker runs from and list its instances (#19)

OP OpenVibers committed 4 Oct 2026 · 1ae4c9752d · 4 files, +34 −9

checks run by GitHub CI (test, test-pg) · CI ✓ 3/3 · review SHIP by Codex GPT-6 Sol

https://github.com/OpenVibers/OpenVibe.Host/pull/19

Changes

ddiff --git a/host.example.json b/host.example.json
iindex 875767d..9725ee8 100644
---- a/host.example.json
++++ b/host.example.json
@@@ -2178,7 +2178,7 @@
}
},
"openre": {
-- "_note": "Release layout: /opt/openre.stream/repo is the git clone, releases/<sha12> the releases (npm ci into each, chowned to ubuntu), `current` the link the API and the coordinator run from (deploy.sh release + api as ovhost deploy openre; ovhost rollback openre). units are the API and the session coordinator. workerUnits are the transport workers: one instance per release (openre-rtmp-ingest@<release>), started by deploy.sh workers (still the repository's script), drained by the coordinator when a newer generation is ready, and exiting on their own. ovhost lists them in status/validate, never starts, stops or restarts them (the inventory refuses a worker unit in units), and never prunes a release a worker instance runs from.",
++ "_note": "Release layout: /opt/openre.stream/repo is the git clone, releases/<sha12> the releases (npm ci into each, chowned to ubuntu), `current` the link the API and the coordinator run from (deploy.sh release + api as ovhost deploy openre; ovhost rollback openre). units are the API and the session coordinator. workerUnits are the transport workers: one instance per release (openre-rtmp-ingest@<release>, openre-jsmpeg@<release>), started by deploy.sh workers (still the repository's script), drained by the coordinator when a newer generation is ready, and exiting on their own. ovhost lists them in status/validate, never starts, stops or restarts them (the inventory refuses a worker unit in units), and never prunes a release a worker instance runs from.",
"repo": "/opt/openre.stream",
"strategy": "release-layout",
"owner": "root",
@@@ -2196,7 +2196,8 @@
],
"workerUnits": [
],
"envFile": "/etc/openvibe/openre.env",
"env": {
ddiff --git a/test/openre-workers.test.js b/test/openre-workers.test.js
iindex 3d22a59..6e5e834 100644
---- a/test/openre-workers.test.js
++++ b/test/openre-workers.test.js
@@@ -15,7 +15,7 @@ const { normalise } = require('../lib/inventory');
const EXAMPLE = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'host.example.json'), 'utf8'));
const RELEASE = '655b98a10aaa';
const OLD = '0123456789ab';
--const WORKERS = [`openre-rtmp-ingest@${RELEASE}.service`, `openre-restream-worker@${RELEASE}.service`];
++const WORKERS = [`openre-rtmp-ingest@${RELEASE}.service`, `openre-restream-worker@${RELEASE}.service`, `openre-jsmpeg@${RELEASE}.service`];
/** The standard fake host plus OpenRe as on openvibe-ovh: release layout, two units, worker instances. */
async function openreHost({ workersRunning = true } = {}) {
@@@ -48,7 +48,9 @@ async function openreHost({ workersRunning = true } = {}) {
host.listeners.set(4500, [{ pid: 3100, process: 'node' }]);
host.addUnit(`openre-rtmp-ingest@${RELEASE}.service`, { mainPid: 3102, ...(workersRunning ? {} : { active: 'inactive', sub: 'dead' }) });
host.addUnit(`openre-restream-worker@${RELEASE}.service`, { mainPid: 3103, ...(workersRunning ? {} : { active: 'inactive', sub: 'dead' }) });
++ host.addUnit(`openre-jsmpeg@${RELEASE}.service`, { mainPid: 3104, ...(workersRunning ? {} : { active: 'inactive', sub: 'dead' }) });
host.addUnit(`openre-rtmp-ingest@${OLD}.service`, { active: 'inactive', sub: 'dead', mainPid: 0 });
++ host.addUnit(`openre-jsmpeg@${OLD}.service`, { active: 'inactive', sub: 'dead', mainPid: 0 });
// openre is PostgreSQL: the production database and a healthy pgBackRest stanza so that
// `ovhost backup openre` (and the drill's restore of its .dump) can succeed.
host.pgDatabases.add('ov_openre');
@@@ -87,7 +89,7 @@ runTests([
const inv = normalise(EXAMPLE);
const o = inv.services.openre;
assert.deepStrictEqual(o.units, ['openre-api.service', 'openre-session-coordinator.service']);
-- assert.deepStrictEqual(o.workerUnits, ['[email protected]', '[email protected]']);
++ assert.deepStrictEqual(o.workerUnits, ['[email protected]', '[email protected]', '[email protected]']);
assert.strictEqual(o.layout, 'release');
assert.strictEqual(o.strategy, 'release-layout');
assert.strictEqual(o.managed, true, 'ovhost deploy openre: release + api (WS-N task 11)');
@@@ -117,9 +119,9 @@ runTests([
const host = await openreHost();
const r = await host.cli('status', 'openre');
assert.strictEqual(r.code, 0, r.out);
-- assert.match(r.out, new RegExp(`openre\\s+${RELEASE}\\s+ready\\s+openre-api=active openre-session-coordinator=active workers\\[openre-rtmp-ingest@${RELEASE}=active openre-rtmp-ingest@${OLD}=inactive openre-restream-worker@${RELEASE}=active\\] ingest sessions=0$`, 'm'));
++ assert.match(r.out, new RegExp(`openre\\s+${RELEASE}\\s+ready\\s+openre-api=active openre-session-coordinator=active workers\\[openre-rtmp-ingest@${RELEASE}=active openre-rtmp-ingest@${OLD}=inactive openre-restream-worker@${RELEASE}=active openre-jsmpeg@${RELEASE}=active openre-jsmpeg@${OLD}=inactive\\] ingest sessions=0$`, 'm'));
const json = JSON.parse((await host.cli('status', 'openre', '--json')).out);
-- assert.deepStrictEqual(json[0].workers.map((w) => [w.unit, w.active]), [[`openre-rtmp-ingest@${RELEASE}.service`, 'active'], [`openre-rtmp-ingest@${OLD}.service`, 'inactive'], [`openre-restream-worker@${RELEASE}.service`, 'active']]);
++ assert.deepStrictEqual(json[0].workers.map((w) => [w.unit, w.active]), [[`openre-rtmp-ingest@${RELEASE}.service`, 'active'], [`openre-rtmp-ingest@${OLD}.service`, 'inactive'], [`openre-restream-worker@${RELEASE}.service`, 'active'], [`openre-jsmpeg@${RELEASE}.service`, 'active'], [`openre-jsmpeg@${OLD}.service`, 'inactive']]);
const lists = host.calls.filter((c) => c.cmd === 'systemctl' && c.args[0] === 'list-units');
assert.deepStrictEqual(lists[0].args, ['list-units', '--all', '--plain', '--no-legend', '--no-pager', 'openre-rtmp-ingest@*.service']);
assert.ok(lists.every((c) => !c.privileged), 'listing needs no privileges');
@@@ -130,7 +132,7 @@ runTests([
let host = await openreHost();
let res = JSON.parse((await host.cli('validate', 'openre', '--json')).out);
const workers = res.findings.filter((f) => f.area === 'units' && /^worker /.test(f.message));
-- assert.deepStrictEqual(workers.map((f) => f.level), ['info', 'info', 'info']);
++ assert.deepStrictEqual(workers.map((f) => f.level), ['info', 'info', 'info', 'info', 'info']);
assert.match(workers[0].message, /never starts, stops or restarts it/);
assert.ok(!res.findings.some((f) => /no running instance/.test(f.message)));
assert.deepStrictEqual(stateChanges(host), []);
@@@ -139,6 +141,7 @@ runTests([
assert.deepStrictEqual(res.findings.filter((f) => /no running instance/.test(f.message)).map((f) => [f.level, f.message]), [
['warn', 'no running instance of worker unit [email protected]'],
['warn', 'no running instance of worker unit [email protected]'],
++ ['warn', 'no running instance of worker unit [email protected]'],
]);
assert.deepStrictEqual(stateChanges(host), []);
assert.ok(!JSON.stringify(res).includes(SECRET));
ddiff --git a/test/strategy-hosts.js b/test/strategy-hosts.js
iindex bbc0ceb..1035e56 100644
---- a/test/strategy-hosts.js
++++ b/test/strategy-hosts.js
@@@ -161,7 +161,7 @@ function openreEntry(overrides = {}) {
strategy: 'release-layout',
release: { id: 'sha12', reuseModules: false, chown: 'ubuntu:ubuntu', keep: 2 },
units: ['openre-api.service', 'openre-session-coordinator.service'],
-- workerUnits: ['[email protected]', '[email protected]'],
envFile: '/etc/openvibe/openre.env',
port: 4500,
ready: { url: 'http://127.0.0.1:4500/api/ready', timeoutSeconds: 60 },
@@@ -187,6 +187,7 @@ async function openreHost(overrides = {}) {
host.put('/var/lib/openre/openre.db', 'sqlite', { owner: 'ubuntu' });
host.addUnit(`openre-rtmp-ingest@${host.firstRelease}.service`, { mainPid: 3102 });
host.addUnit(`openre-restream-worker@${host.firstRelease}.service`, { mainPid: 3103 });
++ host.addUnit(`openre-jsmpeg@${host.firstRelease}.service`, { mainPid: 3105 });
host.sessions = 0;
host.sqliteHandler = () => [{ n: host.sessions }];
host.http.set('http://127.0.0.1:4500/api/ready', host.readyFor('openre-api.service'));
ddiff --git a/test/strategy-release-layout.test.js b/test/strategy-release-layout.test.js
iindex c365384..574d755 100644
---- a/test/strategy-release-layout.test.js
++++ b/test/strategy-release-layout.test.js
@@@ -332,7 +332,7 @@ runTests([
assert.ok(host.calls.some((c) => c.cmd === 'chown' && c.args.join(' ') === `-R ubuntu:ubuntu /opt/openre.stream/releases/${to.slice(0, 12)}`));
assert.strictEqual(host.files.get(`/opt/openre.stream/releases/${to.slice(0, 12)}/server/index.js`).owner, 'ubuntu');
assert.deepStrictEqual(host.restarts(), ['openre-api.service', 'openre-session-coordinator.service']);
-- assert.ok(!host.calls.some((c) => c.cmd === 'systemctl' && /openre-(rtmp-ingest|restream-worker)@/.test(String(c.args[1])) && c.args[0] !== 'show'), 'no worker unit is started, stopped or restarted');
++ assert.ok(!host.calls.some((c) => c.cmd === 'systemctl' && /openre-(rtmp-ingest|restream-worker|jsmpeg)@/.test(String(c.args[1])) && c.args[0] !== 'show'), 'no worker unit is started, stopped or restarted');
assert.ok((await host.releaseIds()).includes(first), 'the release the workers run from is kept');
// A second deploy prunes to keep (2), but never the release a worker instance runs from.
host.push({ 'server/index.js': 'api3();' }, 'api3');
@@@ -344,6 +344,26 @@ runTests([
assert.ok(!ids.includes(to.slice(0, 12)), 'an unused old release is pruned');
}),
++ test('openre: a release only its JSMPEG worker runs from survives pruning', async () => {
++ const host = await openreHost();
++ const first = host.firstRelease;
++ // The RTMP and restream workers have moved on; only openre-jsmpeg@<first> is still running from
++ // the first release, so it is the only thing keeping that release alive.
++ for (const w of ['openre-rtmp-ingest', 'openre-restream-worker']) {
++ const u = host.units.get(`${w}@${first}.service`);
++ u.active = 'inactive';
++ u.sub = 'dead';
++ }
++ host.push({ 'server/index.js': 'api2();' }, 'api2');
++ assert.strictEqual((await host.cli('deploy', 'openre')).code, 0);
++ host.push({ 'server/index.js': 'api3();' }, 'api3');
++ assert.strictEqual((await host.cli('deploy', 'openre')).code, 0);
++ const ids = await host.releaseIds();
++ assert.ok(ids.includes(first), `the JSMPEG worker's release survives pruning (${ids.join(' ')})`);
++ // Listing it is all ovhost ever does: the JSMPEG worker unit is never started, stopped or restarted.
++ assert.ok(!host.calls.some((c) => c.cmd === 'systemctl' && c.args[0] !== 'show' && String(c.args[1]).includes('openre-jsmpeg')), 'the JSMPEG worker unit is only ever read');
++ }),
++
test('openre: an ingest session refuses the API restart; --to <sha12> rolls back; a docs-only change needs no restart', async () => {
const host = await openreHost();
const first = host.firstRelease;