test(canvas): verify retry deadlines across wall-clock shifts and recovery
This commit is contained in:
@@ -225,3 +225,48 @@ test('bounded batches drain through settle without waiting for fake timers and h
|
||||
test('a fenced pump reports failed state publication without throwing or sending through the old identity',async()=>{
|
||||
const state=setup({mutate(){throw Error('storage_leader_lost');}});state.scheduler.start();state.add('fenced');await tick();await state.scheduler.settle();assert.equal(state.calls.length,0);assert.equal(state.ledger().getSnapshot().requests.size,0);assert.equal(state.ledger().getSnapshot().operations.get('fenced').phase,'pending');assert.ok(state.errors.some(error=>error.message==='storage_leader_lost'));state.scheduler.stop();
|
||||
});
|
||||
|
||||
// Wall time can change independently of the elapsed time used by browser timers.
|
||||
function adjustableClock(initial=1000){
|
||||
let wall=initial,elapsed=0,next=0;const timers=new Map();
|
||||
return{clock:{now:()=>wall,setTimer(callback,delay){const id=++next;timers.set(id,{callback,delay,at:elapsed+delay});return id;},clearTimer:id=>timers.delete(id)},timers,
|
||||
setWall(value){wall=value;},fireAfter(ms){elapsed+=ms;const due=[...timers].filter(([,timer])=>timer.at<=elapsed);for(const[id,timer]of due){timers.delete(id);timer.callback();}return due.map(([,timer])=>timer.callback);}};
|
||||
}
|
||||
|
||||
test('wall-clock jumps recheck retry deadlines without releasing unknown claims or bypassing dependencies',async()=>{
|
||||
const clock=adjustableClock(),receipt=deferred(),calls=[];
|
||||
const state=setup({clock:clock.clock,
|
||||
async submit(request){calls.push(['submit',request]);return{outcome:request.operationIds[0]==='first'?'unresolved':'applied'};},
|
||||
async query(request){calls.push(['query',request]);return receipt.promise;},
|
||||
});
|
||||
state.add('first');state.add('dependent',['canvas'],['first']);state.scheduler.start();await state.scheduler.settle();
|
||||
const frozen=state.ledger().getSnapshot().requests.get('request-1');assert.equal(frozen.retryAt,1100);
|
||||
clock.setWall(800);clock.fireAfter(100);await state.scheduler.settle();
|
||||
assert.equal(calls.length,1);assert.equal(state.scheduler.getClaims().get('canvas'),frozen.id);
|
||||
assert.equal(clock.timers.size,1);assert.equal([...clock.timers.values()][0].delay,300);
|
||||
state.add('independent',['other']);await state.scheduler.settle();
|
||||
assert.deepEqual(calls.map(([action,request])=>[action,request.operationIds[0]]),[['submit','first'],['submit','independent']]);
|
||||
clock.setWall(5000);const staleCallbacks=clock.fireAfter(300);await tick();
|
||||
assert.equal(calls.at(-1)[0],'query');assert.equal(calls.at(-1)[1].body,frozen.body);assert.equal(calls.at(-1)[1].id,frozen.id);
|
||||
assert.equal(state.scheduler.getClaims().get('canvas'),frozen.id);assert.equal(state.ledger().getSnapshot().operations.get('dependent').phase,'pending');
|
||||
for(const callback of staleCallbacks)callback();await tick();assert.equal(calls.length,3);
|
||||
receipt.resolve({outcome:'applied'});await state.scheduler.settle();
|
||||
assert.deepEqual(calls.map(([action,request])=>[action,request.operationIds[0]]),[['submit','first'],['submit','independent'],['query','first'],['submit','dependent']]);
|
||||
for(const callback of staleCallbacks)callback();await state.scheduler.settle();assert.equal(calls.length,4);assert.equal(clock.timers.size,0);assert.equal(state.scheduler.getClaims().size,0);state.scheduler.stop();
|
||||
});
|
||||
|
||||
test('recovered retries recompute remaining wall time and wake the earliest valid request after either clock shift',async()=>{
|
||||
const original=setup({async submit(request){return{outcome:'unresolved',retryAfterMs:request.operationIds[0]==='long'?500:100};}});
|
||||
original.add('long',['a']);original.add('short',['b']);original.scheduler.start();await original.scheduler.settle();const saved=original.ledger().getSnapshot();original.scheduler.stop();
|
||||
for(const now of[800,1200]){
|
||||
const clock=adjustableClock(now),calls=[],restored=setup({clock:clock.clock,async query(request){calls.push(request);return{outcome:'applied'};}},saved);
|
||||
restored.scheduler.start();await restored.scheduler.settle();
|
||||
assert.deepEqual(calls.map(request=>request.operationIds[0]),now===800?[]:['short']);
|
||||
assert.equal(clock.timers.size,1);assert.equal([...clock.timers.values()][0].delay,300);
|
||||
if(now===800){clock.setWall(1100);clock.fireAfter(300);await restored.scheduler.settle();assert.deepEqual(calls.map(request=>request.operationIds[0]),['short']);assert.equal([...clock.timers.values()][0].delay,400);}
|
||||
clock.setWall(1500);clock.fireAfter(now===800?400:300);await restored.scheduler.settle();
|
||||
assert.deepEqual(calls.map(request=>request.operationIds[0]),['short','long']);
|
||||
for(const request of calls){const before=saved.requests.get(request.id);assert.equal(request.body,before.body);assert.deepEqual(request.operationIds,before.operationIds);}
|
||||
assert.equal(clock.timers.size,0);assert.equal(restored.scheduler.getClaims().size,0);restored.scheduler.stop();
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user