docs: Firehose, Follow-Lexicon und Phase 10

architecture.md zeigt den subscribeRepos-Pfad im Diagramm (er stand zuerst
irrtümlich am Pfeil zum externen Relay) und erklärt, warum es Push *und*
Firehose gibt: der eine ist schnell, der andere verlässlich. Dazu, wo die
Spec-Treue endet — die Frame-Hülle ist konform, die Blöcke darin nicht.

deployment.md bekommt einen Firehose-Abschnitt: Transaktionsgarantie, warum
die seq lückenfrei ist und was der globale Advisory-Lock an Durchsatz
kostet, Cursor-Semantik, WebSocket-Upgrade im Proxy, und die fehlende
Retention für firehose_events.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013HC9HLrUU1LNwkzp8nkDLX
This commit is contained in:
tomdebone
2026-09-10 07:08:46 +02:00
co-authored by Claude Opus 5
parent 6fbea4fe6f
commit b7ce114677
3 changed files with 87 additions and 27 deletions
+38 -5
View File
@@ -287,7 +287,7 @@ Hinweise:
* Es gibt **keinen** Signal-Handler für graceful Shutdown. `systemctl stop`
beendet den Prozess hart; bei der AppView bedeutet das, dass der letzte
Cursor-Flush nur passiert, wenn der Kanal regulär geschlossen wird —
praktisch also mit bis zu 100 Events Verlust (siehe Abschnitt 9). Das ist
praktisch also mit bis zu 100 Events Verlust (siehe Abschnitt 10). Das ist
unkritisch, weil der Cursor beim Resume ohnehin leicht in die Vergangenheit
zeigt und Events idempotent verarbeitet werden.
* Eine Abhängigkeit `After=` auf Postgres/MinIO ist nur nötig, wenn diese auf
@@ -344,7 +344,33 @@ Instanz hinter VPN und für die fail-open-Integrationstests. Die AppView warnt
beim Start in Großbuchstaben. Öffentlich erreichbar heißt das: jeder kann die
Notifications jeder DID lesen und als gelesen markieren.
## 7. Reverse-Proxy
## 7. Firehose
Die PDS liefert `com.atproto.sync.subscribeRepos` als WebSocket aus, die
AppView konsumiert ihn. Betrieblich wichtig:
* **Das Event liegt in derselben Transaktion wie der Commit.** Es kann keinen
Commit ohne Event geben und umgekehrt.
* **Die `seq` ist lückenfrei.** Ein globaler `pg_advisory_xact_lock` sorgt
dafür, dass Commit-Reihenfolge und `seq`-Reihenfolge übereinstimmen — sonst
könnte ein Consumer eine Nummer überspringen, die erst danach sichtbar wird,
und sie nie nachholen. Preis: das Ende jeder schreibenden Transaktion ist
über alle Accounts hinweg serialisiert.
* **Cursor:** `?cursor=<seq>` liefert alles mit `seq > cursor` aus der
Datenbank nach und geht dann nahtlos live weiter. Ohne Cursor nur live. Ein
Cursor aus der Zukunft ist ein Fehler-Frame, ein zu alter ein
`#info`/`OutdatedCursor`.
* **`firehose_events` wächst unbegrenzt.** Es gibt keine Retention. Beschneiden
ist sicher, weil ein zu alter Cursor sauber behandelt wird — wer die Tabelle
aufräumt, sollte aber wissen, wie weit die eigenen Consumer zurückhängen
dürfen (`pds_firehose_seq` in `/healthz` der AppView gegen `MAX(seq)`).
* **Reverse-Proxy:** die Route braucht ein WebSocket-Upgrade (`Upgrade`/
`Connection`-Header durchreichen) und einen Read-Timeout, der längere
Ruhephasen überlebt.
* `PDS_FIREHOSE_ENABLED=false` schaltet den Consumer in der AppView ab; lokale
Commits hängen dann wieder allein am Best-Effort-Push.
## 8. Reverse-Proxy
### PDS
@@ -462,7 +488,7 @@ HTTP-Aufrufe an PDS/AppView laufen über den Rust-IPC-Layer
(`src-tauri/src/pds_client.rs`, `appview_client.rs`), nicht aus dem Webview —
die CSP muss also für neue Backend-URLs nicht angefasst werden.
## 8. Health-Checks und Logs
## 9. Health-Checks und Logs
### PDS
@@ -526,7 +552,7 @@ Log-Zeilen, auf die es sich lohnt zu achten:
| `s3 ping failed at startup` | MinIO beim PDS-Start nicht erreichbar |
| `plc submit failed (dev ok)` | PLC-Directory nicht erreichbar; die DID bleibt lokal gültig, ist aber global nicht registriert |
## 9. Neustart-Verhalten
## 10. Neustart-Verhalten
**PDS.** Zustandslos bis auf Postgres und MinIO. Der In-Memory-Blockstore
(`MemoryBlockstore` in `state.rs`) wird beim Start neu aufgebaut; persistent
@@ -569,7 +595,7 @@ nach — zuerst über die lokale PDS (`PdsHandleResolver`, 2 s Timeout), dann PL
bzw. `did:web`. Nach einem Neustart holt der erste Durchlauf das nach; der
Zustand ist reine Anzeigekosmetik.
## 10. Was noch offen ist
## 11. Was noch offen ist
* Kein Compose-Service für `pds-server` / `appview` — das Compose-File deckt nur
Postgres und MinIO ab. Es gibt kein Dockerfile im Repo.
@@ -581,3 +607,10 @@ Zustand ist reine Anzeigekosmetik.
einsetzt. Signatur, Ablauf, `scope` und `sub` werden geprüft.
* Notifications werden nie gelöscht; ein Unlike/Unfollow lässt die Zeile stehen.
* Kein Backfill-Werkzeug für Jetstream-Lücken.
* **Keine Retention für `firehose_events`.** Die Tabelle wächst mit jedem
Commit und wird nie beschnitten. Pruning ist sicher — ein Consumer mit zu
altem Cursor bekommt `#info`/`OutdatedCursor` und läuft ab der ältesten
überlebenden Zeile weiter — aber es gibt weder Job noch Policy dafür.
* Der globale Advisory-Lock, der die `seq`-Vergabe ordnet, serialisiert das
Ende jeder schreibenden Transaktion über alle Accounts hinweg. Das
begrenzt den Schreibdurchsatz auf ein COMMIT nach dem anderen.