fix(console): stale profile-mismatch response must not stop the replacement poller
Each approval/clarify fallback poller captures its own interval handle; a 409 (or finally) from a request issued by a replaced poller no longer stops or clears the in-flight guard of the poller that superseded it.
This commit is contained in:
1 parent
c713c6efc2
commit
fbb0843acf
3 files changed
+59
-9
No files matched your search
+10
-7
@@ -8063,13 +8063,15 @@ function _startApprovalFallbackPoll(sid) {
|
||||
} catch(e) {
|
||||
// Another profile owns this session now (e.g. a different tab switched the shared
|
||||
// profile cookie): every further poll would 409, so stop and leave the card as-is.
|
||||
if (typeof _sessionProfileMismatchFromError === 'function' && _sessionProfileMismatchFromError(e)) {
|
||||
stopApprovalPollingForSession(sid);
|
||||
// Only this poller may stop itself: a late 409 from a replaced poller must not kill its successor.
|
||||
if (typeof _sessionProfileMismatchFromError === 'function' && _sessionProfileMismatchFromError(e)
|
||||
&& _approvalPollTimer === pollTimer) {
|
||||
stopApprovalPolling();
|
||||
}
|
||||
}
|
||||
finally { _approvalFallbackPollInFlight = false; }
|
||||
finally { if (_approvalPollTimer === pollTimer) _approvalFallbackPollInFlight = false; }
|
||||
};
|
||||
_approvalPollTimer = setInterval(_tick, 1500); // matches the v0.50.247 polling cadence so degraded-mode users see the same responsiveness
|
||||
const pollTimer = _approvalPollTimer = setInterval(_tick, 1500); // matches the v0.50.247 polling cadence so degraded-mode users see the same responsiveness
|
||||
_tick();
|
||||
}
|
||||
|
||||
@@ -9212,8 +9214,9 @@ function _startClarifyFallbackPoll(sid) {
|
||||
};
|
||||
// Profile-mismatch 409: the session is owned by another profile under the current
|
||||
// cookie, so polling can never succeed. Stop quietly; the live card stays standing.
|
||||
// Only this poller may stop itself: a late 409 from a replaced poller must not kill its successor.
|
||||
if (typeof _sessionProfileMismatchFromError === "function" && _sessionProfileMismatchFromError(e)) {
|
||||
stopClarifyPollingForSession(sid);
|
||||
if (_clarifyFallbackTimer === pollTimer) stopClarifyPolling();
|
||||
return;
|
||||
}
|
||||
// A 404 from the active session domain is a STALE-SESSION signal — e.g.
|
||||
@@ -9264,10 +9267,10 @@ function _startClarifyFallbackPoll(sid) {
|
||||
console.warn("[clarify] pending poll failed", logDetails);
|
||||
}
|
||||
} finally {
|
||||
_clarifyFallbackPollInFlight = false;
|
||||
if (_clarifyFallbackTimer === pollTimer) _clarifyFallbackPollInFlight = false;
|
||||
}
|
||||
};
|
||||
_clarifyFallbackTimer = setInterval(_tick, 3000);
|
||||
const pollTimer = _clarifyFallbackTimer = setInterval(_tick, 3000);
|
||||
_tick();
|
||||
}
|
||||
|
||||
|
||||
@@ -209,3 +209,50 @@ def test_clarify_poll_still_warns_on_unexpected_errors():
|
||||
def test_pwa_startup_is_not_preloaded_next_to_its_own_blocking_script():
|
||||
assert '<script src="static/pwa-startup.js?v=__WEBUI_VERSION__"></script>' in INDEX_HTML
|
||||
assert 'rel="preload" href="static/pwa-startup.js' not in INDEX_HTML
|
||||
|
||||
|
||||
def _run_stale_replacement(start_fn, stop_fn, polling_var):
|
||||
"""Poller A's request is in flight; a same-session refresh replaces it with
|
||||
poller B; then A's request fails with a profile-mismatch 409."""
|
||||
node = shutil.which("node")
|
||||
if not node:
|
||||
pytest.skip("node not installed")
|
||||
fns = [
|
||||
_extract_fn(SESSIONS_JS, "_sessionProfileMismatchFromError"),
|
||||
_extract_fn(MESSAGES_JS, start_fn),
|
||||
_extract_fn(MESSAGES_JS, stop_fn),
|
||||
]
|
||||
harness = _HARNESS.replace("STATUS", "409").replace("BODY", json.dumps(_MISMATCH))
|
||||
harness = harness.replace("async function api(){", "var pending = [];\nasync function api(){\n if (pending) return new Promise((_, rej) => pending.push(rej));")
|
||||
script = harness + "\n".join(fns) + f"""
|
||||
(async () => {{
|
||||
{start_fn}('sid1'); // poller A: first request hangs
|
||||
{stop_fn}(); // same-session refresh
|
||||
{polling_var} = 'sid1'; // startApprovalPolling() sets this before the fallback poll
|
||||
{start_fn}('sid1'); // poller B: its request hangs too
|
||||
const timerB = {"_approvalPollTimer" if "Approval" in start_fn else "_clarifyFallbackTimer"};
|
||||
const e = new Error('Session belongs to a different profile');
|
||||
e.status = 409; e.body = JSON.stringify({json.dumps(_MISMATCH)});
|
||||
pending[0](e); // late 409 for poller A
|
||||
await new Promise(r => setTimeout(r, 0));
|
||||
process.stdout.write(JSON.stringify({{
|
||||
polling: {polling_var},
|
||||
timerAlive: {"_approvalPollTimer" if "Approval" in start_fn else "_clarifyFallbackTimer"} === timerB,
|
||||
inFlight: {"_approvalFallbackPollInFlight" if "Approval" in start_fn else "_clarifyFallbackPollInFlight"},
|
||||
}}));
|
||||
}})();
|
||||
"""
|
||||
out = subprocess.run([node, "-e", script], capture_output=True, text=True, timeout=30)
|
||||
assert out.returncode == 0, out.stderr
|
||||
return json.loads(out.stdout)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("start_fn,stop_fn,polling_var", [
|
||||
("_startApprovalFallbackPoll", "stopApprovalPolling", "_approvalPollingSessionId"),
|
||||
("_startClarifyFallbackPoll", "stopClarifyPolling", "_clarifyPollingSessionId"),
|
||||
])
|
||||
def test_stale_profile_mismatch_does_not_stop_replacement_poller(start_fn, stop_fn, polling_var):
|
||||
r = _run_stale_replacement(start_fn, stop_fn, polling_var)
|
||||
assert r["polling"] == "sid1", "a late 409 from a replaced poller must not stop its successor"
|
||||
assert r["timerAlive"] is True
|
||||
assert r["inFlight"] is True, "the successor's in-flight guard must not be cleared by the stale request"
|
||||
@@ -34,11 +34,11 @@ def test_approval_and_clarify_fallback_polls_do_not_overlap():
|
||||
assert "let _approvalFallbackPollInFlight = false" in src
|
||||
assert "if (_approvalFallbackPollInFlight) return" in src
|
||||
assert "_approvalFallbackPollInFlight = true" in src
|
||||
assert "finally { _approvalFallbackPollInFlight = false; }" in src
|
||||
assert "finally { if (_approvalPollTimer === pollTimer) _approvalFallbackPollInFlight = false; }" in src
|
||||
assert "_approvalFallbackPollInFlight = false;\n _approvalPollingSessionId = null;" in src
|
||||
|
||||
assert "let _clarifyFallbackPollInFlight = false" in src
|
||||
assert "if (_clarifyFallbackPollInFlight) return" in src
|
||||
assert "_clarifyFallbackPollInFlight = true" in src
|
||||
assert "finally {\n _clarifyFallbackPollInFlight = false;\n }" in src
|
||||
assert "finally {\n if (_clarifyFallbackTimer === pollTimer) _clarifyFallbackPollInFlight = false;\n }" in src
|
||||
assert "_clarifyFallbackPollInFlight = false;\n _clarifyPollingSessionId = null;" in src
|
||||
Reference in new issue
Block a user