feat: Webhook-Warteschlange in Datenbank persistieren
Build and Push Multi-Platform Images / build-and-push (push) Successful in 31s

Die Warteschlange liegt nicht mehr im Speicher, sondern in der neuen Tabelle
webhook_queue (Entity WebhookQueueItem, documentId als Primärschlüssel). So
überstehen ausstehende IDs einen Neustart und werden nach dem Boot
weiterverarbeitet.

- Neue Entity + Migration (CreateWebhookQueue); in data-source.ts/Barrel
  registriert. Dev: synchronize legt die Tabelle an; Prod: migrationsRun.
- enqueue nutzt INSERT IGNORE (atomar, race-sicher) -> Dedup über den PK.
- processQueue holt FIFO (createdAt ASC), entfernt die Zeile vor der
  Verarbeitung, arbeitet sequenziell (isProcessing-Guard).
- getStatus/queueSize lesen die DB-Anzahl. Controller awaitet enqueue.
- Unit-Test mit simuliertem FIFO-Repo: Dedup, Remove-on-start, Re-Enqueue,
  No-Parallel (deterministisch).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
bjoernpoettkerandClaude Opus 4.8 committed 2026-06-29 17:54:16 +02:00
1 parent 66a2cccd20
commit 9718d6888a
8 files changed
+182 -61

No files matched your search

@@ -26,6 +26,7 @@ import {
CorrespondentEmailMapping,
UserSettings,
LabelPrintJob,
WebhookQueueItem,
} from './entities';
// CLI-Kontext: .env laden (Laufzeit im Container liefert die Variablen via Docker,
@@ -57,6 +58,7 @@ export const entities = [
CorrespondentEmailMapping,
UserSettings,
LabelPrintJob,
WebhookQueueItem,
];
const isProduction = process.env.NODE_ENV === 'production';
@@ -21,3 +21,4 @@ export { InboxPostprocessingAction } from './inbox-postprocessing-action.entity'
export { CorrespondentEmailMapping } from './correspondent-email-mapping.entity';
export { UserSettings } from './user-settings.entity';
export { LabelPrintJob } from './label-print-job.entity';
export { WebhookQueueItem } from './webhook-queue-item.entity';
@@ -0,0 +1,26 @@
import {
Entity,
PrimaryColumn,
Column,
CreateDateColumn,
Index,
} from 'typeorm';
/**
* Persistente Warteschlange der vom Paperless-Webhook gemeldeten Dokument-IDs.
* `documentId` ist Primärschlüssel und erzwingt damit, dass jede ID nur einmal
* in der Warteschlange steht (Dedup auf DB-Ebene). Die Tabelle übersteht
* Neustarts; ausstehende IDs werden nach dem Boot weiterverarbeitet.
*/
@Entity('webhook_queue')
export class WebhookQueueItem {
@PrimaryColumn({ type: 'int' })
documentId!: number;
@Column({ type: 'varchar', length: 100, nullable: true })
action!: string | null;
@Index()
@CreateDateColumn()
createdAt!: Date;
}
@@ -0,0 +1,25 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
/**
* Legt die persistente Webhook-Warteschlange an (`webhook_queue`).
* `documentId` ist Primärschlüssel → jede Dokument-ID kommt nur einmal vor.
*/
export class CreateWebhookQueue1782700000000 implements MigrationInterface {
name = 'CreateWebhookQueue1782700000000';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
CREATE TABLE IF NOT EXISTS \`webhook_queue\` (
\`documentId\` int NOT NULL,
\`action\` varchar(100) NULL,
\`createdAt\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
PRIMARY KEY (\`documentId\`),
INDEX \`IDX_webhook_queue_createdAt\` (\`createdAt\`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP TABLE \`webhook_queue\``);
}
}