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)); + } +}