fix(backend): FTTL に歯抜けがあるときタイムラインAPIがDBのノートを取りこぼす問題を修正#17549
Draft
fruitriin wants to merge 3 commits into
Draft
fix(backend): FTTL に歯抜けがあるときタイムラインAPIがDBのノートを取りこぼす問題を修正#17549fruitriin wants to merge 3 commits into
fruitriin wants to merge 3 commits into
Conversation
FanoutTimelineEndpointService の最終 dbFallback が Redis 由来の最古/最新 ID を境界に 使っていたため、Redis 上に飛び石の歯抜け (3分ガード拒否, TTL evict, LREM 等) があると その歯抜け範囲のノートが DB クエリにも含まれず取りこぼす問題があった。 ps.untilId / ps.sinceId の全範囲を DB に問い合わせ、Redis 由来と id で重複排除して 再ソート + slice する方式に変更する。Redis 満杯 + フィルタ全通過のホットパスでは 既存の早期 return が効くため追加 DB クエリは発生しない。fail-safe 層として機能する。 回帰テストを e2e に 3 本追加 (HTL / Channel TL / Channel TL pagination)。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 tasks
Contributor
|
このPRによるapi.jsonの差分 |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## develop #17549 +/- ##
===========================================
+ Coverage 15.14% 22.40% +7.25%
===========================================
Files 248 1159 +911
Lines 12412 39593 +27181
Branches 4214 11033 +6819
===========================================
+ Hits 1880 8870 +6990
- Misses 8241 24666 +16425
- Partials 2291 6057 +3766 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Member
Contributor
Backend memory usage comparisonBefore GC
After GC
After Request
|
1 task
Contributor
Author
|
原因が全く異なる模様 |
Contributor
Author
前コミットの最終 dbFallback 全範囲化だけでは、Redis ループが「歯抜け補完のため Redis 内の古い ID を消費してフィルタ後 limit 件を満たし early return」してしまい、 返却の最後の ID が本来の上位 limit 範囲より古い側に下がる。結果、次ページ以降で 本来あるべき範囲がスキップされ、ページネーションで取りこぼしが残っていた。 redisResultIds.length >= ps.limit (= 上位 limit 件範囲内に歯抜けなしと見なせる) の場合のみ early return を許可し、歯抜けまたはキャッシュ未飽和時はループを抜けて 最終 dbFallback の全範囲取得 + dedupe + slice で上位 limit 件を再構築する。 これにより e2e の Channel TL ページネーションテストが通る。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
前々コミット (歯抜け時の最終 dbFallback 全範囲化) と前コミット (early return への hasFullRedisCache 追加) では以下 2 件が解消していなかった: 1. ユーザー TL の withReplies:false 等で「他人への返信」が含まれる regression 原因: 全範囲化した dbFallback の結果 (gotFromDb) を filter を通さず merge していたため、Redis 経由でフィルタされていたノートが DB から無フィルタで 結果に紛れ込んでいた。 2. ページネーションテストで歯抜け範囲が永久にスキップされる 原因: Redis 上位 limit 件範囲内の歯抜けは Redis 内データだけからは判別不能 (歯抜けの ID は最初から Redis に存在しないため、検出シグナルがない)。 redisResultIds.length >= ps.limit による近似判定では、Redis 自身が 歯抜けを内包したまま「上位 limit 件揃った」と誤判定して early return し、 次ページの境界が本来より古い側に下がっていた。 対応: - allowPartial ケースを除き、ループ内の early return を廃止し、常に最終 dbFallback (全範囲 + dedupe + sort + slice) に進ませる。これで上位 limit 件 が常に正しく再構築され、ページネーション境界も正しい位置になる。 - gotFromDb にも filter を適用し、Redis 経由のフィルタ (excludeReplies, excludeNoFiles, ミュート/ブロック等) を DB 由来のノートにも揃える。 性能: 毎リクエストで dbFallback が 1 回追加されるが、id < untilId ORDER BY id DESC LIMIT n は B-tree index で高速なので fail-safe 層の代償としては許容範囲。 派生PR (3分ガード等) で歯抜けがなくなれば、Redis 結果と DB 結果は一致し dedupe で吸収される。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
FanoutTimelineEndpointServiceの最終 DB フォールバック処理が、 DB クエリの境界として Redis 結果の最古 ID を使用していたため、 Redis 上のキャッシュリストの中間にギャップがある (= 本来そこに存在するはずのノート ID が抜けた状態) と、 そのギャップ範囲のノートが DB に存在しても永久に取得不能になっていた。ps.untilId/ps.sinceIdの 全範囲 を DB に問い合わせ、 Redis 由来と重複排除した上で再ソートする方式に変更する。Before
After
Why
enableFanoutTimelineを管理画面から ON → OFF → ON のように切り替える運用 (FTT の挙動調査、 一時的なメンテ、 障害対応等で一般に行われる) を経ると、 OFF 期間中の投稿が Redis 上のキャッシュリストに乗らないまま ON 復帰後の新規投稿だけが追加される状態になる。その結果として Redis 上に中間ギャップが生じ、 現状の最終 DB フォールバックの境界式 (Redis 最古 ID を
dbUntilに使う) では、 そのギャップ範囲のノートが DB クエリにも含まれず、 タイムライン API のレスポンスから永久に欠落する。詳細・再現手順 (UI 操作)・原因・用語解説は Issue #XXXXX 参照。
Additional info (optional)
Tests added
packages/backend/test/e2e/timelines.tsに新 describe ブロックFTT Redis gap fallbackを追加 (3 ケース):homeTimelineRedis リスト 20 件中 中間 7 件をLREMで削除して中間ギャップを作り、notes/timeline(limit=20) で DB の全 20 件が返ることを確認channelTimelineのギャップでchannels/timeline(limit=20) を検証修正前後の検証結果
untilId未指定 (limit=20)untilId未指定 (limit=20)Trade-off
最終フォールバックパスで DB クエリの limit が
remainingToReadからps.limitに増える分のコスト。 ただし元コードでも最終フォールバックで DB を叩いており、 追加コストは限定的。 正しさを保証する代償として妥当と判断。useDbFallback=falseの場合はps.dbFallback = () => Promise.resolve([])に置換されるため (FanoutTimelineEndpointService.ts:73)、 修正後も追加 DB クエリは発生しない (既存運用設定を尊重)。Affected endpoints
FanoutTimelineEndpointService.timelineを呼ぶ全 endpoint:notes/timeline(HTL)notes/local-timeline(LTL)notes/hybrid-timelinechannels/timelinenotes/user-list-timelineなお
antennas/notes等はFanoutTimelineEndpointServiceを経由せず独自実装なので、 本 PR の修正は影響しません。Related
Checklist