openvibers/openvibe.live Commits

Home stats: an expired snapshot is answered at once and recomputed behind it, one recompute for everyone (the hero's p95 was 1 s: whoever came after each 30 s expiry waited for the COUNTs) (#189)

OP OpenVibers committed 27 days ago · 343708d6f2 · 2 files, +53 −5

Changes

ddiff --git a/server/db/database.js b/server/db/database.js
iindex fcacb192..4276d5c4 100644
---- a/server/db/database.js
++++ b/server/db/database.js
@@@ -1484,12 +1484,19 @@ async function getReadingSeries(metric, days = 7) {
};
}
++// Once there is a snapshot, nobody waits for the next one: an expired snapshot is answered at once and recomputed
++// behind the answer, one recompute for everyone (the dozens of windowed COUNTs made whoever came after each expiry
++// wait ~1 s: the home hero's p95 on 2026-10-17). Only the very first call waits.
++let _homeStatsRefresh = null;
async function getHomeStats() {
-- const now = Date.now();
-- if (_homeStatsCache && (now - _homeStatsCacheAt) < _HOME_STATS_TTL) return _homeStatsCache;
-- _homeStatsCache = await _computeHomeStats();
-- _homeStatsCacheAt = now;
-- return _homeStatsCache;
++ if (_homeStatsCache && (Date.now() - _homeStatsCacheAt) < _HOME_STATS_TTL) return _homeStatsCache;
++ if (!_homeStatsRefresh) {
++ _homeStatsRefresh = _computeHomeStats()
++ .then((stats) => { _homeStatsCache = stats; _homeStatsCacheAt = Date.now(); return stats; })
++ .finally(() => { _homeStatsRefresh = null; });
++ }
++ if (_homeStatsCache) { _homeStatsRefresh.catch((e) => console.warn('[Home] stats refresh failed:', e.message)); return _homeStatsCache; }
++ return await _homeStatsRefresh;
}
async function _computeHomeStats() {
ddiff --git a/test/home-stats-swr.test.js b/test/home-stats-swr.test.js
nnew file mode 100644
iindex 00000000..17139dc5
---- /dev/null
++++ b/test/home-stats-swr.test.js
@@@ -0,0 +1,41 @@
++'use strict';
++/**
++ * db.getHomeStats once a snapshot exists: an expired snapshot is answered at once and recomputed behind the answer,
++ * one recompute for everyone (whoever came after each 30 s expiry used to wait for dozens of COUNTs, ~1 s: the home
++ * hero's p95 on 2026-10-17). Only the first call waits.
++ *
++ * node --import ./test/helpers/pg-preload.mjs test/home-stats-swr.test.js
++ */
++const assert = require('assert');
++const db = require('../server/db/database');
++
++(async () => {
++ await db.initDb();
++ const realNow = Date.now;
++ let skew = 0;
++ Date.now = () => realNow() + skew;
++
++ await db.createUser({ username: 'ann', email: 'ann@x', password_hash: 'x', display_name: 'ann', stream_key: 'a'.repeat(32) });
++ const first = await db.getHomeStats();
++ assert.strictEqual(first.users, 1, 'the first call waits for a snapshot');
++ assert.strictEqual(await db.getHomeStats(), first, 'fresh: the same snapshot');
++
++ await db.createUser({ username: 'bob', email: 'bob@x', password_hash: 'x', display_name: 'bob', stream_key: 'b'.repeat(32) });
++ skew = 31_000; // past the 30 s memo
++ const [a, b] = await Promise.all([db.getHomeStats(), db.getHomeStats()]);
++ assert.strictEqual(a, first, 'expired: the last snapshot at once');
++ assert.strictEqual(b, first);
++
++ // The recompute lands behind the answers; the next call sees it.
++ let next = first;
++ for (let i = 0; i < 50 && next === first; i++) {
++ await new Promise((r) => setTimeout(r, 20));
++ next = await db.getHomeStats();
++ }
++ assert.notStrictEqual(next, first, 'the recompute landed');
++ assert.strictEqual(next.users, 2);
++
++ Date.now = realNow;
++ console.log('home stats: stale-while-revalidate checks passed');
++ process.exit(0);
++})().catch((e) => { console.error(e); process.exit(1); });