CluPilotCloud/tests/Feature/WatchdogLockContentionTest.php

162 lines
6.1 KiB
PHP

<?php
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Process;
/**
* Der Wächter und der Update-Agent teilen sich eine Sperre — aber nicht die
* Absicht.
*
* Beide haben einen minütlichen Zeitgeber, und beide nahmen `.agent.lock` in
* ihren ersten Zeilen, bevor sie überhaupt wussten, ob sie etwas tun würden.
* Der Wächter tut an fast jedem Tag nichts: seine vier Prüfungen sind allesamt
* lesend. Er hielt die Sperre trotzdem — und der Agent, der eine Sekunde später
* dran war, kam nicht an die Arbeit und schrieb einen Übersprung-Vermerk.
*
* Weil der Agent der ist, der an die Konsole berichtet, stand dort eine
* Dauerstörung, während beide Dienste taten, was sie sollten.
*
* Hier laufen die ECHTEN Skripte gegeneinander, nicht eine Nachbildung ihrer
* Logik — ein Test, der die Sperrenlogik nachrechnet, statt zwei Prozesse
* wirklich um dieselbe Datei streiten zu lassen, prüft genau das nicht, worum
* es geht. `docker` liegt dafür als Attrappe im PATH, die den gesunden Stapel
* meldet und pro Aufruf eine Sekunde braucht: das ist das Zeitfenster, in dem
* der Agent starten muss.
*/
function deployDir(): string
{
$dir = storage_path('app/deploy');
File::ensureDirectoryExists($dir);
return $dir;
}
/**
* Legt die docker-Attrappe an und gibt ihr Verzeichnis zurück.
*
* @param string $running Was `ps --status running` meldet (Zeilenumbrüche)
*/
function dockerStub(string $running = "app\nredis"): string
{
$stub = deployDir().'/stub';
File::ensureDirectoryExists($stub);
// Jeder Aufruf wird protokolliert: nur so lässt sich hinterher belegen,
// dass der Wächter NICHT eingegriffen hat, statt es zu vermuten.
File::put($stub.'/docker', <<<SH
#!/bin/sh
echo "\$*" >> "\$STUB_LOG"
touch "\$STUB_STARTED"
sleep "\${STUB_SLEEP:-1}"
case "\$*" in
*config*) printf '%s\\n' app redis ;;
*"ps "*) printf '%s\\n' $running ;;
*getent*) exit "\${STUB_REDIS_RC:-0}" ;;
*storage/framework/down*) exit 1 ;;
*" up "*) touch "\$STUB_UP_CALLED" ;;
esac
exit 0
SH);
// Ein `git`, das sofort scheitert. Der Agent fängt das ab; dieser Test hat
// keinen Grund, die Gegenstelle abzurufen.
File::put($stub.'/git', "#!/bin/sh\nexit 1\n");
Process::run("chmod +x {$stub}/docker {$stub}/git");
return $stub;
}
afterEach(function () {
File::deleteDirectory(storage_path('app/deploy'));
});
it('lets the agent work while the watchdog is only looking', function () {
// Der gemeldete Fall: nichts ist kaputt, der Wächter sieht nur nach — und
// der Agent kommt trotzdem nicht an die Arbeit.
$dir = deployDir();
$stub = dockerStub();
$result = Process::path(base_path())
->timeout(90)
->env([
'PATH' => $stub.':'.env('PATH', '/usr/local/bin:/usr/bin:/bin'),
'STUB_LOG' => $dir.'/.stub-log',
'STUB_STARTED' => $dir.'/.stub-started',
'STUB_UP_CALLED' => $dir.'/.stub-up-called',
])
->run(<<<BASH
set -e
bash deploy/watchdog.sh >/dev/null 2>&1 &
waechter=\$!
# Nachweislich MITTEN im Lauf des Wächters, nicht davor und nicht
# danach: erst wenn die Attrappe angefasst wurde, ist er drin.
until [ -f "\$STUB_STARTED" ]; do sleep 0.05; done
bash deploy/update-agent.sh >/dev/null 2>&1 || true
kill \$waechter 2>/dev/null || true
wait \$waechter 2>/dev/null || true
BASH);
expect($result->successful())->toBeTrue($result->errorOutput());
$alive = json_decode(File::get($dir.'/agent-alive.json'), true);
expect($alive['state'])->toBe('running')
->and($alive['skips'])->toBe(0);
});
it('keeps the watchdog away while an update holds the lock', function () {
// Die andere Hälfte, und die teurere: der Wächter darf NICHT eingreifen,
// während ein Update läuft. Er sieht hier einen fehlenden Dienst — also
// etwas, das er sonst sofort richten würde — und muss die Hände trotzdem
// stillhalten, weil die Sperre gehalten wird.
$dir = deployDir();
$stub = dockerStub(running: 'redis');
$result = Process::path(base_path())
->timeout(90)
->env([
'PATH' => $stub.':'.env('PATH', '/usr/local/bin:/usr/bin:/bin'),
'STUB_LOG' => $dir.'/.stub-log',
'STUB_STARTED' => $dir.'/.stub-started',
'STUB_UP_CALLED' => $dir.'/.stub-up-called',
])
->run(<<<BASH
set -e
flock storage/app/deploy/.agent.lock -c 'touch storage/app/deploy/.held; sleep 20' &
halter=\$!
until [ -f storage/app/deploy/.held ]; do sleep 0.05; done
bash deploy/watchdog.sh > storage/app/deploy/.watchdog-out 2>&1 || true
kill \$halter 2>/dev/null || true
BASH);
expect($result->successful())->toBeTrue($result->errorOutput());
expect(File::exists($dir.'/.stub-up-called'))
->toBeFalse('Der Waechter hat eingegriffen, obwohl die Sperre gehalten wurde.');
});
it('says in the journal that a run was skipped', function () {
// Ein übersprungener Lauf sah im Journal aus wie ein erfolgreicher — der
// Agent beendet sich sauber, systemd meldet Starting → Deactivated. Das
// hat die Fehlersuche zweimal in die falsche Richtung geschickt.
$dir = deployDir();
$result = Process::path(base_path())
->timeout(60)
->run(<<<'BASH'
flock storage/app/deploy/.agent.lock -c 'touch storage/app/deploy/.held; sleep 20' &
halter=$!
until [ -f storage/app/deploy/.held ]; do sleep 0.05; done
bash deploy/update-agent.sh 2>&1
kill $halter 2>/dev/null || true
BASH);
// Die Zeile nennt den Halter mit: der Waechter fuer einen Lauf ist
// Betrieb, derselbe Halter seit vierzig Laeufen ist es nicht — und genau
// diese Unterscheidung war im Journal bisher nicht zu treffen.
expect($result->output())
->toContain('Uebersprungen')
->toContain('die Sperre haelt');
});