# Background-Jobs

Lalabase verarbeitet Aufgaben wie E-Mail-Versand, Benachrichtigungen und KI-Berechnungen
asynchron über **Solid Queue** — das native Job-System von Rails 8. Die Jobs liegen in
derselben PostgreSQL-Datenbank; ein separater Dienst wie Sidekiq ist nicht nötig.

## Worker-Prozess

In der Container-Installation läuft der Worker als eigener Dienst neben der Web-Anwendung.
Prüfe seinen Status mit:

```bash
docker compose ps
docker compose logs -f worker
```

## Stockende Jobs erkennen

Stauen sich Jobs an, ist meist der Worker-Prozess gestoppt oder eine externe Abhängigkeit
(z. B. der KI-Provider) nicht erreichbar. Ein Blick in die Worker-Logs zeigt die Ursache.

<div class="docs-callout docs-callout--info">
  <div class="docs-callout__title">Nach einem Update</div>
  <p>Starte den Worker nach jedem Update mit neu, damit er den aktuellen Code lädt — sonst verarbeitet er Jobs weiter mit der alten Version.</p>
</div>

## Wiederkehrende Jobs

Einige Aufgaben laufen nach einem festen Zeitplan (Solid-Queue-Recurring-Tasks in
`config/recurring.yml`), z. B. das Aufräumen erledigter Jobs, geplante Konto-Löschungen
und die Doku-Synchronisation.

### Nicht verknüpfte Dateien aufräumen (DSGVO)

Beim Hochladen entsteht eine Datei im Speicher, **bevor** das zugehörige Formular
abgeschickt ist. Wird das Formular nie abgeschickt — oder wird ein Bild in einen Text
eingefügt, wieder entfernt und der Text gespeichert —, bleibt die Datei ohne
Verknüpfung liegen. Rails räumt solche Dateien nicht von selbst ab.

Täglich um **05:15 Uhr** löscht `UnattachedBlobPurgeJob` alle Dateien, die **länger als
sieben Tage** ohne Verknüpfung sind. Erst ab dieser Löschung laufen sie in dieselben
Sicherungsfristen wie alle übrigen Daten — ohne den Job liefen sie in gar keine.

<div class="docs-callout docs-callout--info">
  <div class="docs-callout__title">Warum sieben Tage warten?</div>
  <p>Zwischen dem Hochladen und dem Absenden eines Formulars ist eine noch nicht
  verknüpfte Datei technisch nicht von einer aufgegebenen zu unterscheiden. Die Wartefrist
  ist der Schutz davor, jemandem die Datei unter einem offenen Formular wegzulöschen.
  Dieser Job hat deshalb <strong>keine</strong> Scharfschalt-Variable: Er löscht ab dem
  ersten Lauf, die Sicherheit steckt in der Wartefrist und in einer Obergrenze pro Lauf.</p>
</div>

Vorher ansehen, was gelöscht würde (löscht nichts):

```bash
bin/rails attachments:unattached_report
```

Was der Job getan hat, steht im Anwendungsprotokoll unter `[unattached_blob_purge]` —
je gelöschter Datei eine Zeile mit Name, Typ, Größe und Datum, niemals mit Inhalt.

### Verzeichnis gelöschter Dateianhänge (DSGVO)

Wird ein Dateianhang gelöscht, schreibt Lalabase einen Eintrag in ein Verzeichnis:
Organisation, Dateiname, Typ, Größe, Trägerart und Zeitpunkt der Löschung. **Der Inhalt der
Datei wird nie erfasst.** Das Verzeichnis belegt gegenüber dem Verantwortlichen, dass und
wann gelöscht wurde (Rechenschaftspflicht, Art. 5 Abs. 2 DSGVO).

Zwei wiederkehrende Jobs halten es in Stand:

| Job | Zeit | Aufgabe |
|---|---|---|
| `AttachmentBackupConfirmationJob` | 05:45 | trägt aus dem Protokoll des Sicherungsskripts nach, wann die Kopie aus Spiegel und Ablageort verschwunden ist |
| `AttachmentDeletionRecordPurgeJob` | 06:00 | löscht Einträge, die **drei Jahre** alt sind |

<div class="docs-callout docs-callout--info">
  <div class="docs-callout__title">Warum die Frist an der Löschung hängt</div>
  <p>Die drei Jahre laufen ab der Löschung im Produktivsystem, nicht ab dem nachgetragenen
  Ablauf der Sicherungskopien. Der Nachtrag kann ausbleiben (Ausfall des Sicherungslaufs,
  verlorene Protokollzeile, eine Datei, die nie in eine Sicherung gelangt ist). Hinge die
  Frist daran, hätte so ein Eintrag <strong>gar kein Fristende</strong> — genau der Mangel,
  den das Verzeichnis belegen soll.</p>
</div>

Der Nachtrag setzt voraus, dass der Worker das Protokoll des Sicherungsskripts **lesen
kann**. Das Skript legt es root-eigen an; die Freigabe über eine Lesegruppe ist in
`ops/backup/README.md` beschrieben. Fehlt sie, protokolliert der Job eine Warnung unter
`[deletion_register]` statt still nichts zu tun.

**Auskunft erteilen.** Verlangt ein Verantwortlicher den Nachweis für seine Organisation,
führen zwei Wege zur selben Aussage: die Seite **Löschverzeichnis** im Adminbereich oder
der Bericht auf der Kommandozeile.

```bash
bin/rails deletion_register:report                        # alles
ORG=<organisation-uuid> bin/rails deletion_register:report
```

Beide nennen den Stichtag des Verzeichnisses und weisen Einträge ohne bestätigten
Sicherungsablauf gesondert aus. Beides gehört in die Antwort: Vor dem Stichtag Gelöschtes
ist nicht erfasst, und ein Eintrag ohne Nachtrag ist einer, dessen Ablauf niemand bestätigt
hat. Die Auswahl der Organisation kommt aus dem Verzeichnis selbst und nicht aus der
Organisationsliste, denn ein Eintrag überlebt die Organisation, die er betrifft.

### Embedding-Aufräumung (DSGVO)

Für die semantische Suche und den KI-Assistenten speichert Lalabase **abgeleitete
Embedding-Vektoren**. Wird der zugrunde liegende Inhalt gelöscht, entfernen die
Löschpfade ihre Vektoren in aller Regel **sofort und synchron** (insbesondere bei
personenbezogenen Daten wie Meeting-Transkripten und -Analysen).

Als zusätzliches Netz läuft **täglich um 04:30 Uhr** ein Aufräum-Job
(`Embeddings::OrphanGcJob`), der verwaiste Vektoren einsammelt, deren Ursprungsinhalt
über einen technischen Sonderweg ohne synchrone Bereinigung entfernt wurde. Dadurch
persistieren abgeleitete Vektoren gelöschter Inhalte **höchstens rund 24 Stunden**.

<div class="docs-callout docs-callout--info">
  <div class="docs-callout__title">Scharfschaltung (EMBEDDINGS_GC_ENFORCE)</div>
  <p>Der tägliche Job startet aus Sicherheitsgründen im <strong>Probelauf</strong>: Er
  protokolliert nur, wie viele verwaiste Vektoren er einsammeln würde, löscht aber
  nichts. Prüfe die ersten Nächte die Log-Zahlen (oder <code>DRY_RUN=1 bin/rails "embeddings:gc"</code>).
  Sind die Zahlen plausibel, setze am Worker-Prozess die Umgebungsvariable
  <code>EMBEDDINGS_GC_ENFORCE=1</code> — dann löscht der Job tatsächlich. Der manuelle
  Task unten (ohne <code>DRY_RUN</code>) löscht dagegen sofort scharf, unabhängig von
  dieser Variable.</p>
</div>

Manuell anstoßen (z. B. nach einer großen Löschaktion):

```bash
bin/rails "embeddings:gc"          # ein globaler Lauf über alle Organisationen
DRY_RUN=1 bin/rails "embeddings:gc" # nur zählen, nichts löschen
```

### Alte Vektorräume abräumen (Retention)

Wechselt eine Organisation ihr Embedding-Modell, bleibt der alte Vektorraum als
**Rückfallnetz** liegen — man kann jederzeit dorthin zurückrollen. Wie viele Räume eine
Organisation behält, steuert die Einstellung `embedding_retention_limit` (Standard: 2, also
der aktive plus ein älterer). Derselbe nächtliche Job räumt alles darüber hinaus ab.

Zwei Räume rührt er dabei **nie** an: den, der gerade die Suche bedient, und einen, der
gerade aufgebaut wird. Der Raum im Aufbau zählt außerdem nicht gegen das Limit — sonst würde
ausgerechnet der Vergleichsraum verschwinden, während man zwei Modelle gegeneinander testet.

<div class="docs-callout docs-callout--warning">
  <div class="docs-callout__title">Eigene Scharfschaltung (EMBEDDINGS_RETENTION_ENFORCE)</div>
  <p>Dieses Aufräumen hat einen <strong>eigenen</strong> Schalter, absichtlich getrennt von
  <code>EMBEDDINGS_GC_ENFORCE</code>. Der Orphan-Lauf löscht Vektoren, deren Ursprungsinhalt
  nachweislich weg ist — die sind wertlos. Hier dagegen werden <em>intakte</em> Vektoren
  gelöscht: das Rückfallnetz. Wer den einen Schalter umlegt, soll nicht ungefragt auch den
  anderen scharf schalten. Setze <code>EMBEDDINGS_RETENTION_ENFORCE=1</code> am
  Worker-Prozess, wenn die Probelauf-Zahlen plausibel sind. Achtung: Der manuelle Task oben
  (<code>bin/rails "embeddings:gc"</code> ohne <code>DRY_RUN</code>) löscht auch die Räume
  jenseits des Limits sofort scharf — unabhängig von beiden Variablen. <code>DRY_RUN=1</code>
  ist dort der einzige Schutz.</p>
</div>

**Zeitfenster-Hinweis für Rollbacks:** Während `rake embeddings:rollback` läuft
(Reaktivierung + Delta-Reindex — das dauert so lange wie das Korpus braucht), ist der
Zielraum vorübergehend weder aktiv noch konfiguriert und damit für den Retention-Lauf ein
regulärer Kandidat. Lege Rollbacks deshalb nicht in das nächtliche GC-Fenster (04:30),
solange `EMBEDDINGS_RETENTION_ENFORCE=1` gesetzt ist.

<div class="docs-callout docs-callout--info">
  <div class="docs-callout__title">Was NICHT abgeräumt wird</div>
  <p>Vektoren aus einem Modellwechsel, der vor Einführung der Raumverwaltung stattfand,
  gehören zu keinem registrierten Raum. Sie werden bewusst <strong>nicht</strong> gelöscht:
  dieselbe Signatur hat auch ein gerade erst konfiguriertes Ziel, dessen Aufbau noch nicht
  gestartet ist — auf Verdacht zu löschen würde einen laufenden Aufbau treffen. Der Job
  protokolliert sie stattdessen als <code>UNREGISTERED</code>. Wenn du dort Zeilen siehst,
  ist das eine Aufräum-Entscheidung für dich, keine Fehlfunktion.</p>
</div>
