mirror of
https://github.com/affaan-m/ECC.git
synced 2026-09-29 13:05:18 +02:00
fix(control-pane): stop a stale poll lowering the staleness mark
The settled-token guard correctly ignored an older poll, but that poll still set settledPoll to its own token on the way out. With three polls settling in the order 3, 1, 2, the ignored poll 1 pulled the mark back to 1, so the even older poll 2 then passed the guard and replaced poll 3's counts, or marked a healthy view offline. The mark now only moves forward. A poll that settles late is itself stale, so it has no business moving the mark at all. Covered by a new runtime case that settles three polls out of order as 3, 1, 2, once with an older success and once with an older failure. The success case catches the counts regressing, the failure case catches a healthy view being marked offline. Reverting the fix trips each of them independently, and the unmutated file passes all 21. Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
This commit is contained in:
@@ -307,10 +307,12 @@ function renderControlPlaneViewHtml() {
|
||||
// reported as an outage rather than swallowed.
|
||||
timer = setTimeout(function () { controller.abort(); }, TIMEOUT_MS);
|
||||
// A poll that has already answered, or timed out, owns the view. Anything
|
||||
// that settles later is superseded and must be dropped.
|
||||
// that settles later is superseded and must be dropped. The watermark only
|
||||
// ever moves forward: a poll that settles late is itself stale, and letting
|
||||
// it lower the mark would re-admit an even older poll behind it.
|
||||
function settle() {
|
||||
clearTimeout(timer);
|
||||
settledPoll = token;
|
||||
if (token > settledPoll) settledPoll = token;
|
||||
}
|
||||
fetch('/api/control-plane', { signal: controller.signal }).then(function (r) {
|
||||
if (!r.ok) throw new Error('Control-plane request failed');
|
||||
|
||||
@@ -395,6 +395,56 @@ let passed = 0;
|
||||
`a timed out poll must announce the outage, got ${hung.elements.get('announce').textContent}`);
|
||||
passed += 1;
|
||||
|
||||
// Three polls settling out of order as #3, #1, #2. The ignored #1 must not
|
||||
// pull the staleness mark back to 1, or the even older #2 would then be let
|
||||
// through on top of #3's counts.
|
||||
async function settleOutOfOrder(second, third) {
|
||||
let releaseFirst, releaseSecond, releaseThird;
|
||||
const holdOne = new Promise(resolve => { releaseFirst = resolve; });
|
||||
const holdTwo = new Promise(resolve => { releaseSecond = resolve; });
|
||||
const holdThree = new Promise(resolve => { releaseThird = resolve; });
|
||||
const view = await render([
|
||||
{ ok: true, data: populatedView({ counts: { tasks: 3, lanes: 1, agents: 2, advisories: 1, resolutions: 0 } }), hold: holdOne },
|
||||
second(holdTwo),
|
||||
third(holdThree)
|
||||
]);
|
||||
// Start all three, then settle them newest first.
|
||||
await view.pollAgain();
|
||||
await view.pollAgain();
|
||||
await view.pollAgain();
|
||||
releaseThird();
|
||||
await settle();
|
||||
releaseFirst();
|
||||
await settle();
|
||||
releaseSecond();
|
||||
await settle();
|
||||
return view;
|
||||
}
|
||||
|
||||
const newest = { tasks: 3, lanes: 1, agents: 2, advisories: 9, resolutions: 0 };
|
||||
// The ignored #1 is a success, and the older #2 is also a success.
|
||||
const outOfOrderSuccess = await settleOutOfOrder(
|
||||
hold => ({ ok: true, data: populatedView({ counts: { tasks: 3, lanes: 1, agents: 2, advisories: 2, resolutions: 1 } }), hold }),
|
||||
hold => ({ ok: true, data: populatedView({ counts: newest }), hold })
|
||||
);
|
||||
assert.ok(outOfOrderSuccess.labelOf('c').includes('9 advisories')
|
||||
&& !outOfOrderSuccess.labelOf('c').includes('2 advisories'),
|
||||
`an older success must not lower the staleness mark, got ${outOfOrderSuccess.labelOf('c')}`);
|
||||
passed += 1;
|
||||
|
||||
// The ignored #1 is a success, and the older #2 is a failure, which would
|
||||
// otherwise mark a healthy view offline.
|
||||
const outOfOrderFailure = await settleOutOfOrder(
|
||||
hold => ({ ok: false, data: { ok: false, error: 'snapshot unavailable' }, hold }),
|
||||
hold => ({ ok: true, data: populatedView({ counts: newest }), hold })
|
||||
);
|
||||
assert.notStrictEqual(outOfOrderFailure.elements.get('status').textContent, 'offline',
|
||||
'a superseded failure must not mark a healthy view offline');
|
||||
assert.ok(outOfOrderFailure.labelOf('c').includes('9 advisories')
|
||||
&& !/unavailable/i.test(outOfOrderFailure.labelOf('c')),
|
||||
`the newest counts must survive an ignored poll, got ${outOfOrderFailure.labelOf('c')}`);
|
||||
passed += 1;
|
||||
|
||||
console.log(`Results: Passed: ${passed}, Failed: 0`);
|
||||
})().catch(error => {
|
||||
console.error(error.message);
|
||||
|
||||
Reference in New Issue
Block a user