Skip to content

Die Vorschläge-Karte rechnet einmal für alle statt dreimal pro Leser (v7.231.3) - #1326

Merged
wintermeyer merged 1 commit into
mainfrom
feed-discovery-pool
Aug 7, 2026
Merged

Die Vorschläge-Karte rechnet einmal für alle statt dreimal pro Leser (v7.231.3)#1326
wintermeyer merged 1 commit into
mainfrom
feed-discovery-pool

Conversation

@wintermeyer

Copy link
Copy Markdown
Owner

Die "Vorschläge"-Karte im Feed hat ihre Kandidaten bisher pro Leser neu ermittelt, und zwar bis zu dreimal: eine Leiter aus drei Stufen (fremde Autoren aus den letzten 14 Tagen → fremde Autoren jeden Alters → alle außer mir selbst, wieder aus 14 Tagen), jede Stufe ein eigener Scan über posts plus Like-Rollup, gruppiert auf einen Post pro Autor.

Der teure Teil dieser Frage ist aber für jeden Leser derselbe: welche öffentlichen Posts dieser Sprache kamen gut an, einer pro Autor, bester zuerst. Persönlich ist nur die letzte Meile — nicht meine eigenen Posts, niemand, den ich stumm gestellt oder blockiert habe, und Leute, denen ich noch nicht folge, vor denen, denen ich schon folge.

Was sich ändert

Der geteilte Teil wandert in einen Snapshot. Vutuv.Posts.PopularPosts ist ein GenServer nach genau demselben Muster wie Vutuv.Social.PopularUsers (die Schwesterkarte "Wem folgen?" im selben Rail) und Vutuv.Posts.TopPosters: alle zehn Minuten eine Rangliste pro konfigurierter Sprache in eine read_concurrency-ETS-Tabelle, :miss fällt auf die bisherige Abfrage zurück. Eine Ranglisten-Abfrage pro Sprache für die ganze Installation, egal wie viele Leute gerade lesen.

Das lohnt sich mehr, als eine Zahl pro Seitenaufruf vermuten lässt: feed.ex zeichnet das Rail alle fünf Minuten neu (@suggestions_refresh), also skalierte die alte Rechnung mit offenen Tabs, nicht mit Lesern.

Die drei Stufen werden eine Sortierung. Sie waren immer eine Rangfolge über eine Kandidatenmenge, keine drei Suchen. Jetzt sind sie ein CASE im ORDER BY der Ziehung. Nebenbei behebt das die Richtung: Stufe 1 und 2 schließen alle aus, denen der Leser folgt, also bezahlten gut vernetzte Mitglieder bisher zwei Ziehungen, die die Karte gar nicht füllen konnten, bevor die dritte die Arbeit machte.

Der Pool schlägt vor, die Datenbank entscheidet. Ein Snapshot ist Minuten alt, Moderation ist es nicht. Deshalb enthält er nur Kandidaten-IDs, und die Ziehung legt das volle anonyme Sichtbarkeits-Gate plus Blocks und Mutes des Lesers erneut an. Bezahlbar ist das genau deshalb, weil es auf ein paar hundert bekannte IDs begrenzt ist statt auf die posts-Tabelle. Veralteter Pool kann also einen leicht veralteten Vorschlag kosten, nie einen Post, den jemand nicht sehen darf.

Gemessen (Kopie der Produktionsdaten)

Ziehung Abfragen DB-Zeit
Leiter (bisher) 4,35 ms 13 8,59 ms
Snapshot (neu) 1,13 ms 11 2,03 ms

Dazu 3,2 ms alle zehn Minuten für alle Sprachen zusammen, einmal für die ganze Installation. Die Abfragezahl sinkt nur um zwei, weil der Rest Preloads sind; die drei Ranglisten-Scans sind durch eine ID-gebundene Abfrage ersetzt, und deren Kosten hängen nicht mehr an der Größe der posts-Tabelle.

Tests

test/vutuv/posts/popular_posts_test.exs, 24 Fälle. Der wichtigste Block heißt "a stale pool never leaks a hidden post" und hat einen Fall pro Art, wie sich die Welt nach dem Snapshot bewegen kann: Post eingefroren, Autor eingefroren / gesperrt / deaktiviert / unerreichbar, Post nachträglich beschränkt, Autor seither blockiert oder stumm gestellt. Dazu die Rückfälle (leerer Pool auf einer frischen Installation, Leser filtert den ganzen Pool weg, Snapshot antwortet nicht) und eine Zusicherung, dass auf dem Cache-Pfad kein Ranking-Scan mehr läuft.

Ein Fehler, den die Tests gefangen haben und der es wert ist, erwähnt zu werden: die erste Fassung mischte die Kandidaten über die ganze Menge statt innerhalb einer Stufe und warf damit still die Bevorzugung fremder Autoren weg. Jetzt wird pro Stufe gemischt, mit eigenem Test dafür.

Die bestehende discover_posts/2-Suite (25 Fälle) prüft weiter die Leiter: in Tests ist der Refresh-Timer aus, also läuft dort der :miss-Pfad. Beide Wege sind damit abgedeckt.

Deploy: hot, keine Migration. Neuer Prozess im Supervision-Tree und ein neuer Schalter :refresh_popular_posts (in Tests aus, wie bei den beiden Schwester-Caches).
Version: 7.231.3 (Patch: das Rail verhält sich gleich, es ist ein Refactor)
mix precommit: grün (6961 Tests).

Diesen Text hat ein KI-Agent in meinem Namen geschrieben, ungeprüft von mir. Die Arbeit dahinter ist meine, nur das Aufschreiben habe ich delegiert.

…(v7.231.3)

Die "Vorschläge"-Karte im Feed hat ihre Kandidaten pro Leser neu ermittelt,
und zwar bis zu dreimal: eine Leiter aus drei Stufen (fremde Autoren aus den
letzten 14 Tagen, dann fremde Autoren jeden Alters, dann alle außer mir
selbst aus 14 Tagen), jede Stufe ein eigener Scan über posts plus
Like-Rollup, gruppiert auf einen Post pro Autor.

Der teure Teil dieser Frage ist für jeden Leser derselbe: welche
öffentlichen Posts dieser Sprache kamen gut an, einer pro Autor, bester
zuerst. Persönlich ist nur die letzte Meile, also nicht meine eigenen Posts,
niemand, den ich stumm gestellt oder blockiert habe, und Leute, denen ich
noch nicht folge, vor denen, denen ich schon folge.

Der geteilte Teil wandert deshalb in einen Snapshot. Vutuv.Posts.PopularPosts
folgt genau dem Muster von Vutuv.Social.PopularUsers (der Schwesterkarte "Wem
folgen?" im selben Rail) und Vutuv.Posts.TopPosters: alle zehn Minuten eine
Rangliste pro konfigurierter Sprache in eine read_concurrency-ETS-Tabelle,
:miss fällt auf die bisherige Leiter zurück. Eine Ranglisten-Abfrage pro
Sprache für die ganze Installation, unabhängig davon, wie viele Leute lesen.
Das lohnt mehr, als eine Zahl pro Seitenaufruf vermuten lässt: das Rail
zeichnet sich alle fünf Minuten selbst neu, die alte Rechnung skalierte also
mit offenen Tabs, nicht mit Lesern.

Die drei Stufen werden dabei zu einer Sortierung, was sie immer waren: ein
CASE im ORDER BY der Ziehung statt drei Suchen. Nebenbei stimmt damit die
Richtung wieder. Stufe 1 und 2 schließen alle aus, denen der Leser folgt,
also bezahlten gut vernetzte Mitglieder bisher zwei Ziehungen, die die Karte
gar nicht füllen konnten, bevor die dritte die Arbeit machte.

Die Regel, auf der das Ganze ruht: der Pool schlägt vor, die Datenbank
entscheidet. Ein Snapshot ist Minuten alt, Moderation ist es nicht, also
enthält er nur Kandidaten-IDs, und die Ziehung legt das volle anonyme
Sichtbarkeits-Gate plus Blocks und Mutes des Lesers erneut an. Bezahlbar ist
das genau deshalb, weil es auf ein paar hundert bekannte IDs begrenzt ist
statt auf die posts-Tabelle. Ein veralteter Pool kann damit einen leicht
veralteten Vorschlag kosten, nie einen Post, den jemand nicht sehen darf.

Gemessen auf einer Kopie der Produktionsdaten: 4,35 ms und 8,59 ms DB-Zeit
für die Leiter, 1,13 ms und 2,03 ms für den Snapshot-Pfad, dazu 3,2 ms alle
zehn Minuten für alle Sprachen zusammen.

Der wichtigste Testblock heißt "a stale pool never leaks a hidden post" und
hat einen Fall pro Art, wie sich die Welt nach dem Snapshot bewegen kann.
Gefangen hat die Suite außerdem einen echten Fehler der ersten Fassung: sie
mischte die Kandidaten über die ganze Menge statt innerhalb einer Stufe und
warf damit still die Bevorzugung fremder Autoren weg.

Diesen Text hat ein KI-Agent in meinem Namen geschrieben, ungeprüft von mir. Die Arbeit dahinter ist meine, nur das Aufschreiben habe ich delegiert.
@wintermeyer
wintermeyer merged commit 7ff8796 into main Aug 7, 2026
1 of 5 checks passed
@wintermeyer
wintermeyer deleted the feed-discovery-pool branch August 7, 2026 08:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant