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:
co-authored by
Claude Opus 5
parent
6fbea4fe6f
commit
b7ce114677
+39
-17
@@ -25,7 +25,8 @@ auf welchem Weg kommt ein Post vom Client bis in die Timeline zurück.
|
||||
│ /healthz │ │ GET /api/post|thread/*uri │
|
||||
│ /.well-known/did.json ──────────┼───┼─▶ Schlüssel für 🔒 │
|
||||
│ │ │ GET /api/notifications… 🔒 │
|
||||
│ │ │ GET /api/followers|following│
|
||||
│ /xrpc/…sync.subscribeRepos ─────┼───┼─▶ pds_firehose.rs (WS) │
|
||||
│ (WebSocket, seq-Cursor) │ │ GET /api/followers|following│
|
||||
│ │ │ GET /healthz │
|
||||
│ at-lexicon Validierung (160) │ │ │
|
||||
│ at-repo/at-mst MST + Commit │ │ indexer.rs Upserts │
|
||||
@@ -41,13 +42,13 @@ auf welchem Weg kommt ein Post vom Client bis in die Timeline zurück.
|
||||
┌───────────┐ ┌────────┐ ┌─────────────────┐ │
|
||||
│ Postgres │ │ MinIO │ │ Postgres │ │
|
||||
│ pds :5434 │ │ :9100 │ │ appview :5435 │ │
|
||||
└───────────┘ └────────┘ └─────────────────┘ │
|
||||
│ │
|
||||
│ (heute: kein eigener Firehose-Ausgang) │
|
||||
▼ │
|
||||
│ +firehose │ └────────┘ │ +cursor │ │
|
||||
│ _events │ └─────────────────┘ │
|
||||
└───────────┘ │
|
||||
│
|
||||
┌──────────────────────────────────────────┐ │
|
||||
│ Jetstream-Relay (extern, WebSocket) │──────────────────┘
|
||||
│ JETSTREAM_URL │ at-firehose
|
||||
│ JETSTREAM_URL — kennt diese PDS nicht │ at-firehose
|
||||
└──────────────────────────────────────────┘ JetstreamConsumer
|
||||
```
|
||||
|
||||
@@ -56,27 +57,46 @@ entsprechen. Die AppView verifiziert die ES256-Signatur mit dem öffentlichen
|
||||
Schlüssel, den die PDS in ihrem DID-Dokument veröffentlicht — `PDS_JWT_SECRET`
|
||||
verlässt die PDS nie. Details in [`deployment.md`](deployment.md), Abschnitt 6.
|
||||
|
||||
Zwei Wege führen in die AppView, und das ist Absicht:
|
||||
Drei Wege führen in die AppView, und das ist Absicht:
|
||||
|
||||
1. **Direkter Push (schnell, lokal).** Jeder erfolgreiche Commit auf der PDS
|
||||
wird per `POST /internal/ingest-commit` an die AppView geschoben
|
||||
(`crates/pds-server/src/appview_push.rs`). Best effort, 5 s Timeout, blockiert
|
||||
den Record-Write nie. Damit sieht der Nutzer seinen eigenen Post sofort.
|
||||
2. **Jetstream (global, verzögert).** `at-firehose::JetstreamConsumer` hängt an
|
||||
2. **PDS-Firehose (lokal, garantiert).** Die PDS führt in derselben
|
||||
Transaktion wie den Commit ein Event in `firehose_events` und liefert es
|
||||
über `com.atproto.sync.subscribeRepos` als WebSocket aus
|
||||
(`crates/pds-server/src/firehose.rs`). Die AppView konsumiert das mit
|
||||
persistiertem Cursor (`crates/appview/src/pds_firehose.rs`).
|
||||
3. **Jetstream (global, verzögert).** `at-firehose::JetstreamConsumer` hängt an
|
||||
einem externen Jetstream-Relay und liefert alles, was in den konfigurierten
|
||||
Collections weltweit passiert.
|
||||
|
||||
Wichtig für das Verständnis der Topologie: **die eigene PDS speist den
|
||||
Jetstream nicht.** Es gibt keinen `com.atproto.sync.subscribeRepos`-Endpoint im
|
||||
PDS-Router. Der Firehose-Weg ist ein reiner Konsum-Pfad für fremde Repos; die
|
||||
eigenen Records erreichen die AppView ausschließlich über den Push aus
|
||||
Punkt 1 (noch offen).
|
||||
Warum 1 **und** 2: Der Push ist der schnelle Weg, der Firehose der
|
||||
verlässliche. Ein verlorener Push (AppView kurz weg, Netzwerkfehler) war
|
||||
früher endgültig — der öffentliche Jetstream kennt diese PDS nicht, also
|
||||
wäre der Post nie angekommen. Jetzt holt der Cursor-Replay ihn nach.
|
||||
Dass beide Wege denselben Commit liefern, ist unkritisch: die Indexer-Pfade
|
||||
sind Upserts, und der Dedupe-Index der Notifications fängt den Rest.
|
||||
|
||||
Wichtig für das Verständnis der Topologie bleibt: **die eigene PDS speist den
|
||||
*öffentlichen* Jetstream nicht.** Weg 2 ist ein lokaler Firehose zwischen den
|
||||
eigenen zwei Diensten; ein fremder Relay erfährt von dieser PDS weiterhin
|
||||
nichts.
|
||||
|
||||
Zur Spec-Treue: Die Frame-Hülle ist konformes DAG-CBOR mit Tag-42-CID-Links.
|
||||
Die Blöcke *darin* tragen die Konvention dieses Codebases — CIDs innerhalb von
|
||||
Commit-Blöcken sind Strings, nicht Links (`at-repo/src/commit.rs`). Ein
|
||||
fremder atproto-Consumer kann die Frames also lesen, scheitert aber beim
|
||||
Validieren der Blockinhalte. Das zu ändern hieße, die Blockkodierung zu
|
||||
ändern, und damit ändern sich sämtliche CIDs inklusive der
|
||||
`did:plc:`-Ableitung — eine eigene, bewusste Migration.
|
||||
|
||||
## Crates
|
||||
|
||||
| Crate | Typ | Aufgabe |
|
||||
|---|---|---|
|
||||
| `at-lexicon` | lib | Lexicon-Schemas laden (`Lex::from_json`) und Records validieren. `LexRegistry` in der PDS kennt `app.twi.post` (160 Zeichen), `app.bsky.feed.like`, `app.bsky.feed.repost`, `app.bsky.actor.profile` — alle vier per `include_str!` einkompiliert |
|
||||
| `at-lexicon` | lib | Lexicon-Schemas laden (`Lex::from_json`) und Records validieren. `LexRegistry` in der PDS kennt `app.twi.post` (160 Zeichen), `app.bsky.feed.like`, `app.bsky.feed.repost`, `app.bsky.graph.follow`, `app.bsky.actor.profile` — alle per `include_str!` einkompiliert |
|
||||
| `at-crypto` | lib | secp256k1/P-256-Keypairs, DAG-CBOR-CIDs, multibase/base58btc, JWT (`issue_jwt` / `verify_jwt`), PLC-Operationen inkl. `did_plc_from_op` |
|
||||
| `at-identity` | lib | Handle- und DID-Auflösung. Drei Resolver hinter dem Trait `DidHandleResolver`: `PlcClient` (PLC-Directory), `WebResolver` (`.well-known/did.json`), `PdsHandleResolver` (fragt die lokale PDS) |
|
||||
| `at-mst` | lib | Merkle-Search-Tree: Knoten, `split_around`, `wrap_with_split`, spec-konformes `encode_key` |
|
||||
@@ -164,9 +184,11 @@ Alle Schreibpfade sind Upserts, das Replay nach einem Reconnect ist damit
|
||||
unschädlich. Fehlerhafte Events rücken den Cursor **nicht** vor.
|
||||
|
||||
`ingest.rs` bedient denselben Indexer über HTTP, mit den Aktionen
|
||||
`create` / `delete`; für `app.bsky.graph.follow`-Deletes braucht der Aufrufer
|
||||
`subject_did` im Body, weil der Record-Wert bei Deletes nicht garantiert
|
||||
mitkommt.
|
||||
`create` / `delete`. Für `app.bsky.graph.follow`-Deletes gibt es zwei Wege:
|
||||
der Push schickt `subject_did` im Body mit, der Firehose kennt nur `did` +
|
||||
`rkey` — deshalb speichert `follows` seit Migration 0011 den rkey des
|
||||
Follow-Records und löst darüber auf. Ohne den rkey war ein Unfollow über den
|
||||
Firehose nicht anwendbar und hing allein am Push.
|
||||
|
||||
## Datenbanken und Tabellen
|
||||
|
||||
|
||||
+38
-5
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user