diff options
Diffstat (limited to 'packages/meshbay-hub/tests')
| -rw-r--r-- | packages/meshbay-hub/tests/test_streamed_download_reliability.py | 115 |
1 files changed, 109 insertions, 6 deletions
diff --git a/packages/meshbay-hub/tests/test_streamed_download_reliability.py b/packages/meshbay-hub/tests/test_streamed_download_reliability.py index 45138b4..9e6f77d 100644 --- a/packages/meshbay-hub/tests/test_streamed_download_reliability.py +++ b/packages/meshbay-hub/tests/test_streamed_download_reliability.py @@ -49,7 +49,8 @@ globalThis.localStorage = { removeItem: k => store.delete(k), }; const PLAN = %(plan)s; -const log = { registers: 0, claims: 0, navigations: 0, served: 0 }; +const log = { registers: 0, claims: 0, navigations: 0, served: 0, + unregisters: 0 }; // The worker as the page sees it: something with postMessage. It answers a // navigation by posting mbdl-serving back on the port it was handed, which is @@ -75,15 +76,32 @@ Object.defineProperty(globalThis, 'navigator', { register: async () => { log.registers += 1; if (PLAN.registerThrows) throw new Error('registration blocked'); - if (PLAN.controlAfterMs !== null) { + // A registration that never answers at all. Distinct from one that + // rejects: nothing is reported, nothing fails, the caller just waits. + if (PLAN.registerHangs) await new Promise(() => {}); + // A worker that only becomes installable once the stuck registration + // has been thrown away -- the browser this was reported from. + const healed = PLAN.activeAfterUnregister && log.unregisters > 0; + if (PLAN.controlAfterMs !== null || healed) { setTimeout(() => { controller = makeController(); for (const fn of listeners) fn(); - }, PLAN.controlAfterMs); + }, healed ? 0 : PLAN.controlAfterMs); } - return {active: PLAN.active ? makeController() : null}; + return { + active: (PLAN.active || healed) ? makeController() : null, + unregister: async () => { log.unregisters += 1; return true; }, + }; + }, + // `register()` resolves as soon as the registration object exists, with + // nothing but an installing worker; `ready` is what waits for an active + // one. Measured on Firefox 154: an install handler that rejects leaves + // `ready` unsettled past ten seconds while `register()` returns in 7 ms. + get ready() { + const healed = PLAN.activeAfterUnregister && log.unregisters > 0; + return (PLAN.readySettles || healed) + ? Promise.resolve({}) : new Promise(() => {}); }, - ready: Promise.resolve({}), addEventListener: (type, fn) => { if (type === 'controllerchange') listeners.add(fn); }, removeEventListener: (type, fn) => { listeners.delete(fn); }, }, @@ -122,7 +140,9 @@ const out = {}; def _run(tmp_path, body, *, control_after_ms=0, active=True, - serve="always", register_throws=False, control_budget_ms=800): + serve="always", register_throws=False, control_budget_ms=800, + ready_settles=True, register_hangs=False, + active_after_unregister=False): module = tmp_path / "downloads.mjs" module.write_text(DOWNLOADS.read_text()) (tmp_path / "package.json").write_text('{"type":"module"}') @@ -131,6 +151,9 @@ def _run(tmp_path, body, *, control_after_ms=0, active=True, "active": active, "serveOnNavigation": serve, "registerThrows": register_throws, + "readySettles": ready_settles, + "registerHangs": register_hangs, + "activeAfterUnregister": active_after_unregister, } script = tmp_path / "case.mjs" script.write_text( @@ -274,3 +297,83 @@ def test_the_worker_answers_a_re_claim(tmp_path): if sw.js implements the other half.""" sw = (STATIC / "sw.js").read_text() assert "mbdl-claim" in sw and "clients.claim()" in sw + + +# ── Nothing on this path may wait for ever ────────────────────────────────── + +def test_a_worker_that_never_installs_does_not_hang_every_download(tmp_path): + """The one that reached a person: four downloads stuck at "preparing", for + ever, with nothing in the node's journal because no transfer had been asked + for yet. + + `register()` resolves as soon as the registration object exists — with + nothing but an *installing* worker — and `ready` waits for an active one. + Measured on Firefox 154: an install handler that rejects leaves `ready` + unsettled past ten seconds while `register()` returns in seven + milliseconds. Neither had a deadline, and `_swPromise` is shared, so every + download on the page waited on the same promise that would never settle. + """ + out = _run(tmp_path, """ +const t0 = Date.now(); +out.worker = await M.openStreamedDownload('film.mkv', 1, FAST); +out.ms = Date.now() - t0; +out.why = M.lastStreamFailure(); +""", ready_settles=False, active=False, control_after_ms=None, + control_budget_ms=300) + assert out["worker"] is None + assert out["ms"] < 8000, ( + f"gave up after {out['ms']}ms — a budget that is not enforced is not a " + "budget, and the row above it says 'preparing' the whole time") + assert "active" in out["why"], out["why"] + + +def test_a_stuck_ready_does_not_throw_away_a_working_worker(tmp_path): + """`ready` can be waiting on a *newer* worker that cannot install while an + older one is perfectly able to serve. Giving up then would cost Firefox the + only unbounded way it has to write a download to disk — a deadline must + bound the waiting, never remove the capability.""" + out = _run(tmp_path, """ +const target = await M.openStreamedDownload('film.mkv', 1, FAST); +out.target = target !== null; +// Closing stops the keep-alive; left open, its interval keeps this process +// alive well past the test's own timeout. +if (target) await target.writable.close(); +""", ready_settles=False, active=True, control_budget_ms=300) + assert out["target"] is True + + +def test_a_registration_that_never_answers_gives_up_too(tmp_path): + """The other unbounded await. It rejects loudly in the case above; this is + the case where it says nothing at all.""" + out = _run(tmp_path, """ +const t0 = Date.now(); +out.worker = await M.openStreamedDownload('film.mkv', 1, FAST); +out.ms = Date.now() - t0; +out.why = M.lastStreamFailure(); +""", register_hangs=True, active=False, control_after_ms=None, + control_budget_ms=300) + assert out["worker"] is None + assert out["ms"] < 8000, f"gave up after {out['ms']}ms" + assert "register" in out["why"], out["why"] + + +def test_a_registration_stuck_installing_is_discarded_and_asked_for_again(tmp_path): + """A deadline turns an invisible hang into a named failure, which is better + but is not a fix: a registration stuck with nothing but an installing worker + does not heal on its own. Every later visit finds the same registration and + waits on the same `ready`, so the browser stays unable to stream a download + until somebody opens developer tools — and on Firefox there is nothing else + that can write a film to disk. + + So the stuck registration is thrown away and asked for once more. + """ + out = _run(tmp_path, """ +const target = await M.openStreamedDownload('film.mkv', 1, FAST); +out.target = target !== null; +if (target) await target.writable.close(); +""", ready_settles=False, active=False, control_after_ms=None, + active_after_unregister=True, control_budget_ms=300) + assert out["log"]["unregisters"] == 1, ( + "the stuck registration was left in place") + assert out["target"] is True, ( + "discarding it did not get the page a worker it could stream to") |