openvibers/openvibe.live Commits

Real-user measurements survive deploys: the saved reservoirs are read after the database opens (read at module load, the read always failed and the first save overwrote them, so each deploy emptied RUM: ~17 samples kept of ~1,000 beacons) (#276)

OP OpenVibers committed 24 days ago · 5d26f9221f · 2 files, +25 −5

Changes

ddiff --git a/server/index.js b/server/index.js
iindex 1d448899..bab2f248 100644
---- a/server/index.js
++++ b/server/index.js
@@@ -177,7 +177,7 @@ const stopper = gracefulStop({
// No new background runs from here on (the jobs helper's loops); and end SSE streams so
// the drain can actually complete instead of always hitting the deadline.
() => { require('./utils/jobs').stopAll(); },
-- async () => { if (!drill.enabled) await db.setState('rum_state', JSON.stringify(rum.state())).catch(() => {}); },
++ async () => { if (!drill.enabled && rumRestored) await db.setState('rum_state', JSON.stringify(rum.state())).catch(() => {}); },
() => { require('./streaming/live-events').closeAll(); },
],
beforeDrain: async () => {
@@@ -886,9 +886,17 @@ app.get('/metrics/rum', (req, res) => {
if (!require('openvibe-shared/metrics').isLoopbackDirect(req)) return res.status(404).json({ error: 'Not found' });
res.set('Cache-Control', 'no-store'); res.json(rum.snapshot());
});
--// The reservoirs survive restarts: app_state rum_state (numbers only), saved every 5 minutes and at shutdown.
--if (!drill.enabled) {
-- db.getState('rum_state').then((raw) => { try { rum.restore(JSON.parse(raw || 'null')); } catch { /* start empty */ } }).catch(() => {});
++// The reservoirs survive restarts: app_state rum_state (numbers only), saved every 5 minutes and at shutdown. They are
++// read back once the database is open (start(), after initDb). Read at module load, the read always failed before the
++// pool existed, and the first save then overwrote the stored reservoirs with what had arrived since boot: every
++// deploy emptied them (2026-10-20: about 17 samples kept of ~1,000 beacons in three days). Nothing is saved until the
++// read has succeeded, so a failed read can never overwrite what is stored.
++let rumRestored = false;
++async function restoreRum() {
++ let raw;
++ try { raw = await db.getState('rum_state'); } catch (err) { console.warn('[RUM] could not read the saved reservoirs:', err.message); return; }
++ try { rum.restore(JSON.parse(raw || 'null')); } catch { /* a malformed save: start empty */ }
++ rumRestored = true;
require('./utils/jobs').every('rum-save', 5 * 60_000, () => db.setState('rum_state', JSON.stringify(rum.state())));
}
@@@ -1207,6 +1215,7 @@ async function start() {
await db.initDb();
await analyticsStore.ready();
console.log('[Server] Database ready');
++ await restoreRum(); // the real-user reservoirs, now that the database is open
// Request telemetry (openvibe-sdk/service, plan T1): one aggregated sample per route template, method and status
// class each minute, stored in telemetry_samples; rows older than 14 days go daily. A drill never gets here.
telemetry.init({ service: 'live', sink: createPgTelemetrySink({ db: db.getDb(), log: console }), skipPrefixes: ['/shared'], intervalMs: 60_000 });
ddiff --git a/test/rum.test.js b/test/rum.test.js
iindex d313c100..7bcd630b 100644
---- a/test/rum.test.js
++++ b/test/rum.test.js
@@@ -92,7 +92,18 @@ const { createRum, pageKind, deviceClass } = require('../server/web/rum');
assert.deepStrictEqual([Object.keys(junk.snapshot().pages), junk.snapshot().pages['home|phone'].lcp.n], [['home|phone'], 1], 'malformed keys and values are left out');
const index = fs.readFileSync(path.join(__dirname, '../server/index.js'), 'utf8');
assert.ok(index.includes("app.get('/metrics/rum'") && index.includes('isLoopbackDirect(req)'), 'the host reads the snapshot on loopback only');
-- assert.ok(/if \(!drill\.enabled\) \{\s*db\.getState\('rum_state'\)/.test(index), 'restored at boot, never in a restore drill');
++ // Restored once the database is open (read at module load, the read failed before the pool existed and the first save
++ // overwrote the stored reservoirs: every deploy emptied them), and nothing is saved before a successful read.
++ const initAt = index.indexOf('await db.initDb();');
++ const restoreAt = index.indexOf('await restoreRum();');
++ assert.ok(initAt > 0 && restoreAt > initAt, 'restoreRum() runs in start(), after initDb');
++ assert.ok(index.indexOf('if (drill.enabled) return await startDrill();') < restoreAt, 'never in a restore drill');
++ const fn = index.slice(index.indexOf('async function restoreRum()'), index.indexOf('async function restoreRum()') + 900);
++ assert.ok(fn.indexOf("db.getState('rum_state')") < fn.indexOf('rumRestored = true') && fn.indexOf('rumRestored = true') < fn.indexOf("every('rum-save'"),
++ 'read first, then mark restored, then start saving');
++ assert.ok(/catch \(err\) \{[^}]*return; \}/.test(fn), 'a failed read returns before any save is scheduled');
++ assert.ok(index.includes("if (!drill.enabled && rumRestored) await db.setState('rum_state'"), 'the shutdown save waits for a restore too');
++ assert.ok(!/^db\.getState\('rum_state'\)/m.test(index) && !/^\s{4}db\.getState\('rum_state'\)\.then/m.test(index), 'no read at module load any more');
// app.js: every page load, once idle, through the runtime's versioned loader (a quarter of them gave too few
// samples at this traffic; automated browsers are skipped in the script itself).