162 lines
6.1 KiB
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');
|
|
});
|