feat(backend): Aenderungsfeed per Long Polling (Schritt 4)
GET /documents/{id}/changes haelt die Anfrage offen und antwortet, sobald
sich etwas tut — kumuliertes Diff seit der bekannten Version, dazu die
Ereignisse mit ihrem Absender. Ist die Basis verdichtet oder hat der Client
noch gar nichts, kommt der Volltext statt der Operationen: ein Roundtrip und
ein Sonderzustand weniger als ein eigener Fehlerpfad.
Der Feed arbeitet auf der Historie, nicht am Dokument — ein geloeschtes
Dokument muss sein DELETED noch zustellen koennen.
Blockierend auf virtuellen Threads statt DeferredResult (D76-Nachtrag 5):
So behaelt der Endpunkt die aus der Spezifikation generierte Signatur, und
API-First bleibt fuer ihn unangetastet; ein Wartender kostet trotzdem fast
nichts. Geweckt wird nach dem Commit, nie davor, und ueber einen Stempel, den
der Aufrufer VOR dem Nachsehen liest — sonst ginge ein Signal aus der Luecke
dazwischen verloren.
122 Tests. Das Szenario "ein Wartender wird geweckt" misst die Dauer: Ohne
das bestuende es auch dann, wenn der Wartende bloss in den Timeout liefe und
danach die Aenderung vorfaende. Gegenprobe: Benachrichtigung entfernt ->
genau dieses Szenario faellt; Volltext-Rueckfall entfernt -> genau jenes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
ae85503b79
commit
741b41ef11
@@ -6114,3 +6114,49 @@ Zahlen: 104 Tests. Gegenproben je Regel — Prüfsumme nicht geprüft, Idempoten
|
||||
entfernt, veraltete Basis abgelehnt statt verschoben, Schreibpause ignoriert,
|
||||
Rückfall wieder als `RESTORED`, jüngster Stand aus der Historie genommen: Es
|
||||
fallen jeweils genau die danach benannten Zusicherungen.
|
||||
|
||||
**Nachtrag 5 — Long Polling blockiert, aber auf einem virtuellen Thread
|
||||
(2026-08-26).** Der Haupttext sah `DeferredResult` vor, damit ein Wartender
|
||||
keinen Server-Thread bindet. Beim Bauen stellte sich das als teurer heraus,
|
||||
als es klingt: Der Endpunkt steht in der OpenAPI-Spezifikation, und der
|
||||
Generator erzeugt daraus eine **synchrone** Signatur
|
||||
(`ResponseEntity<ChangeFeed>`). Ein `DeferredResult` verlangt eine andere —
|
||||
also entweder die Operation aus der Generierung herausnehmen (dann prüft
|
||||
niemand mehr, ob Vertrag und Code zusammenpassen; genau die Zusage, für die
|
||||
API-First in diesem Projekt gebaut ist) oder den Generator umstellen (WebFlux
|
||||
für alles).
|
||||
|
||||
**Gewählt: blockieren, und `spring.threads.virtual.enabled=true`.** Auf JDK 21
|
||||
kostet ein wartender virtueller Thread praktisch nichts — kein Stack von einem
|
||||
Megabyte, keine Poolgrenze. Das Argument gegen das Blockieren war der
|
||||
Speicher, und der ist auf der Zielumgebung tatsächlich die knappe Größe
|
||||
(D76-Nachtrag 3); genau dort löst der virtuelle Thread es auf, statt es zu
|
||||
verschieben. Der Endpunkt behält die generierte Signatur, und für ihn gilt
|
||||
dieselbe Regel wie für alle anderen: Weicht die Implementierung vom Vertrag
|
||||
ab, bricht der Compile.
|
||||
|
||||
**Zwei Dinge, die daran hängen und leicht zu übersehen sind.** Erstens darf
|
||||
das Warten nicht mit `synchronized`/`wait()` gebaut sein — ein Monitor nagelt
|
||||
den virtuellen Thread an seinen Träger (JDK 21). Der `ChangeNotifier` benutzt
|
||||
deshalb `ReentrantLock`/`Condition`. Zweitens darf **während** des Wartens
|
||||
keine Transaktion und keine Datenbankverbindung offen sein; der
|
||||
`LiveEditingService` ist ohnehin nicht transaktional und liest über je eigene
|
||||
Aufrufe.
|
||||
|
||||
**Geweckt wird nach dem Commit, nicht davor** (`afterCommit` der
|
||||
Transaktions-Synchronisation). Davor geweckt läse ein Beobachter einen Stand,
|
||||
der noch nicht steht — und bekäme das Ereignis nie wieder, denn er zieht
|
||||
danach mit der neuen Version weiter.
|
||||
|
||||
**Der Stempel statt der Versionsnummer.** Der Aufrufer liest den Stempel des
|
||||
Dokuments, **bevor** er in der Datenbank nachsieht. Ändert sich etwas in der
|
||||
Lücke dazwischen, kehrt das Warten sofort zurück. Ohne diesen Griff ginge das
|
||||
Signal verloren und der Client bekäme seine Änderung erst nach Ablauf der
|
||||
vollen Wartezeit — ein Fehler, der im Test nur auffällt, wenn man die **Dauer**
|
||||
misst. Das Cucumber-Szenario tut das (< 4 s bei 5 s Wartezeit); gegengeprüft
|
||||
durch Entfernen der Benachrichtigung, dann fällt genau dieses Szenario.
|
||||
|
||||
**`RENAMED` ist noch nicht vergeben.** Der Feed kennt `CREATED`, `UPDATED`,
|
||||
`DELETED`, `RESTORED` und `ROLLED_BACK`. Das Umbenennen bekommt seinen eigenen
|
||||
Weg (`PATCH /title` mit `expectedVersion`) und erst damit den Typ — ihn vorher
|
||||
zu deklarieren wäre eine Zusage ohne Deckung.
|
||||
|
||||
Reference in New Issue
Block a user