89 lines
3.1 KiB
JavaScript
89 lines
3.1 KiB
JavaScript
|
|
// The leftover-flush watcher, driven with a fake clock and fake s3d stats.
|
||
|
|
|
||
|
|
import test from 'node:test';
|
||
|
|
import assert from 'node:assert/strict';
|
||
|
|
import { createAutoFlush, clampMinutes } from '../core/autoflush.js';
|
||
|
|
|
||
|
|
function rig({ enabled = true, minutes = 5 } = {}) {
|
||
|
|
const r = { t: 0, pending: { pendingObjects: 0, pendingSize: 0 }, flushes: 0, fail: null, prefs: { enabled, minutes }, down: false };
|
||
|
|
r.af = createAutoFlush({
|
||
|
|
settings: () => r.prefs,
|
||
|
|
stats: async () => { if (r.down) throw new Error('refused'); return { ...r.pending }; },
|
||
|
|
flush: async () => {
|
||
|
|
if (r.fail) throw new Error(r.fail);
|
||
|
|
r.flushes++;
|
||
|
|
r.pending = { pendingObjects: 0, pendingSize: 0 };
|
||
|
|
},
|
||
|
|
now: () => r.t,
|
||
|
|
});
|
||
|
|
r.at = async (min) => { r.t = min * 60_000; await r.af.tick(); };
|
||
|
|
return r;
|
||
|
|
}
|
||
|
|
|
||
|
|
test('flushes once the pending set has been unchanged for N minutes', async () => {
|
||
|
|
const r = rig({ minutes: 5 });
|
||
|
|
r.pending = { pendingObjects: 3, pendingSize: 1000 };
|
||
|
|
await r.at(0);
|
||
|
|
await r.at(4.9);
|
||
|
|
assert.equal(r.flushes, 0);
|
||
|
|
assert.equal(r.af.status().dueAt, 5 * 60_000);
|
||
|
|
await r.at(5);
|
||
|
|
assert.equal(r.flushes, 1);
|
||
|
|
assert.equal(r.af.status().lastFlushAt, 5 * 60_000);
|
||
|
|
await r.at(20);
|
||
|
|
assert.equal(r.flushes, 1, 'nothing pending, nothing to flush');
|
||
|
|
});
|
||
|
|
|
||
|
|
test('a new upload restarts the quiet period', async () => {
|
||
|
|
const r = rig({ minutes: 5 });
|
||
|
|
r.pending = { pendingObjects: 1, pendingSize: 10 };
|
||
|
|
await r.at(0);
|
||
|
|
await r.at(4);
|
||
|
|
r.pending = { pendingObjects: 2, pendingSize: 20 };
|
||
|
|
await r.at(4.5);
|
||
|
|
await r.at(9);
|
||
|
|
assert.equal(r.flushes, 0);
|
||
|
|
await r.at(9.5);
|
||
|
|
assert.equal(r.flushes, 1);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('off means never, and s3d being down is not an error', async () => {
|
||
|
|
const r = rig({ enabled: false });
|
||
|
|
r.pending = { pendingObjects: 5, pendingSize: 50 };
|
||
|
|
for (const m of [0, 10, 100]) await r.at(m);
|
||
|
|
assert.equal(r.flushes, 0);
|
||
|
|
assert.equal(r.af.status().dueAt, null);
|
||
|
|
r.prefs.enabled = true;
|
||
|
|
r.down = true;
|
||
|
|
for (const m of [101, 200]) await r.at(m);
|
||
|
|
assert.equal(r.flushes, 0);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('failed flushes back off, doubling up to an hour', async () => {
|
||
|
|
const r = rig({ minutes: 2 });
|
||
|
|
r.pending = { pendingObjects: 1, pendingSize: 1 };
|
||
|
|
r.fail = 'indexer unreachable';
|
||
|
|
await r.at(0);
|
||
|
|
await r.at(2); // fails; next try 4 min later
|
||
|
|
assert.equal(r.af.status().lastError, 'indexer unreachable');
|
||
|
|
assert.equal(r.af.status().dueAt, 6 * 60_000);
|
||
|
|
await r.at(6); // fails again; next try 8 min later
|
||
|
|
assert.equal(r.af.status().dueAt, 14 * 60_000);
|
||
|
|
for (let i = 0; i < 6; i++) await r.at(r.af.status().dueAt / 60_000);
|
||
|
|
assert.equal(r.af.status().dueAt - r.t, 60 * 60_000, 'capped at an hour');
|
||
|
|
r.fail = null;
|
||
|
|
await r.at(r.af.status().dueAt / 60_000);
|
||
|
|
assert.equal(r.flushes, 1);
|
||
|
|
assert.equal(r.af.status().lastError, null);
|
||
|
|
r.pending = { pendingObjects: 1, pendingSize: 2 };
|
||
|
|
await r.at(r.t / 60_000);
|
||
|
|
assert.equal(r.af.status().dueAt - r.t, 2 * 60_000, 'a success resets the backoff');
|
||
|
|
});
|
||
|
|
|
||
|
|
test('minutes are clamped to a sane range', () => {
|
||
|
|
assert.equal(clampMinutes(0), 1);
|
||
|
|
assert.equal(clampMinutes(99999), 1440);
|
||
|
|
assert.equal(clampMinutes('abc'), 10);
|
||
|
|
assert.equal(clampMinutes('7'), 7);
|
||
|
|
});
|