Fehlgeschlagenen Codeberg-Sync zeitnah wiederholen statt bis zum naechsten Intervall zu warten
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 <noreply@anthropic.com>
This commit is contained in:
parent
3efa0ee423
commit
ad14447092
4 changed files with 135 additions and 2 deletions
12
API.md
12
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,
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
43
api/src/Sync/SyncSchedule.php
Normal file
43
api/src/Sync/SyncSchedule.php
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Monitor\Api\Sync;
|
||||
|
||||
/**
|
||||
* Bestimmt, wie lange der Sync-Dauerlaeufer (bin/sync-codeberg.php --watch) bis zum
|
||||
* naechsten Lauf wartet.
|
||||
*
|
||||
* Nach einem erfolgreichen Lauf ist das schlicht MONITOR_SYNC_INTERVAL. Nach einem
|
||||
* Fehlschlag wird der naechste Versuch dagegen deutlich vorgezogen: Ein per Webhook
|
||||
* angeforderter Sync ist zu diesem Zeitpunkt bereits quittiert (SyncTrigger::consume()
|
||||
* loescht die Anforderung vor dem Lauf), er wuerde sonst also erst nach vollen 600s
|
||||
* nachgeholt - genau die Verzoegerung, die der Webhook abschaffen soll.
|
||||
*
|
||||
* Der Abstand waechst exponentiell (5s, 10s, 20s, …), gedeckelt auf das regulaere
|
||||
* Intervall. Damit erholt sich ein kurzer Aussetzer (beobachtet: vereinzelte
|
||||
* curl-Timeouts nach 15s ohne Antwort) binnen Sekunden, waehrend eine laengere
|
||||
* Codeberg-Stoerung nicht in einen Dauerbeschuss laeuft.
|
||||
*/
|
||||
final class SyncSchedule
|
||||
{
|
||||
/** Ab hier waere 2^$failures groesser als jedes sinnvolle Intervall - schuetzt vor Overflow. */
|
||||
private const MAX_EXPONENT = 20;
|
||||
|
||||
public function __construct(
|
||||
private readonly int $intervalSeconds,
|
||||
private readonly int $retryBaseSeconds = 5,
|
||||
) {
|
||||
}
|
||||
|
||||
public function secondsUntilNextRun(int $consecutiveFailures): int
|
||||
{
|
||||
if ($consecutiveFailures < 1) {
|
||||
return $this->intervalSeconds;
|
||||
}
|
||||
|
||||
$exponent = min($consecutiveFailures - 1, self::MAX_EXPONENT);
|
||||
|
||||
return min($this->intervalSeconds, $this->retryBaseSeconds * (2 ** $exponent));
|
||||
}
|
||||
}
|
||||
65
api/tests/Unit/Sync/SyncScheduleTest.php
Normal file
65
api/tests/Unit/Sync/SyncScheduleTest.php
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Monitor\Api\Tests\Sync;
|
||||
|
||||
use Monitor\Api\Sync\SyncSchedule;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class SyncScheduleTest extends TestCase
|
||||
{
|
||||
public function testUsesTheRegularIntervalWhileEverythingWorks(): void
|
||||
{
|
||||
$schedule = new SyncSchedule(600);
|
||||
|
||||
self::assertSame(600, $schedule->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));
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue