v3.28.7: the batch panel freeze
ramBatchFetch's re-entrancy guard (_ramBatchFetching) is only cleared when the fetch settles, and ramBatchApplyStatus returned without rendering whenever it decided a fetch was due - so one stalled /api/ram-batch/status request (the browser's fetch has no timeout) froze the whole panel (counts, phase, notice, throbber) at the last successful render while the server kept installing (live 2026-10-14: 31 installs, the panel stuck at "Installed: 12"; the log showed all 31 attempts and a healthy /api/status cadence, so the data was there). - The summary render now runs on every poll before the fetch decision: the headline (phase, current card, counts, notice) always follows the 2 s /api/status poll; the detailed fetch only adds the attempts table. - A fetch guard older than RAM_BATCH_FETCH_STALE_MS (10 s) is released, so a stalled fetch cannot block the report forever - the next poll retries and the attempts table recovers. Reproduced with the page's real functions: 12 good fetches, the 13th hung -> the panel stuck at 12 (the reported number); with the fix it follows the summary to 31 and the stale guard retries. Tests: a hung fetch keeps the counts following the summary; a stale guard is released and retried (a fresh one is kept). 919 frontend + 928 Python; sw cache simple-v445.
This commit is contained in:
+15
-3
@@ -1910,7 +1910,7 @@
|
|||||||
// ===== Version =====
|
// ===== Version =====
|
||||||
// Single source of truth for the PWA version: shown in the header and used
|
// Single source of truth for the PWA version: shown in the header and used
|
||||||
// by the server version check in pysimConnect().
|
// by the server version check in pysimConnect().
|
||||||
const SIMPLE_VERSION = '3.28.6';
|
const SIMPLE_VERSION = '3.28.7';
|
||||||
document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION;
|
document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION;
|
||||||
|
|
||||||
// ===== view history (the browser Back/Forward buttons) =====
|
// ===== view history (the browser Back/Forward buttons) =====
|
||||||
@@ -10155,8 +10155,11 @@ function ramHideProgress() {
|
|||||||
// (the attempts list) is fetched only when the summary says it changed.
|
// (the attempts list) is fetched only when the summary says it changed.
|
||||||
let _ramBatchLast = null; // the last detailed /api/ram-batch/status
|
let _ramBatchLast = null; // the last detailed /api/ram-batch/status
|
||||||
let _ramBatchFetching = false;
|
let _ramBatchFetching = false;
|
||||||
|
let _ramBatchFetchAt = 0; // when the in-flight fetch started (the watchdog)
|
||||||
let _ramBatchSummary = null; // /api/status.ram_batch
|
let _ramBatchSummary = null; // /api/status.ram_batch
|
||||||
let _ramBatchProgress = null; // /api/status.ram_progress
|
let _ramBatchProgress = null; // /api/status.ram_progress
|
||||||
|
// A fetch that never settled must not block the report forever (v3.28.7).
|
||||||
|
const RAM_BATCH_FETCH_STALE_MS = 10000;
|
||||||
|
|
||||||
function ramBatchPanelVisible() {
|
function ramBatchPanelVisible() {
|
||||||
const op = document.getElementById('ram-op');
|
const op = document.getElementById('ram-op');
|
||||||
@@ -10167,6 +10170,7 @@ function ramBatchPanelVisible() {
|
|||||||
async function ramBatchFetch() {
|
async function ramBatchFetch() {
|
||||||
if (_ramBatchFetching) return;
|
if (_ramBatchFetching) return;
|
||||||
_ramBatchFetching = true;
|
_ramBatchFetching = true;
|
||||||
|
_ramBatchFetchAt = Date.now();
|
||||||
try {
|
try {
|
||||||
_ramBatchLast = await pysimFetch('/api/ram-batch/status');
|
_ramBatchLast = await pysimFetch('/api/ram-batch/status');
|
||||||
} catch (e) { /* the next poll retries */ }
|
} catch (e) { /* the next poll retries */ }
|
||||||
@@ -10181,14 +10185,22 @@ function ramBatchApplyStatus(status) {
|
|||||||
_ramBatchSummary = status.ram_batch || null;
|
_ramBatchSummary = status.ram_batch || null;
|
||||||
_ramBatchProgress = status.ram_progress || null;
|
_ramBatchProgress = status.ram_progress || null;
|
||||||
if (!ramBatchPanelVisible()) return;
|
if (!ramBatchPanelVisible()) return;
|
||||||
|
// The summary carries the headline (the phase, the current card, the
|
||||||
|
// counts, the notice): render it on every poll, even when a detailed fetch
|
||||||
|
// is due or stuck - a hung fetch used to freeze the whole panel at the
|
||||||
|
// last successful render (v3.28.7).
|
||||||
|
ramBatchRender();
|
||||||
|
if (_ramBatchFetching && Date.now() - _ramBatchFetchAt > RAM_BATCH_FETCH_STALE_MS) {
|
||||||
|
// the in-flight fetch is stale (a stalled connection): release the
|
||||||
|
// guard so this poll retries and the report can recover
|
||||||
|
_ramBatchFetching = false;
|
||||||
|
}
|
||||||
const total = _ramBatchSummary ? (_ramBatchSummary.attempts_total || 0) : 0;
|
const total = _ramBatchSummary ? (_ramBatchSummary.attempts_total || 0) : 0;
|
||||||
const active = _ramBatchSummary ? !!_ramBatchSummary.active : false;
|
const active = _ramBatchSummary ? !!_ramBatchSummary.active : false;
|
||||||
if (!_ramBatchLast || (_ramBatchLast.attempts_total || 0) !== total
|
if (!_ramBatchLast || (_ramBatchLast.attempts_total || 0) !== total
|
||||||
|| !!_ramBatchLast.active !== active) {
|
|| !!_ramBatchLast.active !== active) {
|
||||||
ramBatchFetch();
|
ramBatchFetch();
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
ramBatchRender();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function ramBatchRender() {
|
function ramBatchRender() {
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
const CACHE = 'simple-v444';
|
const CACHE = 'simple-v445';
|
||||||
const URLS = [
|
const URLS = [
|
||||||
'index.html',
|
'index.html',
|
||||||
'help.html',
|
'help.html',
|
||||||
|
|||||||
@@ -29,6 +29,9 @@ for (const fn of ['ramBatchPanelVisible', 'ramBatchAttemptsHtml', 'ramBatchRende
|
|||||||
code += extractFunc(html, fn, ASYNC_FNS.indexOf(fn) >= 0) + '\n';
|
code += extractFunc(html, fn, ASYNC_FNS.indexOf(fn) >= 0) + '\n';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const RAM_BATCH_FETCH_STALE_MS = parseInt(
|
||||||
|
/const RAM_BATCH_FETCH_STALE_MS = (\d+);/.exec(html)[1], 10);
|
||||||
|
|
||||||
const els = {};
|
const els = {};
|
||||||
function setEl(id, value) {
|
function setEl(id, value) {
|
||||||
els[id] = {
|
els[id] = {
|
||||||
@@ -54,7 +57,11 @@ const app = new Function('els', `
|
|||||||
var t = k => k;
|
var t = k => k;
|
||||||
var isViewVisible = () => sandbox.visibility;
|
var isViewVisible = () => sandbox.visibility;
|
||||||
var alert = m => sandbox.alerts.push(String(m));
|
var alert = m => sandbox.alerts.push(String(m));
|
||||||
var pysimFetch = async (p) => { sandbox.fetched.push(p); return sandbox.replies[p] || null; };
|
var pysimFetch = async (p) => {
|
||||||
|
sandbox.fetched.push(p);
|
||||||
|
if (sandbox.hangs) return new Promise(() => {}); // a stalled connection
|
||||||
|
return sandbox.replies[p] || null;
|
||||||
|
};
|
||||||
var pysimFetchOk = async (p, body) => {
|
var pysimFetchOk = async (p, body) => {
|
||||||
sandbox.fetched.push(p);
|
sandbox.fetched.push(p);
|
||||||
sandbox.bodies = sandbox.bodies || {};
|
sandbox.bodies = sandbox.bodies || {};
|
||||||
@@ -68,12 +75,17 @@ const app = new Function('els', `
|
|||||||
var pysimApplyAvailability = () => {};
|
var pysimApplyAvailability = () => {};
|
||||||
var _ramBatchLast = null;
|
var _ramBatchLast = null;
|
||||||
var _ramBatchFetching = false;
|
var _ramBatchFetching = false;
|
||||||
|
var _ramBatchFetchAt = 0;
|
||||||
var _ramBatchSummary = null;
|
var _ramBatchSummary = null;
|
||||||
var _ramBatchProgress = null;
|
var _ramBatchProgress = null;
|
||||||
|
var RAM_BATCH_FETCH_STALE_MS = ${RAM_BATCH_FETCH_STALE_MS};
|
||||||
${code}
|
${code}
|
||||||
sandbox.getLast = () => _ramBatchLast;
|
sandbox.getLast = () => _ramBatchLast;
|
||||||
sandbox.setLast = v => { _ramBatchLast = v; };
|
sandbox.setLast = v => { _ramBatchLast = v; };
|
||||||
sandbox.getSummary = () => _ramBatchSummary;
|
sandbox.getSummary = () => _ramBatchSummary;
|
||||||
|
sandbox.isFetching = () => _ramBatchFetching;
|
||||||
|
sandbox.setFetchingStale = ms => { _ramBatchFetching = true; _ramBatchFetchAt = Date.now() - ms; };
|
||||||
|
sandbox.resetFetching = () => { _ramBatchFetching = false; };
|
||||||
sandbox.getProgress = () => _ramBatchProgress;
|
sandbox.getProgress = () => _ramBatchProgress;
|
||||||
sandbox.ramBatchPanelVisible = ramBatchPanelVisible;
|
sandbox.ramBatchPanelVisible = ramBatchPanelVisible;
|
||||||
sandbox.ramBatchAttemptsHtml = ramBatchAttemptsHtml;
|
sandbox.ramBatchAttemptsHtml = ramBatchAttemptsHtml;
|
||||||
@@ -265,6 +277,65 @@ test('the panel headline follows the live summary (phase and step)', () => {
|
|||||||
assert.strictEqual(els['ram-batch-state'].textContent, 'Insert next card. Waiting for a card...');
|
assert.strictEqual(els['ram-batch-state'].textContent, 'Insert next card. Waiting for a card...');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('a hung detailed fetch does not freeze the counts (v3.28.7)', async () => {
|
||||||
|
clearEls();
|
||||||
|
setEl('ram-op', 'batch');
|
||||||
|
setEl('ram-batch-panel', '');
|
||||||
|
setEl('ram-batch-start', '');
|
||||||
|
setEl('ram-batch-stop', '');
|
||||||
|
setEl('ram-batch-clear', '');
|
||||||
|
setEl('ram-batch-spin', '');
|
||||||
|
setEl('ram-batch-state', '');
|
||||||
|
setEl('ram-batch-notice', '');
|
||||||
|
setEl('ram-batch-counts', '');
|
||||||
|
setEl('ram-batch-attempts', '');
|
||||||
|
app.hangs = true; // every detailed fetch stalls
|
||||||
|
const sum = n => ({ ram_batch: { active: true, phase: 'waiting_card', attempts_total: n,
|
||||||
|
counts: { installed: n, failed: 0, skipped: 0 } } });
|
||||||
|
app.ramBatchApplyStatus(sum(12));
|
||||||
|
await new Promise(r => setImmediate(r));
|
||||||
|
assert.match(els['ram-batch-counts'].textContent, /Installed: 12/);
|
||||||
|
// the fetch never settles, but the headline must follow the summary
|
||||||
|
app.ramBatchApplyStatus(sum(31));
|
||||||
|
await new Promise(r => setImmediate(r));
|
||||||
|
assert.match(els['ram-batch-counts'].textContent, /Installed: 31/);
|
||||||
|
app.hangs = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a stale fetch guard is released so the report recovers (v3.28.7)', async () => {
|
||||||
|
clearEls();
|
||||||
|
setEl('ram-op', 'batch');
|
||||||
|
setEl('ram-batch-panel', '');
|
||||||
|
setEl('ram-batch-start', '');
|
||||||
|
setEl('ram-batch-stop', '');
|
||||||
|
setEl('ram-batch-clear', '');
|
||||||
|
setEl('ram-batch-spin', '');
|
||||||
|
setEl('ram-batch-state', '');
|
||||||
|
setEl('ram-batch-notice', '');
|
||||||
|
setEl('ram-batch-counts', '');
|
||||||
|
setEl('ram-batch-attempts', '');
|
||||||
|
app.fetched = [];
|
||||||
|
app.replies['/api/ram-batch/status'] = { active: true, phase: 'waiting_card', attempts: [],
|
||||||
|
attempts_total: 31, counts: { installed: 31, failed: 0, skipped: 0 } };
|
||||||
|
// the guard stuck for 11 s: the poll releases it and retries
|
||||||
|
app.setFetchingStale(11000);
|
||||||
|
app.ramBatchApplyStatus({ ram_batch: { active: true, phase: 'waiting_card', attempts_total: 31,
|
||||||
|
counts: { installed: 31, failed: 0, skipped: 0 } } });
|
||||||
|
// the stale guard is released and this poll retries (the fetch is issued)
|
||||||
|
await new Promise(r => setImmediate(r));
|
||||||
|
assert.deepStrictEqual(app.fetched, ['/api/ram-batch/status']);
|
||||||
|
assert.ok(!app.isFetching(), 'the guard must be clear again after the fetch');
|
||||||
|
assert.match(els['ram-batch-counts'].textContent, /Installed: 31/);
|
||||||
|
// a fresh guard is kept (no duplicate fetch)
|
||||||
|
app.fetched = [];
|
||||||
|
app.setFetchingStale(1000);
|
||||||
|
app.ramBatchApplyStatus({ ram_batch: { active: true, phase: 'waiting_card', attempts_total: 31,
|
||||||
|
counts: { installed: 31, failed: 0, skipped: 0 } } });
|
||||||
|
await new Promise(r => setImmediate(r));
|
||||||
|
assert.deepStrictEqual(app.fetched, []);
|
||||||
|
app.resetFetching();
|
||||||
|
});
|
||||||
|
|
||||||
test('the stop and clear calls hit their endpoints', async () => {
|
test('the stop and clear calls hit their endpoints', async () => {
|
||||||
clearEls();
|
clearEls();
|
||||||
app.fetched = [];
|
app.fetched = [];
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "pysim-simple-server"
|
name = "pysim-simple-server"
|
||||||
version = "3.28.6"
|
version = "3.28.7"
|
||||||
description = "HTTP REST server wrapping pysim for the SIMple PWA"
|
description = "HTTP REST server wrapping pysim for the SIMple PWA"
|
||||||
requires-python = ">=3.8"
|
requires-python = ">=3.8"
|
||||||
# pysim is a git-only dependency installed explicitly by setup.bat/setup.sh.
|
# pysim is a git-only dependency installed explicitly by setup.bat/setup.sh.
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ from osmocom.tlv import BER_TLV_IE
|
|||||||
from osmocom.utils import rpad
|
from osmocom.utils import rpad
|
||||||
|
|
||||||
|
|
||||||
VERSION = '3.28.6'
|
VERSION = '3.28.7'
|
||||||
|
|
||||||
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE
|
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user