mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-05-26 12:55:16 -07:00
fix(anilist): dedupe failures during retry cooldown and block dead-lette
- Ignore markFailure calls while an item is still within its retry backoff window - Prevent enqueue from re-adding keys already in the dead-letter queue
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
type: fixed
|
||||
area: anilist
|
||||
|
||||
- Prevent repeated missing-token checks from rapidly exhausting AniList retry attempts or duplicating dead-letter entries for the same episode.
|
||||
@@ -71,7 +71,7 @@ test('anilist update queue applies retry backoff and dead-letter', () => {
|
||||
assert.equal((pendingPayload.pending[0]?.nextAttemptAt ?? now) - now, 30_000);
|
||||
|
||||
for (let attempt = 2; attempt <= 8; attempt += 1) {
|
||||
queue.markFailure('k2', `fail-${attempt}`, now);
|
||||
queue.markFailure('k2', `fail-${attempt}`, now + attempt * 6 * 60 * 60 * 1000);
|
||||
}
|
||||
|
||||
const snapshot = queue.getSnapshot(Number.MAX_SAFE_INTEGER);
|
||||
@@ -83,6 +83,52 @@ test('anilist update queue applies retry backoff and dead-letter', () => {
|
||||
);
|
||||
});
|
||||
|
||||
test('anilist update queue ignores duplicate failures while retry is cooling down', () => {
|
||||
const queueFile = createTempQueueFile();
|
||||
const loggerState = createLogger();
|
||||
const queue = createAnilistUpdateQueue(queueFile, loggerState.logger);
|
||||
|
||||
const now = 1_700_000 * 1_000_000;
|
||||
queue.enqueue('k2', 'Backoff Demo', 2);
|
||||
queue.markFailure('k2', 'fail-1', now);
|
||||
|
||||
for (let attempt = 2; attempt <= 12; attempt += 1) {
|
||||
queue.markFailure('k2', `duplicate-${attempt}`, now + attempt);
|
||||
}
|
||||
|
||||
const payload = JSON.parse(fs.readFileSync(queueFile, 'utf-8')) as {
|
||||
pending: Array<{ attemptCount: number; lastError: string | null }>;
|
||||
deadLetter: Array<unknown>;
|
||||
};
|
||||
assert.equal(payload.pending[0]?.attemptCount, 1);
|
||||
assert.equal(payload.pending[0]?.lastError, 'fail-1');
|
||||
assert.deepEqual(queue.getSnapshot(Number.MAX_SAFE_INTEGER), {
|
||||
pending: 1,
|
||||
ready: 1,
|
||||
deadLetter: 0,
|
||||
});
|
||||
});
|
||||
|
||||
test('anilist update queue does not re-enqueue dead-lettered keys', () => {
|
||||
const queueFile = createTempQueueFile();
|
||||
const loggerState = createLogger();
|
||||
const queue = createAnilistUpdateQueue(queueFile, loggerState.logger);
|
||||
|
||||
const now = 1_700_000 * 1_000_000;
|
||||
queue.enqueue('k4', 'Dead Letter Demo', 4);
|
||||
for (let attempt = 1; attempt <= 8; attempt += 1) {
|
||||
queue.markFailure('k4', `fail-${attempt}`, now + attempt * 6 * 60 * 60 * 1000);
|
||||
}
|
||||
|
||||
queue.enqueue('k4', 'Dead Letter Demo', 4);
|
||||
|
||||
assert.deepEqual(queue.getSnapshot(Number.MAX_SAFE_INTEGER), {
|
||||
pending: 0,
|
||||
ready: 0,
|
||||
deadLetter: 1,
|
||||
});
|
||||
});
|
||||
|
||||
test('anilist update queue persists and reloads from disk', () => {
|
||||
const queueFile = createTempQueueFile();
|
||||
const loggerState = createLogger();
|
||||
|
||||
@@ -108,7 +108,8 @@ export function createAnilistUpdateQueue(
|
||||
|
||||
return {
|
||||
enqueue(key: string, title: string, episode: number, season: number | null = null): void {
|
||||
const existing = pending.find((item) => item.key === key);
|
||||
const existing =
|
||||
pending.find((item) => item.key === key) || deadLetter.find((item) => item.key === key);
|
||||
if (existing) {
|
||||
return;
|
||||
}
|
||||
@@ -147,6 +148,9 @@ export function createAnilistUpdateQueue(
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
if (item.attemptCount > 0 && item.nextAttemptAt > nowMs) {
|
||||
return;
|
||||
}
|
||||
item.attemptCount += 1;
|
||||
item.lastError = reason;
|
||||
if (item.attemptCount >= MAX_ATTEMPTS) {
|
||||
|
||||
Reference in New Issue
Block a user