Horizon fuer Live vorbereitet
- Supervisor bedient alle Queues der Jobs (high-priority, default, dhl-*), production mit 3 statt 10 Prozessen - Redis-Queue mit after_commit: der Stripe-Paid-Pfad plant den Rechnungsnummer-Sync innerhalb der Transaktion ein - /horizon nur fuer System-Admins (admin >= 4) - horizon:snapshot alle 5 Minuten, queue:prune-failed nach 30 Tagen - deploy.sh: QUEUE_WORKER (horizon | none), Anleitung Abschnitt F Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
e1c6178bc7
commit
21daab259f
7 changed files with 95 additions and 25 deletions
|
|
@ -98,6 +98,10 @@ class Kernel extends ConsoleKernel
|
|||
// Cleanup old log files weekly (keeps logs for 30 days)
|
||||
$schedule->command('logs:cleanup --days=30')->weekly()->sundays()->at('05:00');
|
||||
|
||||
// Horizon: Kennzahlen fuer das Dashboard, fehlgeschlagene Jobs nach 30 Tagen loeschen
|
||||
$schedule->command('horizon:snapshot')->everyFiveMinutes();
|
||||
$schedule->command('queue:prune-failed --hours=720')->dailyAt('05:15');
|
||||
|
||||
// DHL Tracking Update: Stündlich mit status-basierten Intervallen und Batch-API
|
||||
$schedule->command('dhl:update-tracking --days=30 --send-emails')
|
||||
->hourly()
|
||||
|
|
|
|||
|
|
@ -27,10 +27,6 @@ class HorizonServiceProvider extends HorizonApplicationServiceProvider
|
|||
*/
|
||||
protected function gate(): void
|
||||
{
|
||||
Gate::define('viewHorizon', function ($user = null) {
|
||||
return in_array(optional($user)->email, [
|
||||
//
|
||||
]);
|
||||
});
|
||||
Gate::define('viewHorizon', fn ($user = null): bool => (bool) $user?->isSySAdmin());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -182,7 +182,7 @@ return [
|
|||
'defaults' => [
|
||||
'supervisor-1' => [
|
||||
'connection' => 'redis',
|
||||
'queue' => ['default'],
|
||||
'queue' => ['high-priority', 'default', 'dhl-shipments', 'dhl-returns', 'dhl-cancellations', 'dhl-tracking'],
|
||||
'balance' => 'auto',
|
||||
'autoScalingStrategy' => 'time',
|
||||
'maxProcesses' => 1,
|
||||
|
|
@ -198,7 +198,7 @@ return [
|
|||
'environments' => [
|
||||
'production' => [
|
||||
'supervisor-1' => [
|
||||
'maxProcesses' => 10,
|
||||
'maxProcesses' => 3,
|
||||
'balanceMaxShift' => 1,
|
||||
'balanceCooldown' => 3,
|
||||
],
|
||||
|
|
|
|||
|
|
@ -63,6 +63,8 @@ return [
|
|||
'queue' => 'default',
|
||||
'retry_after' => 90,
|
||||
'block_for' => null,
|
||||
// Jobs aus einer DB-Transaktion (z. B. Stripe-Paid-Pfad) erst nach dem Commit einstellen
|
||||
'after_commit' => true,
|
||||
],
|
||||
|
||||
],
|
||||
|
|
|
|||
|
|
@ -8,7 +8,8 @@ von Hand per SSH aus und bricht ab, **bevor** es etwas verändert, wenn etwas ni
|
|||
|
||||
| | Wert |
|
||||
|---|---|
|
||||
| Server | Ploi, Benutzer `ploi`, PHP-FPM 8.4 (`php8.4-fpm`), Horizon als Ploi-Daemon |
|
||||
| Server | Ploi, Benutzer `ploi`, PHP-FPM 8.4 (`php8.4-fpm`), Redis lokal |
|
||||
| Queue | Horizon als Ploi-Daemon, `QUEUE_DRIVER=redis` (bis zur Umstellung F: `sync`) |
|
||||
| Live-Verzeichnis | `/home/ploi/mivita.care` |
|
||||
| Git-Server (Deploy liest hier) | `origin` = `ssh://git@forge.adametz.media:222/kadmin/mivita.git` |
|
||||
| Release-Branch | `master` — Tags werden auf `master` gesetzt |
|
||||
|
|
@ -75,15 +76,7 @@ sleep 2; ps -o pid,lstart,args -C php-fpm8.4 | head -3 # master-PID gleich,
|
|||
Fragt `sudo` nach einem Passwort: beim Deploy `DEPLOY_FPM_RELOAD= bash dev/betrieb/deploy.sh …`,
|
||||
das Skript wartet dann auf Enter, bis FPM in Ploi neu geladen ist.
|
||||
|
||||
**Horizon und Supervisor prüfen:**
|
||||
|
||||
```bash
|
||||
systemctl is-enabled supervisor # enabled
|
||||
php artisan horizon:status # running
|
||||
grep -E '^(QUEUE_DRIVER|QUEUE_CONNECTION)=' .env # QUEUE_DRIVER=redis (config/queue.php liest QUEUE_DRIVER!)
|
||||
```
|
||||
|
||||
In Ploi: Horizon als Daemon (`php artisan horizon`), **„Quick Deploy“ und Deploy-Webhook aus**.
|
||||
In Ploi: **„Quick Deploy“ und Deploy-Webhook aus.** Horizon einrichten: Abschnitt F.
|
||||
|
||||
## B — Einmalig: Live-Verzeichnis an Ort und Stelle zum Checkout machen
|
||||
|
||||
|
|
@ -175,9 +168,9 @@ git push nas master --follow-tags # Spiegel auf dem NAS
|
|||
| Schritt im Skript | Bei Fehler |
|
||||
|---|---|
|
||||
| Arbeitsverzeichnis sauber? Tag auf origin? Neue Migrationen ohne `--migrations`? | Abbruch, nichts verändert |
|
||||
| `down` → Checkout → `RELEASE` → `composer install --no-dev` → `migrate` → Caches leeren → `horizon:terminate` + `queue:restart` → FPM-Reload → `up` | Seite bleibt im Wartungsmodus, Skript nennt den Rollback-Befehl |
|
||||
| `down` → Checkout → `RELEASE` → `composer install --no-dev` → `migrate` → Caches leeren → `horizon:terminate` → FPM-Reload → `up` | Seite bleibt im Wartungsmodus, Skript nennt den Rollback-Befehl |
|
||||
| `about --only=mivita` (Version) | — |
|
||||
| `monitoring:queue-probe --wait=60` | **Exit 3**: live, aber Horizon holt keine Jobs ab — `horizon:status`, Daemon in Ploi prüfen |
|
||||
| `monitoring:queue-probe --wait=60` (entfällt bei `QUEUE_WORKER="none"`) | **Exit 3**: live, aber Horizon holt keine Jobs ab — `horizon:status`, Daemon in Ploi prüfen |
|
||||
|
||||
Exit-Codes: `0` fertig · `1` abgebrochen · `2` falscher Aufruf · `3` kein Worker.
|
||||
Option `--clear-schedule` setzt `withoutOverlapping`-Sperren zurück (nur wenn nötig).
|
||||
|
|
@ -197,11 +190,40 @@ bash dev/betrieb/deploy.sh <vorheriger Tag>
|
|||
|
||||
Baut `vendor/` für den alten Tag neu auf. Nur bei Datenproblemen das DB-Backup einspielen.
|
||||
|
||||
## F — Einmalig: Horizon aktivieren (Umstellung von `QUEUE_DRIVER=sync`)
|
||||
|
||||
Bis Oktober 2026 lief Live mit `QUEUE_DRIVER=sync`: Jobs liefen im Request mit (Stripe-Webhook
|
||||
wartete z. B. auf den Rechnungsnummer-Sync und SMTP). Mit Horizon laufen sie im Hintergrund.
|
||||
Betroffen: `SyncStripeInvoiceReference`, Fehlermails aus `MyLog` (`MailLog`), `PaymentIncidentAlert`,
|
||||
DHL-Jobs (nur bei `DHL_USE_QUEUE`).
|
||||
|
||||
Voraussetzung: Der Tag enthält die Horizon-Einstellungen (alle Queues inkl. `high-priority`/`dhl-*`,
|
||||
`after_commit` an der Redis-Verbindung, Gate für System-Admins, `horizon:snapshot`).
|
||||
|
||||
```bash
|
||||
cd /home/ploi/mivita.care
|
||||
redis-cli ping # PONG
|
||||
grep -E '^(APP_ENV|QUEUE_DRIVER|REDIS_HOST)=' .env # APP_ENV=production
|
||||
```
|
||||
|
||||
1. In Ploi → Site → **Daemons**: Befehl `php8.4 artisan horizon`, Verzeichnis `/home/ploi/mivita.care`,
|
||||
Benutzer `ploi`, 1 Prozess. Prüfen: `php artisan horizon:status` → `running`.
|
||||
2. In `.env`: `QUEUE_DRIVER=redis` (`config/queue.php` liest `QUEUE_DRIVER`, nicht `QUEUE_CONNECTION`).
|
||||
Ohne Config-Cache wirkt das sofort für Web-Requests; Horizon neu starten: `php artisan horizon:terminate`.
|
||||
3. `php artisan monitoring:queue-probe --wait=60` → „Queue-Worker läuft“.
|
||||
4. `/horizon` als System-Admin (admin ≥ 4) öffnen: Supervisor aktiv, keine Failed Jobs.
|
||||
5. Nach der nächsten Stripe-Zahlung: Rechnungsnummer in den PaymentIntent-Metadaten.
|
||||
|
||||
Zurück: `QUEUE_DRIVER=sync` in `.env`, in `deploy.sh` `QUEUE_WORKER="none"`. Liegen gebliebene Jobs
|
||||
vorher mit `php artisan queue:work redis --stop-when-empty` abarbeiten.
|
||||
|
||||
## Fallstricke
|
||||
|
||||
- **FPM-Restart statt Reload** → 502 für Sekunden.
|
||||
- **Horizon nicht neu gestartet** → Worker arbeiten mit altem Code (neue Job-Klassen unbekannt).
|
||||
Das Skript ruft `horizon:terminate`; die Worker-Prüfung am Ende fängt einen nicht zurückgekehrten Horizon ab.
|
||||
- **Neue Queue** (`onQueue('…')`) → in `config/horizon.php` unter `queue` eintragen, sonst bleibt der Job
|
||||
liegen. Ein Test (`HorizonSetupTest`) prüft das für `app/Jobs`.
|
||||
- **Nie `queue:retry all`**: alte Jobs tragen den Klassenstand von damals. Erst `queue:failed`, dann gezielt.
|
||||
- **Push ohne Tags** → `deploy.sh` findet den Tag nicht. Immer `--follow-tags`.
|
||||
- **`.gitignore` enthält `Icon`** (macOS). Bei `core.ignorecase=true` trifft das auch Ordner `icon/` —
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@
|
|||
# entfernt kurz den Socket, Nginx liefert 502.
|
||||
#
|
||||
# Exit-Codes: 0 fertig, 1 abgebrochen (siehe Ausgabe), 2 falscher Aufruf,
|
||||
# 3 live, aber Horizon hat den Test-Job nicht abgeholt.
|
||||
# 3 live, aber kein Worker hat den Test-Job abgeholt.
|
||||
#
|
||||
# Rollback: dasselbe Skript mit dem vorherigen Tag.
|
||||
#
|
||||
|
|
@ -37,7 +37,7 @@ set -euo pipefail
|
|||
DEPLOY_SCRIPT="dev/betrieb/deploy.sh" # Pfad dieses Skripts im Repository
|
||||
PHP_FPM_SERVICE="php8.4-fpm" # systemd-Name des FPM-Dienstes
|
||||
ABOUT_SECTION="mivita" # Abschnitt aus AboutCommand::add (AppServiceProvider)
|
||||
QUEUE_PROBE=1 # 1 = monitoring:queue-probe am Ende
|
||||
QUEUE_WORKER="horizon" # horizon = Ploi-Daemon „php artisan horizon“, none = QUEUE_DRIVER=sync
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
# Git ersetzt diese Datei beim Checkout. Bash liest Skripte stückweise, deshalb
|
||||
|
|
@ -136,11 +136,12 @@ if [[ "$clear_schedule" -eq 1 ]]; then
|
|||
php artisan schedule:clear-cache
|
||||
fi
|
||||
|
||||
step "Horizon und PHP-FPM"
|
||||
step "Worker und PHP-FPM"
|
||||
# Horizon hält den alten Code im Speicher; terminate beendet es nach den
|
||||
# laufenden Jobs, Supervisor (Ploi-Daemon) startet es neu.
|
||||
php artisan horizon:terminate
|
||||
php artisan queue:restart
|
||||
if [[ "$QUEUE_WORKER" == "horizon" ]]; then
|
||||
php artisan horizon:terminate
|
||||
fi
|
||||
default_fpm_reload="echo '' | sudo -S service $PHP_FPM_SERVICE reload"
|
||||
fpm_reload="${DEPLOY_FPM_RELOAD-$default_fpm_reload}"
|
||||
if [[ -n "$fpm_reload" ]]; then
|
||||
|
|
@ -161,7 +162,7 @@ trap - ERR
|
|||
# Läuft Horizon wieder? `horizon:terminate` oben beendet es; neu starten muss
|
||||
# es Supervisor. Läuft Supervisor nicht, steht die Queue still, ohne dass es
|
||||
# jemand merkt.
|
||||
if [[ "$QUEUE_PROBE" -eq 1 ]]; then
|
||||
if [[ "$QUEUE_WORKER" != "none" ]]; then
|
||||
step "Queue-Worker"
|
||||
if ! php artisan monitoring:queue-probe --wait=60; then
|
||||
echo
|
||||
|
|
|
|||
45
tests/Feature/Monitoring/HorizonSetupTest.php
Normal file
45
tests/Feature/Monitoring/HorizonSetupTest.php
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
<?php
|
||||
|
||||
use App\User;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Tests\TestCase;
|
||||
|
||||
uses(TestCase::class);
|
||||
|
||||
/**
|
||||
* Horizon auf Live (QUEUE_DRIVER=redis): jede Queue, auf die ein Job sich
|
||||
* legt, muss ein Supervisor bedienen — sonst bleibt der Job still liegen.
|
||||
*/
|
||||
test('horizon serves every queue the jobs are pushed to', function () {
|
||||
$queues = collect(glob(app_path('Jobs/*.php')))
|
||||
->flatMap(function (string $file): array {
|
||||
preg_match_all("/onQueue\\('([^']+)'\\)/", file_get_contents($file), $matches);
|
||||
|
||||
return $matches[1];
|
||||
})
|
||||
->push('default')
|
||||
->unique();
|
||||
|
||||
$served = collect(config('horizon.defaults'))->pluck('queue')->flatten();
|
||||
|
||||
expect($queues->diff($served)->values()->all())->toBe([]);
|
||||
});
|
||||
|
||||
test('jobs dispatched inside a transaction wait for the commit', function () {
|
||||
expect(config('queue.connections.redis.after_commit'))->toBeTrue();
|
||||
});
|
||||
|
||||
test('only system admins may open the horizon dashboard', function (int $admin, bool $allowed) {
|
||||
$user = new User;
|
||||
$user->admin = $admin;
|
||||
|
||||
expect(Gate::forUser($user)->allows('viewHorizon'))->toBe($allowed);
|
||||
})->with([
|
||||
'system admin' => [4, true],
|
||||
'super admin' => [3, false],
|
||||
'consultant' => [0, false],
|
||||
]);
|
||||
|
||||
test('guests may not open the horizon dashboard', function () {
|
||||
expect(Gate::allows('viewHorizon'))->toBeFalse();
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue