From ad144470922afb815e055bb7ef20473a15c3f10c Mon Sep 17 00:00:00 2001 From: fds2610-bot Date: Wed, 29 Jul 2026 21:29:20 +0200 Subject: [PATCH] Fehlgeschlagenen Codeberg-Sync zeitnah wiederholen statt bis zum naechsten Intervall zu warten MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Auf der Produktionsmaschine traten vereinzelt Timeouts nach codeberg.org auf ("Operation timed out after 15003 milliseconds with 0 bytes received"), waehrend die Verbindung sonst in ~0,1s antwortet. Bisher fiel der Dauerlaeufer nach so einem Fehlschlag in den regulaeren Takt zurueck. Weil SyncTrigger::consume() die Webhook-Anforderung schon vor dem Lauf quittiert, war eine per Webhook angeforderte Aktualisierung damit verloren und kam erst bis zu MONITOR_SYNC_INTERVAL Sekunden spaeter - genau die Verzoegerung, die der Webhook abschaffen sollte. Neu wird der naechste Versuch nach einem Fehlschlag vorgezogen: 5s, 10s, 20s, … gedeckelt auf das regulaere Intervall (Sync\SyncSchedule). Ein kurzer Aussetzer ist damit binnen Sekunden ueberstanden, eine laengere Codeberg-Stoerung laeuft trotzdem nicht in einen Dauerbeschuss. Nach einem erfolgreichen Lauf gilt sofort wieder das regulaere Intervall. Die Wartezeit-Berechnung liegt in einer eigenen Klasse, weil die Schleife in bin/sync-codeberg.php selbst nicht testbar ist. Co-Authored-By: Claude Opus 5 --- API.md | 12 +++++ api/bin/sync-codeberg.php | 17 ++++++- api/src/Sync/SyncSchedule.php | 43 ++++++++++++++++ api/tests/Unit/Sync/SyncScheduleTest.php | 65 ++++++++++++++++++++++++ 4 files changed, 135 insertions(+), 2 deletions(-) create mode 100644 api/src/Sync/SyncSchedule.php create mode 100644 api/tests/Unit/Sync/SyncScheduleTest.php diff --git a/API.md b/API.md index a963b1a..b89663a 100644 --- a/API.md +++ b/API.md @@ -295,6 +295,18 @@ Beim manuellen Aufruf werden Fehler direkt auf der Konsole sichtbar (`Codeberg-A erreichbar: …` bzw. `Codeberg-API-Fehler (HTTP 4xx)`) - der erste Anlaufpunkt, wenn Backlog und Done leer bleiben. +Schlaegt ein Lauf fehl (vereinzelte Timeouts nach codeberg.org kommen vor), wird der +naechste Versuch **vorgezogen** statt bis zum regulaeren Intervall zu warten: 5s, 10s, 20s, +… bis hoechstens `MONITOR_SYNC_INTERVAL` (`Sync\SyncSchedule`). Sonst ginge eine per Webhook +angeforderte Aktualisierung verloren - die Anforderung wird vor dem Lauf quittiert und +waere damit erst nach vollen `MONITOR_SYNC_INTERVAL` Sekunden nachgeholt. Nach einem +erfolgreichen Lauf gilt sofort wieder das regulaere Intervall. Im Log sieht das so aus: + +``` +[sync] Lauf fehlgeschlagen (1x in Folge, naechster Versuch in 5s): Codeberg-API nicht erreichbar: … +[sync] Lauf fehlgeschlagen (2x in Folge, naechster Versuch in 10s): … +``` + ### `POST /api/v1/webhooks/codeberg` — Sofort-Sync per Webhook Damit Issue-Aenderungen nicht erst beim naechsten periodischen Lauf auf dem Board landen, diff --git a/api/bin/sync-codeberg.php b/api/bin/sync-codeberg.php index cf8f871..0c72cfa 100755 --- a/api/bin/sync-codeberg.php +++ b/api/bin/sync-codeberg.php @@ -8,6 +8,7 @@ use Monitor\Api\Db\Database; use Monitor\Api\Repository\TicketRepository; use Monitor\Api\Sync\CodebergSyncService; use Monitor\Api\Sync\CurlCodebergClient; +use Monitor\Api\Sync\SyncSchedule; use Monitor\Api\Sync\SyncTrigger; require dirname(__DIR__) . '/vendor/autoload.php'; @@ -32,23 +33,35 @@ if (!in_array('--watch', $argv, true)) { $trigger = new SyncTrigger($config->syncTriggerPath()); $interval = $config->syncIntervalSeconds(); +$schedule = new SyncSchedule($interval); fwrite(STDOUT, sprintf("[sync] Watch-Modus, periodischer Lauf alle %ds, Webhook-Trigger sofort.\n", $interval)); $lastRun = 0; +$failures = 0; while (true) { $triggered = $trigger->consume(); - $due = (time() - $lastRun) >= $interval; + $due = (time() - $lastRun) >= $schedule->secondsUntilNextRun($failures); if ($triggered || $due) { $lastRun = time(); try { $service->sync(); + $failures = 0; fwrite(STDOUT, sprintf("[sync] Lauf beendet (%s).\n", $triggered ? 'Webhook' : 'periodisch')); } catch (Throwable $e) { // Ein fehlgeschlagener Lauf (Codeberg nicht erreichbar, Rate-Limit) darf die // Schleife nicht beenden, sonst landet der Container in einer Restart-Schleife. - fwrite(STDERR, sprintf("[sync] Lauf fehlgeschlagen: %s\n", $e->getMessage())); + // Stattdessen wird der naechste Versuch vorgezogen (siehe SyncSchedule) - eine + // per Webhook angeforderte Aktualisierung ist sonst verloren, weil die + // Anforderung vor dem Lauf quittiert wurde. + $failures++; + fwrite(STDERR, sprintf( + "[sync] Lauf fehlgeschlagen (%dx in Folge, naechster Versuch in %ds): %s\n", + $failures, + $schedule->secondsUntilNextRun($failures), + $e->getMessage() + )); } } diff --git a/api/src/Sync/SyncSchedule.php b/api/src/Sync/SyncSchedule.php new file mode 100644 index 0000000..8bc82b8 --- /dev/null +++ b/api/src/Sync/SyncSchedule.php @@ -0,0 +1,43 @@ +intervalSeconds; + } + + $exponent = min($consecutiveFailures - 1, self::MAX_EXPONENT); + + return min($this->intervalSeconds, $this->retryBaseSeconds * (2 ** $exponent)); + } +} diff --git a/api/tests/Unit/Sync/SyncScheduleTest.php b/api/tests/Unit/Sync/SyncScheduleTest.php new file mode 100644 index 0000000..515f904 --- /dev/null +++ b/api/tests/Unit/Sync/SyncScheduleTest.php @@ -0,0 +1,65 @@ +secondsUntilNextRun(0)); + } + + /** + * Der Kern des Fixes: Nach einem Fehlschlag darf nicht bis zum naechsten regulaeren + * Lauf gewartet werden - die per Webhook angeforderte Aktualisierung waere sonst bis + * zu MONITOR_SYNC_INTERVAL Sekunden verzoegert. + */ + public function testRetriesQuicklyAfterAFailure(): void + { + $schedule = new SyncSchedule(600, retryBaseSeconds: 5); + + self::assertSame(5, $schedule->secondsUntilNextRun(1)); + } + + public function testBacksOffExponentiallyWhileFailuresContinue(): void + { + $schedule = new SyncSchedule(600, retryBaseSeconds: 5); + + self::assertSame(10, $schedule->secondsUntilNextRun(2)); + self::assertSame(20, $schedule->secondsUntilNextRun(3)); + self::assertSame(40, $schedule->secondsUntilNextRun(4)); + } + + /** Eine laengere Codeberg-Stoerung darf nicht in einen Dauerbeschuss laufen. */ + public function testNeverWaitsLongerThanTheRegularInterval(): void + { + $schedule = new SyncSchedule(600, retryBaseSeconds: 5); + + self::assertSame(600, $schedule->secondsUntilNextRun(8)); + self::assertSame(600, $schedule->secondsUntilNextRun(50)); + } + + /** Sehr viele Fehlschlaege duerfen 2^n nicht ueberlaufen lassen. */ + public function testSurvivesAnAbsurdNumberOfFailures(): void + { + $schedule = new SyncSchedule(600, retryBaseSeconds: 5); + + self::assertSame(600, $schedule->secondsUntilNextRun(PHP_INT_MAX)); + } + + /** Bei sehr kurzem Intervall bleibt dieses die Untergrenze, nicht die Retry-Basis. */ + public function testShortIntervalIsNotStretchedByTheRetryBase(): void + { + $schedule = new SyncSchedule(3, retryBaseSeconds: 5); + + self::assertSame(3, $schedule->secondsUntilNextRun(0)); + self::assertSame(3, $schedule->secondsUntilNextRun(1)); + } +}