mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-21 00:17:47 +08:00
131 lines
3.1 KiB
JavaScript
131 lines
3.1 KiB
JavaScript
const assert = require('node:assert/strict');
|
|
const { test } = require('node:test');
|
|
const {
|
|
InvalidLocalScheduleError,
|
|
assertLocalSchedulePageSize,
|
|
resolveLocalScheduleDecision,
|
|
} = require('../dist/scheduler/localScheduler');
|
|
|
|
const MINUTE = 60_000;
|
|
|
|
function nextMinute(schedule, afterMs) {
|
|
if (schedule.expression !== '* * * * *' || schedule.timezone !== 'UTC') {
|
|
throw new Error('unsupported test schedule');
|
|
}
|
|
return Math.floor(afterMs / MINUTE + 1) * MINUTE;
|
|
}
|
|
|
|
function candidate(overrides = {}) {
|
|
return {
|
|
projectId: 'default',
|
|
triggerId: 'trigger-1',
|
|
triggerRevision: 1,
|
|
triggerContentDigest: 'a'.repeat(64),
|
|
triggerUpdatedAtMs: 1,
|
|
taskId: 'task-1',
|
|
taskRevision: 1,
|
|
taskContentDigest: 'b'.repeat(64),
|
|
expression: '* * * * *',
|
|
timezone: 'UTC',
|
|
misfirePolicy: 'skip',
|
|
stateVersion: 0,
|
|
nextFireAtMs: MINUTE,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
test('admits one on-time occurrence and advances beyond the observation', () => {
|
|
assert.deepEqual(
|
|
resolveLocalScheduleDecision(
|
|
candidate(),
|
|
MINUTE + 1_000,
|
|
5_000,
|
|
nextMinute,
|
|
),
|
|
{
|
|
candidate: candidate(),
|
|
observedAtMs: MINUTE + 1_000,
|
|
scheduledForMs: MINUTE,
|
|
nextFireAtMs: 2 * MINUTE,
|
|
disposition: 'admit',
|
|
},
|
|
);
|
|
});
|
|
|
|
test('applies skip and fire-once misfire without replaying a backlog', () => {
|
|
const observedAtMs = 10 * MINUTE + 30_000;
|
|
const skipped = resolveLocalScheduleDecision(
|
|
candidate(),
|
|
observedAtMs,
|
|
5_000,
|
|
nextMinute,
|
|
);
|
|
assert.equal(skipped.disposition, 'skip');
|
|
assert.equal(skipped.nextFireAtMs, 11 * MINUTE);
|
|
|
|
const admitted = resolveLocalScheduleDecision(
|
|
candidate({ misfirePolicy: 'fire_once' }),
|
|
observedAtMs,
|
|
5_000,
|
|
nextMinute,
|
|
);
|
|
assert.equal(admitted.disposition, 'admit');
|
|
assert.equal(admitted.scheduledForMs, MINUTE);
|
|
assert.equal(admitted.nextFireAtMs, 11 * MINUTE);
|
|
});
|
|
|
|
test('initializes migrated state without inventing a due occurrence', () => {
|
|
const decision = resolveLocalScheduleDecision(
|
|
candidate({ triggerUpdatedAtMs: 30_000, nextFireAtMs: null }),
|
|
40_000,
|
|
5_000,
|
|
nextMinute,
|
|
);
|
|
assert.deepEqual(
|
|
{
|
|
disposition: decision.disposition,
|
|
nextFireAtMs: decision.nextFireAtMs,
|
|
scheduledForMs: decision.scheduledForMs,
|
|
},
|
|
{
|
|
disposition: 'initialize',
|
|
nextFireAtMs: MINUTE,
|
|
scheduledForMs: undefined,
|
|
},
|
|
);
|
|
});
|
|
|
|
test('rejects widened candidates, invalid cron and unbounded pages', () => {
|
|
assert.throws(
|
|
() =>
|
|
resolveLocalScheduleDecision(
|
|
{ ...candidate(), extra: true },
|
|
MINUTE,
|
|
0,
|
|
nextMinute,
|
|
),
|
|
InvalidLocalScheduleError,
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
resolveLocalScheduleDecision(
|
|
candidate({ expression: 'invalid cron' }),
|
|
MINUTE,
|
|
0,
|
|
nextMinute,
|
|
),
|
|
InvalidLocalScheduleError,
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
resolveLocalScheduleDecision(
|
|
candidate(),
|
|
MINUTE,
|
|
0,
|
|
(_schedule, afterMs) => afterMs,
|
|
),
|
|
InvalidLocalScheduleError,
|
|
);
|
|
assert.throws(() => assertLocalSchedulePageSize(257), RangeError);
|
|
});
|