Merge pull request 'Fehlgeschlagenen Codeberg-Sync zeitnah wiederholen statt bis zum naechsten Intervall zu warten' (#25) from fds2610-bot/monitor:main into main
Reviewed-on: https://codeberg.org/fds2610/monitor/pulls/25
This commit is contained in:
commit
523e8a4dcf
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
|
erreichbar: …` bzw. `Codeberg-API-Fehler (HTTP 4xx)`) - der erste Anlaufpunkt, wenn Backlog
|
||||||
und Done leer bleiben.
|
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
|
### `POST /api/v1/webhooks/codeberg` — Sofort-Sync per Webhook
|
||||||
|
|
||||||
Damit Issue-Aenderungen nicht erst beim naechsten periodischen Lauf auf dem Board landen,
|
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\Repository\TicketRepository;
|
||||||
use Monitor\Api\Sync\CodebergSyncService;
|
use Monitor\Api\Sync\CodebergSyncService;
|
||||||
use Monitor\Api\Sync\CurlCodebergClient;
|
use Monitor\Api\Sync\CurlCodebergClient;
|
||||||
|
use Monitor\Api\Sync\SyncSchedule;
|
||||||
use Monitor\Api\Sync\SyncTrigger;
|
use Monitor\Api\Sync\SyncTrigger;
|
||||||
|
|
||||||
require dirname(__DIR__) . '/vendor/autoload.php';
|
require dirname(__DIR__) . '/vendor/autoload.php';
|
||||||
|
|
@ -32,23 +33,35 @@ if (!in_array('--watch', $argv, true)) {
|
||||||
|
|
||||||
$trigger = new SyncTrigger($config->syncTriggerPath());
|
$trigger = new SyncTrigger($config->syncTriggerPath());
|
||||||
$interval = $config->syncIntervalSeconds();
|
$interval = $config->syncIntervalSeconds();
|
||||||
|
$schedule = new SyncSchedule($interval);
|
||||||
|
|
||||||
fwrite(STDOUT, sprintf("[sync] Watch-Modus, periodischer Lauf alle %ds, Webhook-Trigger sofort.\n", $interval));
|
fwrite(STDOUT, sprintf("[sync] Watch-Modus, periodischer Lauf alle %ds, Webhook-Trigger sofort.\n", $interval));
|
||||||
|
|
||||||
$lastRun = 0;
|
$lastRun = 0;
|
||||||
|
$failures = 0;
|
||||||
while (true) {
|
while (true) {
|
||||||
$triggered = $trigger->consume();
|
$triggered = $trigger->consume();
|
||||||
$due = (time() - $lastRun) >= $interval;
|
$due = (time() - $lastRun) >= $schedule->secondsUntilNextRun($failures);
|
||||||
|
|
||||||
if ($triggered || $due) {
|
if ($triggered || $due) {
|
||||||
$lastRun = time();
|
$lastRun = time();
|
||||||
try {
|
try {
|
||||||
$service->sync();
|
$service->sync();
|
||||||
|
$failures = 0;
|
||||||
fwrite(STDOUT, sprintf("[sync] Lauf beendet (%s).\n", $triggered ? 'Webhook' : 'periodisch'));
|
fwrite(STDOUT, sprintf("[sync] Lauf beendet (%s).\n", $triggered ? 'Webhook' : 'periodisch'));
|
||||||
} catch (Throwable $e) {
|
} catch (Throwable $e) {
|
||||||
// Ein fehlgeschlagener Lauf (Codeberg nicht erreichbar, Rate-Limit) darf die
|
// Ein fehlgeschlagener Lauf (Codeberg nicht erreichbar, Rate-Limit) darf die
|
||||||
// Schleife nicht beenden, sonst landet der Container in einer Restart-Schleife.
|
// 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