From 33bd93ca400fa4b6914724f7f3e953479ec62bdd Mon Sep 17 00:00:00 2001 From: tai-cha Date: Tue, 27 Jan 2026 17:31:15 +0900 Subject: [PATCH] =?UTF-8?q?Revert=20"Revert=20"perf:=20=E7=A9=BA=E3=81=AE?= =?UTF-8?q?=E3=82=BF=E3=82=A4=E3=83=A0=E3=83=A9=E3=82=A4=E3=83=B3=E8=AA=AD?= =?UTF-8?q?=E3=81=BF=E8=BE=BC=E3=81=BF=E6=99=82=E3=81=AE=E7=84=A1=E9=A7=84?= =?UTF-8?q?=E3=81=AADB=E3=82=A2=E3=82=AF=E3=82=BB=E3=82=B9=E3=82=92?= =?UTF-8?q?=E5=89=8A=E6=B8=9B=E3=81=99=E3=82=8B=E3=81=9F=E3=82=81=E3=80=81?= =?UTF-8?q?Redis=E3=82=BF=E3=82=A4=E3=83=A0=E3=83=A9=E3=82=A4=E3=83=B3?= =?UTF-8?q?=E3=81=AB=E3=83=80=E3=83=9F=E3=83=BCID=E3=82=92=E6=8C=BF?= =?UTF-8?q?=E5=85=A5=E3=81=99=E3=82=8B=E6=A9=9F=E8=83=BD=E3=82=92=E8=BF=BD?= =?UTF-8?q?=E5=8A=A0=E3=81=97=E3=81=BE=E3=81=97=E3=81=9F=E3=80=82""?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit b99ed65051fa7fcade5108181f801c63e8e96493. --- .../src/core/FanoutTimelineEndpointService.ts | 22 +++++- .../backend/src/core/FanoutTimelineService.ts | 5 ++ .../unit/FanoutTimelineEndpointService.ts | 75 +++++++++++++++++-- 3 files changed, 94 insertions(+), 8 deletions(-) diff --git a/packages/backend/src/core/FanoutTimelineEndpointService.ts b/packages/backend/src/core/FanoutTimelineEndpointService.ts index 2d6b7eab06..c3c7ead201 100644 --- a/packages/backend/src/core/FanoutTimelineEndpointService.ts +++ b/packages/backend/src/core/FanoutTimelineEndpointService.ts @@ -14,6 +14,7 @@ import type { NotesRepository } from '@/models/_.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { FanoutTimelineName, FanoutTimelineService } from '@/core/FanoutTimelineService.js'; import { UtilityService } from '@/core/UtilityService.js'; +import { IdService } from '@/core/IdService.js'; import { isUserRelated } from '@/misc/is-user-related.js'; import { isQuote, isRenote } from '@/misc/is-renote.js'; import { CacheService } from '@/core/CacheService.js'; @@ -60,6 +61,7 @@ export class FanoutTimelineEndpointService { private fanoutTimelineService: FanoutTimelineService, private utilityService: UtilityService, private channelMutingService: ChannelMutingService, + private idService: IdService, ) { } @@ -217,7 +219,25 @@ export class FanoutTimelineEndpointService { return [...redisTimeline, ...gotFromDb]; } - return await ps.dbFallback(ps.untilId, ps.sinceId, ps.limit); + // RedisおよびDBが空の場合、次回以降の無駄なDBアクセスを防ぐためダミーIDを保存する + const gotFromDb = await ps.dbFallback(ps.untilId, ps.sinceId, ps.limit); + if ( + redisResultIds.length === 0 && + ps.sinceId == null && ps.untilId == null && + gotFromDb.length === 0 + ) { + const dummyId = this.idService.gen(); + + Promise.all(ps.redisTimelines.map((tl, i) => { + // 有効なソースかつ結果が空だった場合のみダミーを入れる + if (redisResult[i] && redisResult[i].length === 0) { + return this.fanoutTimelineService.injectDummy(tl, dummyId); + } + return Promise.resolve(); + })); + } + + return gotFromDb; } private async getAndFilterFromDb(noteIds: string[], noteFilter: NoteFilter, idCompare: (a: string, b: string) => number): Promise { diff --git a/packages/backend/src/core/FanoutTimelineService.ts b/packages/backend/src/core/FanoutTimelineService.ts index 24999bf4da..619fd7a89d 100644 --- a/packages/backend/src/core/FanoutTimelineService.ts +++ b/packages/backend/src/core/FanoutTimelineService.ts @@ -108,6 +108,11 @@ export class FanoutTimelineService { }); } + @bindThis + public injectDummy(tl: FanoutTimelineName, id: string) { + return this.redisForTimelines.lpush('list:' + tl, id); + } + @bindThis public purge(name: FanoutTimelineName) { return this.redisForTimelines.del('list:' + name); diff --git a/packages/backend/test/unit/FanoutTimelineEndpointService.ts b/packages/backend/test/unit/FanoutTimelineEndpointService.ts index 375bf5a97a..1dc605648f 100644 --- a/packages/backend/test/unit/FanoutTimelineEndpointService.ts +++ b/packages/backend/test/unit/FanoutTimelineEndpointService.ts @@ -67,6 +67,7 @@ describe('FanoutTimelineEndpointService', () => { .overrideProvider(FanoutTimelineService) .useValue({ getMulti: jest.fn(), + injectDummy: jest.fn(), }) .compile(); @@ -119,7 +120,7 @@ describe('FanoutTimelineEndpointService', () => { const dbFallback = jest.fn((_untilId: string | null, _sinceId: string | null, _limit: number) => Promise.resolve([] as MiNote[])); const ps = { - redisTimelines: ['homeTimeline', 'localTimeline'] as FanoutTimelineName[], + redisTimelines: [`homeTimeline:${alice.id}`, 'localTimeline'] as FanoutTimelineName[], useDbFallback: true, limit: 10, allowPartial: false, @@ -130,9 +131,6 @@ describe('FanoutTimelineEndpointService', () => { sinceId: null, }; - // See comments in original file for logic explanation. - // Essentially, we expect the fallback to start from the end of the most recent reliable timeline (HTL). - await service.getMiNotes(ps); expect(dbFallback).toHaveBeenCalled(); @@ -173,7 +171,7 @@ describe('FanoutTimelineEndpointService', () => { const result = await service.getMiNotes(ps); // With the fix, we should get note1 and note2. - // Without the fix, we would get only note3 (or empty if limit blocked it). + expect(result).toHaveLength(2); expect(result[0].id).toBe(note1.id); expect(result[1].id).toBe(note2.id); @@ -224,7 +222,7 @@ describe('FanoutTimelineEndpointService', () => { const dbFallback = jest.fn((untilId: string | null, sinceId: string | null, limit: number) => Promise.resolve([] as MiNote[])); const ps = { - redisTimelines: ['homeTimeline', 'localTimeline'] as FanoutTimelineName[], + redisTimelines: [`homeTimeline:${alice.id}`, 'localTimeline'] as FanoutTimelineName[], useDbFallback: false, limit: 10, allowPartial: true, @@ -237,9 +235,72 @@ describe('FanoutTimelineEndpointService', () => { const result = await service.getMiNotes(ps); - // With the previous logic, note3 and note4 would be filtered out because they are older than the "threshold" (end of TL1). // With the fixed logic (skipping filter when !useDbFallback), all notes should be present. expect(result).toHaveLength(4); expect(result.map(n => n.id)).toEqual([note1.id, note2.id, note3.id, note4.id]); }); + + // Test for dummy ID optimization + test('should inject dummy ID when DB fallback returns empty on initial load', async () => { + const redisResult: string[][] = [[], []]; // Empty timelines + + fanoutTimelineService.getMulti.mockResolvedValue(redisResult); + + // Mock dbFallback to return empty array + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const dbFallback = jest.fn((_untilId: string | null, _sinceId: string | null, _limit: number) => Promise.resolve([] as MiNote[])); + + const ps = { + redisTimelines: [`homeTimeline:${alice.id}`, 'localTimeline'] as FanoutTimelineName[], + useDbFallback: true, + limit: 10, + allowPartial: true, + excludePureRenotes: false, + dbFallback, + noteFilter: () => true, + untilId: null, + sinceId: null, + }; + + const result = await service.getMiNotes(ps); + + expect(result).toEqual([]); + // Should have tried to inject dummy ID for both empty timelines + expect(fanoutTimelineService.injectDummy).toHaveBeenCalledTimes(2); + expect(fanoutTimelineService.injectDummy).toHaveBeenCalledWith(`homeTimeline:${alice.id}`, expect.any(String)); + expect(fanoutTimelineService.injectDummy).toHaveBeenCalledWith('localTimeline', expect.any(String)); + }); + + // Test for behavior when dummy ID exists + test('should return empty result when only dummy ID exists in Redis and DB has no newer data', async () => { + const now = Date.now(); + const dummyId = idService.gen(now); + // Redis has only dummy ID + const redisResult: string[][] = [[dummyId]]; + + fanoutTimelineService.getMulti.mockResolvedValue(redisResult); + + // Mock dbFallback (should be called to check for newer notes than the dummy ID) + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const dbFallback = jest.fn((_untilId: string | null, _sinceId: string | null, _limit: number) => Promise.resolve([] as MiNote[])); + + const ps = { + redisTimelines: [`homeTimeline:${alice.id}`] as FanoutTimelineName[], + useDbFallback: true, + limit: 10, + allowPartial: false, + excludePureRenotes: false, + dbFallback, + noteFilter: () => true, + untilId: null, + sinceId: null, + }; + + const result = await service.getMiNotes(ps); + + expect(result).toEqual([]); + // Fallback should be called to check for newer notes (ascending check from dummy ID) + expect(dbFallback).toHaveBeenCalled(); + }); });