Skip to content

fix(backend): enableFanoutTimeline トグル時のタイムライン取りこぼし問題を修正#17548

Draft
fruitriin wants to merge 3 commits into
misskey-dev:developfrom
fruitriin:fix/fanout-timeline-toggle-purge
Draft

fix(backend): enableFanoutTimeline トグル時のタイムライン取りこぼし問題を修正#17548
fruitriin wants to merge 3 commits into
misskey-dev:developfrom
fruitriin:fix/fanout-timeline-toggle-purge

Conversation

@fruitriin
Copy link
Copy Markdown
Contributor

@fruitriin fruitriin commented Jun 5, 2026

What

管理画面で enableFanoutTimelineON → OFF → ON のように切り替えると、 OFF 期間中の投稿が Redis 上のキャッシュリスト (list:homeTimeline:* 等) に乗らないまま、 ON 復帰後の新規投稿だけが Redis 先頭に追加される。 これにより Redis 上の時系列にギャップが生じ、 タイムライン API のレスポンスから OFF 期間の投稿が永久に取りこぼされていた。

admin/update-metaenableFanoutTimeline の値が変化したとき、 FanoutTimelineService.purgeAll で Redis 上の list:* キーを一括削除するように変更した。

削除後の Redis は空になるが、 タイムライン取得側 (FanoutTimelineEndpointService) は noteIds.length === 0 を検知して shouldFallbackToDb=true 経路で DB に直行するため、 結果として DB から自然に再構築 される。 そのあとの新規投稿は通常通り Redis に積み上がる。

追加した API

FanoutTimelineService に以下のメソッドを追加:

@bindThis
public async purgeAll(): Promise<number> {
		let cursor = '0';
		let totalDeleted = 0;
		do {
				const [next, keys] = await this.redisForTimelines.scan(cursor, 'MATCH', this.redisForTimelines.options.keyPrefix + 'list:*', 'COUNT', 100);
				cursor = next;
				if (keys.length === 0) continue;
				const prefix = this.redisForTimelines.options.keyPrefix ?? '';
				const stripped = prefix !== '' ? keys.map(k => k.startsWith(prefix) ? k.slice(prefix.length) : k) : keys;
				await this.redisForTimelines.del(...stripped);
				totalDeleted += stripped.length;
		} while (cursor !== '0');
		return totalDeleted;
}

SCAN でカーソル回しているため、 リスト数が多くても Redis サーバーをブロックしない。

admin/update-meta の変更点

const before = await this.metaService.fetch(true);
await this.metaService.update(set);
const after = await this.metaService.fetch(true);

// enableFanoutTimeline を切り替えた場合、Redis 上のキャッシュリストが DB と
// 整合しない状態 (OFF 期間中に投稿されたノートが Redis に乗っていない等) になり得る。
// 取得側 (FanoutTimelineEndpointService) はそのギャップを完全には埋められないため、
// トグル時に Redis 上の list:* キーを一括削除して、次回以降の取得で
// shouldFallbackToDb=true 経由で DB から自然に再構築されるようにする。
if (before.enableFanoutTimeline !== after.enableFanoutTimeline) {
		await this.fanoutTimelineService.purgeAll();
}

Why

enableFanoutTimeline のトグル操作は管理画面から 1 クリックでできる 通常の運用操作 (FTT の挙動調査、 障害対応、 一時メンテ等で行われる)。 現状の実装では、 このトグル前後に投稿された OFF 期間中のノート が Redis 上のキャッシュリストに乗らず、 ON 復帰後の新規投稿との間に Redis 上で時系列ギャップが生じる。

そして取得側の FanoutTimelineEndpointService の最終 DB フォールバックは、 DB クエリの境界として Redis 上の最古 ID を使うため、 中間ギャップに該当するノートを補完できない (ギャップ範囲が DB クエリの対象外になる)。 結果、 ユーザーから「過去のノートが TL に表示されない」 「ページネーションで深掘りしても出てこない」 という障害が起きる。

本 PR はトグル時に Redis をパージすることで、 Redis 上にギャップが残らないようにし、 取得側のロジックを変更せず根本的に解消する。

詳細・再現手順・関連 PR は Issue #fruitriin#38 参照。

Additional info (optional)

Tests added

packages/backend/test/e2e/timelines.ts に新 describe ブロック FTT toggle purge を追加 (2 ケース):

  1. HTL: ON → 5 件投稿 → OFF → 5 件投稿 → ON → 5 件投稿 した後、 notes/timeline で全 15 件が返ることを確認
  2. Channel TL: 同様のシナリオで channels/timeline を検証

ローカル動作確認

ローカル dev サーバーに修正を当てて手動で実証:

  1. 既存の Redis FTT (alice の HTL 13 件、 channel 13 件、 list:* 全 19 個) がある状態
  2. admin/update-meta { enableFanoutTimeline: false } を叩く
  3. Redis: list:* が 19 → 0 で全削除されたことを確認 ✓
  4. admin/update-meta { enableFanoutTimeline: true } で ON に戻す
  5. notes/timeline limit=20 → DB から 20 件正しく取得 ✓
  6. channels/timeline limit=20 → DB から 20 件正しく取得 ✓
  7. 新規ノート投稿 → Redis に再度 lpush されることを確認 ✓

Trade-off

  • パージ直後の性能: トグル直後は Redis FTT が空になるため、 次のリロードは全 user/channel で DB 直行になる。 その後の新規投稿で Redis が自然に再構築される。 大規模インスタンスでは一時的にタイムライン取得が DB ヒット中心になる点を許容する判断。
  • OFF → ON だけでなく ON → OFF でもパージ: ON → OFF 方向もパージするが、 OFF 期間中は FTT を使わないため副作用なし。 むしろ将来の ON 復帰時の状態をクリーンにする利点がある。
  • SCAN の範囲: list:* で全 FTT キーを対象にする。 アンテナタイムライン (list:antennaTimeline:*) も含まれるが、 アンテナ TL endpoint は別実装で Redis 状態の影響を受ける挙動を持つため、 整合性向上の観点でこれも初期化する方が安全と判断。

Affected endpoints / services

Related

PR #17549 と本 PR は独立に merge 可能。 両方入れば「トグル時に Redis をパージ」 (本 PR) + 「万一 Redis ギャップが残っても取得側で補完」 の二重防御になる。

Checklist

  • Read the contribution guide
  • Test working in a local environment
  • (If needed) Update CHANGELOG.md
  • (If possible) Add tests

トグル直後に Redis 上の list:* キャッシュと DB が不整合になりノートが取りこぼされる問題を、過渡期フラグ `fanoutTimelineActive` の導入で解消する。トグルされた瞬間に active=false にしてデータプレーンを FTTL から切り離し、BullMQ ジョブで Redis を purge してから active=true に書き戻す。過渡期中の `enableFanoutTimeline` 再変更は 409 (FANOUT_TIMELINE_TRANSITION_IN_PROGRESS) で拒否し、管理画面の UI も同期して disable する。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@codecov
Copy link
Copy Markdown

codecov Bot commented Jun 5, 2026

Codecov Report

❌ Patch coverage is 4.16667% with 46 lines in your changes missing coverage. Please review.
✅ Project coverage is 22.63%. Comparing base (e2bcd9c) to head (f647329).
⚠️ Report is 5 commits behind head on develop.

Files with missing lines Patch % Lines
...processors/PurgeFanoutTimelinesProcessorService.ts 0.00% 13 Missing ⚠️
packages/backend/src/core/FanoutTimelineService.ts 0.00% 9 Missing and 3 partials ⚠️
...kend/src/server/api/endpoints/admin/update-meta.ts 0.00% 6 Missing and 4 partials ⚠️
packages/backend/src/core/QueueService.ts 0.00% 2 Missing ⚠️
...ackages/backend/src/queue/QueueProcessorService.ts 0.00% 2 Missing ⚠️
...ges/backend/src/server/ActivityPubServerService.ts 0.00% 0 Missing and 1 partial ⚠️
...kend/src/server/api/endpoints/channels/timeline.ts 0.00% 0 Missing and 1 partial ⚠️
.../src/server/api/endpoints/notes/hybrid-timeline.ts 0.00% 0 Missing and 1 partial ⚠️
...d/src/server/api/endpoints/notes/local-timeline.ts 0.00% 0 Missing and 1 partial ⚠️
...backend/src/server/api/endpoints/notes/timeline.ts 0.00% 0 Missing and 1 partial ⚠️
... and 2 more
Additional details and impacted files
@@             Coverage Diff             @@
##           develop   #17548      +/-   ##
===========================================
+ Coverage    15.14%   22.63%   +7.49%     
===========================================
  Files          248     1164     +916     
  Lines        12412    39879   +27467     
  Branches      4214    11082    +6868     
===========================================
+ Hits          1880     9028    +7148     
- Misses        8241    24773   +16532     
- Partials      2291     6078    +3787     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 5, 2026

このPRによるapi.jsonの差分

差分はこちら
--- base
+++ head
@@ -9902,6 +9902,9 @@
                     "enableFanoutTimeline": {
                       "type": "boolean"
                     },
+                    "fanoutTimelineActive": {
+                      "type": "boolean"
+                    },
                     "enableFanoutTimelineDbFallback": {
                       "type": "boolean"
                     },
@@ -10215,6 +10218,7 @@
                     "manifestJsonOverride",
                     "policies",
                     "enableFanoutTimeline",
+                    "fanoutTimelineActive",
                     "enableFanoutTimelineDbFallback",
                     "perLocalUserUserTimelineCacheMax",
                     "perRemoteUserUserTimelineCacheMax",
@@ -19836,6 +19840,16 @@
                   "$ref": "#/components/schemas/Error"
                 },
                 "examples": {
+                  "FANOUT_TIMELINE_TRANSITION_IN_PROGRESS": {
+                    "value": {
+                      "error": {
+                        "message": "A fanout timeline toggle is currently being applied. Please wait until the purge job completes before changing enableFanoutTimeline again.",
+                        "code": "FANOUT_TIMELINE_TRANSITION_IN_PROGRESS",
+                        "id": "65b9b26a-700c-41ea-96f7-96b9a7902301",
+                        "httpStatusCode": 409
+                      }
+                    }
+                  },
                   "INVALID_PARAM": {
                     "value": {
                       "error": {

Get diff files from Workflow Page

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 5, 2026

Backend memory usage comparison

Before GC

Metric base (MB) head (MB) Diff (MB) Diff (%)
VmRSS 307.39 MB 306.60 MB -0.78 MB -0.25%
VmHWM 307.39 MB 306.60 MB -0.78 MB -0.25%
VmSize 23172.12 MB 23172.70 MB 0.57 MB 0%
VmData 1372.66 MB 1372.90 MB +0.23 MB +0.01%

After GC

Metric base (MB) head (MB) Diff (MB) Diff (%)
VmRSS 307.39 MB 306.62 MB -0.77 MB -0.25%
VmHWM 307.39 MB 306.62 MB -0.77 MB -0.25%
VmSize 23172.12 MB 23172.79 MB 0.66 MB 0%
VmData 1372.66 MB 1372.98 MB +0.32 MB +0.02%

After Request

Metric base (MB) head (MB) Diff (MB) Diff (%)
VmRSS 308.01 MB 306.83 MB -1.17 MB -0.38%
VmHWM 308.01 MB 306.83 MB -1.17 MB -0.38%
VmSize 23172.29 MB 23172.79 MB 0.49 MB 0%
VmData 1372.83 MB 1372.98 MB +0.15 MB +0.01%

See workflow logs for details

fruitriin and others added 2 commits June 5, 2026 15:39
3 点の修正:
1. update-meta で `ps.enableFanoutTimeline` が現状と同値のときに `set` が空になり、`metaService.update({})` が空 SET の UPDATE を発行して 500 を返していた。`set.enableFanoutTimeline = ps.enableFanoutTimeline` を常に入れて回避。
2. 過渡期判定を「`!fanoutTimelineActive`」から「`enableFanoutTimeline !== fanoutTimelineActive` (= 乖離している状態)」に変更。stable な OFF/OFF からの ON 化が拒否されなくなる。frontend のトグル disable 判定も同じ条件に揃える。
3. test-server エントリでは BullMQ ワーカーが未起動なので、`FTT toggle purge` describe で `startJobQueue()` を beforeAll で起動して purge ジョブを完走させる。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ion を防ぐ

`moderationLogService.log` は内部で `await insert()` する async メソッドだが、admin/update-meta では fire-and-forget で呼ばれていた。INSERT が FK 違反等で失敗すると Unhandled Promise Rejection になり、E2E テストランナー (vitest) が落ちる。FTT トグル追加で update-meta の呼び出し回数が増え、in-flight log が cleanup と衝突して CI が失敗していたため、ここで await する。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

packages/backend:test packages/backend Server side specific issue/PR packages/frontend Client side specific issue/PR packages/misskey-js

Projects

Development

Successfully merging this pull request may close these issues.

1 participant