From 095cc4bb021096dc9264eccfd8d2ce91e4f7d1d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20P=C3=B6ttker?= Date: Wed, 22 Jul 2026 12:07:51 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20Warteschlange=20blieb=20stehen=20?= =?UTF-8?q?=E2=80=93=20Altersvergleich=20in=20die=20DB=20verlagert?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Der Mindest-Wartezeit-Check verglich Date.now() (Node-Uhr) mit dem aus der DB gelesenen createdAt. Ohne gesetzte timezone (mysql2-Default 'local') wird createdAt bei abweichender DB-Zeitzone als zukünftig interpretiert → Alter negativ → jeder Eintrag galt als "zu jung" und wurde nie verarbeitet. Der Vergleich läuft jetzt in der DB (createdAt <= NOW(6) - INTERVAL 5s via TypeORM Raw), unabhängig von Zeitzone/Uhr des Node-Prozesses. Bereits wartende Einträge werden dadurch nach dem Deploy sofort abgearbeitet. Test-Mock bildet den DB-Alters-Filter nach. Co-Authored-By: Claude Opus 4.8 --- .../src/webhook/webhook-queue.service.spec.ts | 13 ++++++----- .../src/webhook/webhook-queue.service.ts | 23 ++++++++++++------- 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/paperless-backend/src/webhook/webhook-queue.service.spec.ts b/paperless-backend/src/webhook/webhook-queue.service.spec.ts index bec68d8..29e75a0 100644 --- a/paperless-backend/src/webhook/webhook-queue.service.spec.ts +++ b/paperless-backend/src/webhook/webhook-queue.service.spec.ts @@ -41,16 +41,17 @@ function createQueueRepoMock() { }), })), find: jest.fn(() => { - const sorted = [...store].sort( - (a, b) => a.createdAt.getTime() - b.createdAt.getTime(), - ); + // Simuliert den DB-seitigen Alters-Filter (createdAt <= NOW() - 5000 ms). + const eligible = [...store] + .filter((s) => Date.now() - s.createdAt.getTime() >= 5000) + .sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime()); return Promise.resolve( - sorted.length + eligible.length ? [ { - documentId: sorted[0].documentId, + documentId: eligible[0].documentId, action: null, - createdAt: sorted[0].createdAt, + createdAt: eligible[0].createdAt, }, ] : [], diff --git a/paperless-backend/src/webhook/webhook-queue.service.ts b/paperless-backend/src/webhook/webhook-queue.service.ts index 1a68268..fd44739 100644 --- a/paperless-backend/src/webhook/webhook-queue.service.ts +++ b/paperless-backend/src/webhook/webhook-queue.service.ts @@ -1,7 +1,7 @@ import { Injectable, Logger } from '@nestjs/common'; import { Interval } from '@nestjs/schedule'; import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; +import { Raw, Repository } from 'typeorm'; import { PaperlessProcessorService } from '../paperless/paperless-processor.service'; import { Setting } from '../database/entities/setting.entity'; import { WebhookQueueItem } from '../database/entities/webhook-queue-item.entity'; @@ -99,18 +99,25 @@ export class WebhookQueueService { if (this.isProcessing) return; this.isProcessing = true; try { - // Solange Einträge vorhanden sind, einzeln und sequenziell abarbeiten. + // Solange (alte genug) Einträge vorhanden sind, sequenziell abarbeiten. for (;;) { - // Ältesten Eintrag (FIFO) holen ... + // Ältesten Eintrag (FIFO) holen, der die Mindest-Wartezeit erfüllt. + // Der Altersvergleich läuft bewusst in der DB (NOW() vs. createdAt) und + // ist damit unabhängig von Zeitzone/Uhr des Node-Prozesses – ein Node/DB- + // Zeitversatz würde sonst den Vergleich verfälschen (Einträge nie "alt + // genug"). Nur ein numerisches Konstantenliteral wird interpoliert. const [next] = await this.queueRepo.find({ + where: { + createdAt: Raw( + (alias) => + `${alias} <= (NOW(6) - INTERVAL ${MIN_QUEUE_AGE_MS * 1000} MICROSECOND)`, + ), + }, order: { createdAt: 'ASC' }, take: 1, }); - if (!next) break; - // Mindest-Wartezeit einhalten: ist der älteste Eintrag noch zu jung, - // sind es (FIFO) alle → diesen Tick beenden, beim nächsten erneut prüfen. - if (Date.now() - next.createdAt.getTime() < MIN_QUEUE_AGE_MS) break; - // ... und SOFORT (vor Verarbeitungsstart) aus der Tabelle entfernen. + if (!next) break; // Warteschlange leer oder noch nichts alt genug + // SOFORT (vor Verarbeitungsstart) aus der Tabelle entfernen. await this.queueRepo.delete({ documentId: next.documentId }); await this.handleDocument(next.documentId); }