# DWS Stapel-Worker (Batch)

Arbeitet Stapelaufträge ab, die über `PATCH /api/{version}/batch/{batchJobId}/job/START` in die
Warteschlange gestellt wurden. Läuft als `php-cli` unter systemd, **nicht** über Apache.

Je Umgebung eine Instanz — dieselbe Datei, unterschiedlicher Ablageort:

| Instanz | Skript | Datenbank | Redis-Präfix |
|---|---|---|---|
| `develop` | `/var/www/html/dws/develop/v1/batchworker.php` | `DatamatWebServiceDevelop` | `dws:develop:batch:*` |
| `productive` | `/var/www/html/dws/productive/v1/batchworker.php` | `DatamatWebServiceProductive` | `dws:productive:batch:*` |

Die Umgebung ergibt sich allein aus dem Ablageort: `__rootloader.php` leitet daraus Datenbank, `.env`
und den Redis-Schlüsselpräfix ab. Am Worker selbst ist nichts umzustellen.

## Aufbau

```
batch/
├── systemd/dws-batchworker@.service   Template-Unit (eine Datei für beide Instanzen)
├── instances/develop.env              Pfad zum Skript + Servername für die CLI
└── instances/productive.env
```

Alles Fachliche steht in `{umgebung}/v1/.config/.env`, nicht in den Instanz-Dateien.

## Redis

Redis ist reiner Transportweg: es überträgt ausschließlich Job-IDs, nie Daten und nie Zustände.
Wahrheit ist `tblBatchJob`. Wer einen Auftrag tatsächlich bekommt, entscheidet der atomare
Zustandswechsel `QUEUED → RUNNING` in der Datenbank — ein verlorener Queue-Eintrag ist damit folgenlos
(der Reconciler reicht ihn nach), ein doppelter ebenso (der zweite Zugriff scheitert am Zustand).

Fällt Redis ganz aus, nimmt die API weiter Aufträge an und der Worker fällt auf den Treiber `dbpoll`
zurück: er sucht sich den nächsten Auftrag dann selbst in der Datenbank. Langsamer, aber lückenlos.

### Einrichtung (einmalig, als root)

```bash
apt update && apt install -y redis-server
```

Absichern — nur lokal erreichbar, Passwort, und die Warteschlange darf **nie** verdrängt werden:

```bash
sed -i 's/^# *requirepass .*/requirepass HIER_EIN_LANGES_PASSWORT/' /etc/redis/redis.conf
grep -q '^maxmemory-policy' /etc/redis/redis.conf && sed -i 's/^maxmemory-policy.*/maxmemory-policy noeviction/' /etc/redis/redis.conf || echo 'maxmemory-policy noeviction' >> /etc/redis/redis.conf
grep -q '^bind 127.0.0.1' /etc/redis/redis.conf || sed -i 's/^bind .*/bind 127.0.0.1 ::1/' /etc/redis/redis.conf
systemctl enable --now redis-server && systemctl restart redis-server
redis-cli -a HIER_EIN_LANGES_PASSWORT --no-auth-warning ping
```

Erwartet: `PONG`. `maxmemory-policy noeviction` ist der entscheidende Punkt — mit einer
Verdrängungsstrategie würde Redis unter Speicherdruck stillschweigend Aufträge aus der Warteschlange
werfen.

### `.env` je Umgebung ergänzen

Die `.env` wird weder per SFTP hochgeladen noch promotet, also in **beiden** Umgebungen von Hand:

```
DWS_BATCH_QUEUE_DRIVER=redis
DWS_REDIS_HOST=127.0.0.1
DWS_REDIS_PORT=6379
DWS_REDIS_PASSWORD=HIER_EIN_LANGES_PASSWORT
DWS_REDIS_DB=0
```

`DWS_REDIS_DB` trennt DEV und PROD zusätzlich zum Schlüsselpräfix — z. B. `0` für develop, `1` für
productive. Ohne `DWS_BATCH_QUEUE_DRIVER=redis` läuft alles im `dbpoll`-Betrieb.

## Dienst einrichten

```bash
cp /var/www/html/dws/batch/systemd/dws-batchworker@.service /etc/systemd/system/
systemctl daemon-reload
systemctl enable --now dws-batchworker@develop
systemctl status dws-batchworker@develop --no-pager
```

Für Productive dasselbe mit `@productive`.

## Betrieb

```bash
journalctl -u dws-batchworker@develop -f          # mitlesen
systemctl restart dws-batchworker@develop         # nach einem Deploy
systemctl stop dws-batchworker@develop            # anhalten
```

Von Hand, ohne Dienst:

```bash
cd /var/www/html/dws/develop/v1
php batchworker.php --once -v                     # genau einen Auftrag
php batchworker.php --daemon -v                   # Dauerbetrieb im Vordergrund
php batchworker.php --reconcile -v                # nur aufräumen
php batchworker.php --once --driver=dbpoll -v     # Notbetrieb erzwingen (ohne Redis)
```

Ein Deploy erfordert einen Neustart des Dienstes — der Worker lädt den Code beim Start, nicht je Auftrag.

## Was der Reconciler tut

Läuft im Daemon alle 5 Minuten mit, einzeln über `--reconcile`:

1. **Hängengebliebene Aufträge**: `RUNNING` ohne Fortschritt seit 15 Minuten → zurück auf `QUEUED`.
   Passiert, wenn ein Worker mitten in der Arbeit stirbt. Bereits fertige Positionen bleiben erhalten,
   der zweite Anlauf verarbeitet nur die restlichen — und rechnet sie damit auch kein zweites Mal ab.
2. **Verlorene Queue-Einträge**: `QUEUED` seit über einer Minute → erneut einreihen. Das ist die
   Absicherung gegen einen Redis-Ausfall zum Startzeitpunkt.
3. **Abgelaufene Aufträge**: Ordner löschen, Zeilen entfernen — sowohl die nie gestarteten
   (Upload-Frist) als auch die fertigen (Haltedauer).

## Fehlersuche

| Beobachtung | Ursache |
|---|---|
| `Queue-Treiber: dbpoll`, obwohl `redis` konfiguriert | Redis nicht erreichbar oder Passwort falsch — `redis-cli -a … ping` prüfen |
| Aufträge bleiben auf `QUEUED` | Kein Worker läuft (`systemctl status`), oder er läuft in der falschen Umgebung |
| Jede Position endet mit `SRV-100` | Validator-Daemon aus: `systemctl status dws-validator@develop` |
| Auftrag steht dauerhaft auf `RUNNING` | Worker gestorben; der Reconciler holt ihn nach 15 Minuten zurück |
| `[dws-fatal]` im Journal | PHP-Fehler beim Start, meist Datenbank oder fehlende Tabellen |
