press-releases/jobs: Scheduler respektiert manuelle Prüfung und kommt an Blockaden vorbei, Newsletter-Versand per Schalter gesperrt, kurze Scheduler-Sperren (Umsetzungsaudit 29.09.: K-01, K-02, K-05, K-06, K-09, E9, OPT-8, OPT-9; 02.10.: O06)
- K-01: publish-scheduled filtert manual_review_required_at; publish() prüft für ki/scheduler unter der Sperre (ManualReviewRequiredException) - K-02: Kontingent- und Bildsperre sind fachliche Ausgänge (Exit 0, einmaliger Verlaufseintrag); KI-Job ohne Retry bei Bildsperre - O06: Kandidaten per Cursor (scheduled_at, id), Limit zählt bearbeitete Meldungen, Blockaden nur ein Scan-Budget - K-05: KI-Job lädt vor dem Routing neu, überholter Status ohne Retry; backoff - K-06: Admin-Aktionen mit Permission-Prüfung und Fehlermeldung statt 500 - K-09: withoutOverlapping() mit kurzer Sperrdauer, purge-events 03:50, neuer Wochenlauf press-release-images:sweep-orphan-originals (So 04:45) - E9: NEWSLETTER_SENDING_ENABLED (Standard aus) sperrt Start, Resume und Job - OPT-9: ExpireGrandfathering prüft die Queue-Voraussetzung aus E18 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
d6e49c11c3
commit
c82f279fef
20 changed files with 927 additions and 89 deletions
|
|
@ -18,6 +18,7 @@ use App\Console\Commands\PurgePressReleaseEvents;
|
|||
use App\Console\Commands\PurgeRedactionSnapshots;
|
||||
use App\Console\Commands\RefreshMarketData;
|
||||
use App\Console\Commands\ResetMonthlyPressReleaseQuota;
|
||||
use App\Console\Commands\SweepOrphanPressReleaseOriginals;
|
||||
use App\Support\OperationalAlert;
|
||||
use Illuminate\Console\Scheduling\Event;
|
||||
use Illuminate\Foundation\Inspiring;
|
||||
|
|
@ -39,6 +40,12 @@ $alertOnFailure = fn (string $task) => function (Event $event) use ($task): void
|
|||
]);
|
||||
};
|
||||
|
||||
// Sperren von withoutOverlapping() (Audit K-09): Ohne Angabe hält Laravel
|
||||
// sie 24 h. Stürzt ein Lauf ab (OOM, Deploy, Container-Neustart), würde der
|
||||
// Task bis zu einen Tag still übersprungen. Jede Sperre endet deshalb kurz
|
||||
// nach der erwarteten Höchstlaufzeit: häufige Tasks nach ihrem Intervall,
|
||||
// nächtliche nach 30–180 Minuten je nach Datenmenge.
|
||||
|
||||
// ========================================
|
||||
// Tägliche Bereinigungen
|
||||
// ========================================
|
||||
|
|
@ -48,7 +55,7 @@ $alertOnFailure = fn (string $task) => function (Event $event) use ($task): void
|
|||
// beendet sich der Befehl sofort.
|
||||
Schedule::command(FetchMailBounces::class)
|
||||
->everyTenMinutes()
|
||||
->withoutOverlapping()
|
||||
->withoutOverlapping(10)
|
||||
->runInBackground()
|
||||
->onFailure($alertOnFailure('mail:fetch-bounces'));
|
||||
|
||||
|
|
@ -57,21 +64,21 @@ Schedule::command(FetchMailBounces::class)
|
|||
// Befund darf nicht unbegrenzt als Domainsperre stehen bleiben.
|
||||
Schedule::command(CheckMailDomains::class, ['--stale', '--limit=500'])
|
||||
->dailyAt('04:20')
|
||||
->withoutOverlapping()
|
||||
->withoutOverlapping(60)
|
||||
->runInBackground()
|
||||
->onFailure($alertOnFailure('mail:check-domains'));
|
||||
|
||||
// Magic-Links: Abgelaufene / verbrauchte Tokens entfernen (täglich 03:00)
|
||||
Schedule::command(PurgeMagicLinks::class, ['--days=30'])
|
||||
->dailyAt('03:00')
|
||||
->withoutOverlapping()
|
||||
->withoutOverlapping(30)
|
||||
->runInBackground()
|
||||
->onFailure($alertOnFailure('magic-links:purge'));
|
||||
|
||||
// Meldeverfahren: Schwärzungs-Snapshots nach Löschfrist entfernen (täglich 03:10)
|
||||
Schedule::command(PurgeRedactionSnapshots::class)
|
||||
->dailyAt('03:10')
|
||||
->withoutOverlapping()
|
||||
->withoutOverlapping(30)
|
||||
->runInBackground()
|
||||
->onFailure($alertOnFailure('snapshots:purge'));
|
||||
|
||||
|
|
@ -79,13 +86,19 @@ Schedule::command(PurgeRedactionSnapshots::class)
|
|||
// Tombstone-Stand exportieren (03:30), damit er einen Restore überlebt.
|
||||
Schedule::command(ArchivePurge::class)
|
||||
->dailyAt('03:20')
|
||||
->withoutOverlapping()
|
||||
->withoutOverlapping(120)
|
||||
->runInBackground()
|
||||
->onFailure($alertOnFailure('archive:purge'));
|
||||
|
||||
// Bewusst eigener Termin statt ->then() an archive:purge (OPT-6, K-09):
|
||||
// Als Anhängsel liefe der Export nach einem gescheiterten Purge gar nicht
|
||||
// (onSuccess) bzw. ohne eigenen Failure-Alarm (then). So bleibt der Export
|
||||
// auch dann aktuell und meldet sich selbst. Die Sperren enden nach 2 h bzw.
|
||||
// 1 h; ein Purge, der länger als 10 Minuten läuft, überschneidet sich mit dem
|
||||
// Export — der Export liest nur und nimmt den nächsten Stand am Folgetag mit.
|
||||
Schedule::command(ArchiveExportTombstones::class)
|
||||
->dailyAt('03:30')
|
||||
->withoutOverlapping()
|
||||
->withoutOverlapping(60)
|
||||
->runInBackground()
|
||||
->onFailure($alertOnFailure('archive:export-tombstones'));
|
||||
|
||||
|
|
@ -93,13 +106,13 @@ Schedule::command(ArchiveExportTombstones::class)
|
|||
// nach 24 h Karenz entfernen (03:40) — Umsetzungsaudit 25.09.2026, N11.
|
||||
Schedule::command(ArchiveRetryPending::class)
|
||||
->dailyAt('03:35')
|
||||
->withoutOverlapping()
|
||||
->withoutOverlapping(60)
|
||||
->runInBackground()
|
||||
->onFailure($alertOnFailure('archive:retry-pending'));
|
||||
|
||||
Schedule::command(ArchiveSweepOrphans::class)
|
||||
->dailyAt('03:40')
|
||||
->withoutOverlapping()
|
||||
->withoutOverlapping(120)
|
||||
->runInBackground()
|
||||
->onFailure($alertOnFailure('archive:sweep-orphans'));
|
||||
|
||||
|
|
@ -108,10 +121,21 @@ Schedule::command(PurgeExpiredPressReleaseDrafts::class, ['--days=180'])
|
|||
->weekly()
|
||||
->sundays()
|
||||
->at('04:00')
|
||||
->withoutOverlapping()
|
||||
->withoutOverlapping(180)
|
||||
->runInBackground()
|
||||
->onFailure($alertOnFailure('press-releases:purge-drafts'));
|
||||
|
||||
// Beweisoriginale ohne Bildzeile (Umsetzungsaudit 29.09.2026, B-11): Reste
|
||||
// eines hart abgebrochenen Uploads. 48 h Karenz schützt laufende Uploads;
|
||||
// während eines Restore-Abgleichs (Archiv-Sweep pausiert) löscht er nichts.
|
||||
Schedule::command(SweepOrphanPressReleaseOriginals::class, ['--execute', '--hours=48'])
|
||||
->weekly()
|
||||
->sundays()
|
||||
->at('04:45')
|
||||
->withoutOverlapping(60)
|
||||
->runInBackground()
|
||||
->onFailure($alertOnFailure('press-release-images:sweep-orphan-originals'));
|
||||
|
||||
// ========================================
|
||||
// Geplante PM-Veröffentlichung
|
||||
// ========================================
|
||||
|
|
@ -120,7 +144,7 @@ Schedule::command(PurgeExpiredPressReleaseDrafts::class, ['--days=180'])
|
|||
// Läuft alle 5 Min — passt zum FormRule "scheduled_at min. 5 Min in Zukunft".
|
||||
Schedule::command(PublishScheduledPressReleases::class)
|
||||
->everyFiveMinutes()
|
||||
->withoutOverlapping()
|
||||
->withoutOverlapping(10)
|
||||
->runInBackground()
|
||||
->onFailure($alertOnFailure('press-releases:publish-scheduled'));
|
||||
|
||||
|
|
@ -133,17 +157,18 @@ Schedule::command(PublishScheduledPressReleases::class)
|
|||
// darf keine Web-Worker blockieren (siehe AggregatePressReleaseViews).
|
||||
Schedule::command(AggregatePressReleaseViews::class)
|
||||
->everyMinute()
|
||||
->withoutOverlapping()
|
||||
->withoutOverlapping(15)
|
||||
->runInBackground()
|
||||
->onFailure($alertOnFailure('press-releases:aggregate-views'));
|
||||
|
||||
// Ereignis-Log auf den Aufbewahrungszeitraum begrenzen (täglich 03:20).
|
||||
// Ereignis-Log auf den Aufbewahrungszeitraum begrenzen (täglich 03:50 —
|
||||
// nicht mehr zeitgleich mit archive:purge um 03:20, Audit K-09).
|
||||
// Die Tabelle nimmt jeden Seitenaufruf auf und wuchs als einzige Log-Tabelle
|
||||
// des Systems unbegrenzt. Gelöscht wird nur unterhalb der Wasserstandsmarke —
|
||||
// noch nicht in `hits` verbuchte Aufrufe bleiben unangetastet.
|
||||
Schedule::command(PurgePressReleaseEvents::class)
|
||||
->dailyAt('03:20')
|
||||
->withoutOverlapping()
|
||||
->dailyAt('03:50')
|
||||
->withoutOverlapping(120)
|
||||
->runInBackground()
|
||||
->onFailure($alertOnFailure('press-releases:purge-events'));
|
||||
|
||||
|
|
@ -151,7 +176,7 @@ Schedule::command(PurgePressReleaseEvents::class)
|
|||
// Ohne GSC_ENABLED/Schlüsseldatei beendet sich der Befehl sofort.
|
||||
Schedule::command(CheckIndexStatus::class)
|
||||
->dailyAt('05:10')
|
||||
->withoutOverlapping()
|
||||
->withoutOverlapping(60)
|
||||
->runInBackground()
|
||||
->onFailure($alertOnFailure('press-releases:check-index-status'));
|
||||
|
||||
|
|
@ -159,7 +184,7 @@ Schedule::command(CheckIndexStatus::class)
|
|||
// Frist zusammen mit der Tabelle festgelegt (395 Tage), siehe config/conversion.php.
|
||||
Schedule::command(PurgeConversionEvents::class)
|
||||
->dailyAt('03:25')
|
||||
->withoutOverlapping()
|
||||
->withoutOverlapping(60)
|
||||
->runInBackground()
|
||||
->onFailure($alertOnFailure('conversion:purge-events'));
|
||||
|
||||
|
|
@ -172,7 +197,7 @@ Schedule::command(PurgeConversionEvents::class)
|
|||
// abgerechnet wird.
|
||||
Schedule::command(ExpireGrandfathering::class)
|
||||
->dailyAt('04:15')
|
||||
->withoutOverlapping()
|
||||
->withoutOverlapping(30)
|
||||
->runInBackground()
|
||||
->onFailure($alertOnFailure('billing:expire-grandfathering'));
|
||||
|
||||
|
|
@ -181,7 +206,7 @@ Schedule::command(ExpireGrandfathering::class)
|
|||
// Stripe (STR-Kreis) und werden hier nicht angefasst.
|
||||
Schedule::command(GenerateManualInvoices::class)
|
||||
->dailyAt('04:30')
|
||||
->withoutOverlapping()
|
||||
->withoutOverlapping(60)
|
||||
->runInBackground()
|
||||
->onFailure($alertOnFailure('billing:generate-manual-invoices'));
|
||||
|
||||
|
|
@ -192,7 +217,7 @@ Schedule::command(GenerateManualInvoices::class)
|
|||
// Monatlicher Reset des verbrauchten PM-Kontingents (am 1. um 00:05).
|
||||
Schedule::command(ResetMonthlyPressReleaseQuota::class)
|
||||
->monthlyOn(1, '00:05')
|
||||
->withoutOverlapping()
|
||||
->withoutOverlapping(30)
|
||||
->runInBackground()
|
||||
->onFailure($alertOnFailure('press-releases:reset-monthly-quota'));
|
||||
|
||||
|
|
@ -204,6 +229,6 @@ Schedule::command(ResetMonthlyPressReleaseQuota::class)
|
|||
// nur aus dem Cache — ein API-Ausfall leert die Anzeige nicht sofort.
|
||||
Schedule::command(RefreshMarketData::class)
|
||||
->everyFifteenMinutes()
|
||||
->withoutOverlapping()
|
||||
->withoutOverlapping(15)
|
||||
->runInBackground()
|
||||
->onFailure($alertOnFailure('market:refresh'));
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue