fix: FanoutTimelineServiceにリストが空の場合のみダミーIDを挿入するinjectDummyIfEmptyメソッドを追加しました。

This commit is contained in:
tai-cha 2026-01-27 15:19:06 +09:00
parent 33bd93ca40
commit 3fe1d27927
4 changed files with 101 additions and 5 deletions

View File

@ -231,7 +231,7 @@ export class FanoutTimelineEndpointService {
Promise.all(ps.redisTimelines.map((tl, i) => {
// 有効なソースかつ結果が空だった場合のみダミーを入れる
if (redisResult[i] && redisResult[i].length === 0) {
return this.fanoutTimelineService.injectDummy(tl, dummyId);
return this.fanoutTimelineService.injectDummyIfEmpty(tl, dummyId);
}
return Promise.resolve();
}));

View File

@ -109,10 +109,20 @@ export class FanoutTimelineService {
}
@bindThis
public injectDummy(tl: FanoutTimelineName, id: string) {
injectDummy(tl: FanoutTimelineName, id: string) {
return this.redisForTimelines.lpush('list:' + tl, id);
}
@bindThis
public injectDummyIfEmpty(tl: FanoutTimelineName, id: string): Promise<boolean> {
return this.redisForTimelines.eval(
'if redis.call("LLEN", KEYS[1]) == 0 then redis.call("LPUSH", KEYS[1], ARGV[1]) return 1 else return 0 end',
1,
'list:' + tl,
id,
).then(res => res === 1);
}
@bindThis
public purge(name: FanoutTimelineName) {
return this.redisForTimelines.del('list:' + name);

View File

@ -68,6 +68,7 @@ describe('FanoutTimelineEndpointService', () => {
.useValue({
getMulti: jest.fn(),
injectDummy: jest.fn(),
injectDummyIfEmpty: jest.fn(),
})
.compile();
@ -266,9 +267,9 @@ describe('FanoutTimelineEndpointService', () => {
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));
expect(fanoutTimelineService.injectDummyIfEmpty).toHaveBeenCalledTimes(2);
expect(fanoutTimelineService.injectDummyIfEmpty).toHaveBeenCalledWith(`homeTimeline:${alice.id}`, expect.any(String));
expect(fanoutTimelineService.injectDummyIfEmpty).toHaveBeenCalledWith('localTimeline', expect.any(String));
});
// Test for behavior when dummy ID exists

View File

@ -0,0 +1,85 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { describe, jest, test, expect, afterEach, beforeAll, afterAll } from '@jest/globals';
import { Test, TestingModule } from '@nestjs/testing';
import * as Redis from 'ioredis';
import { FanoutTimelineService } from '@/core/FanoutTimelineService.js';
import { IdService } from '@/core/IdService.js';
import { DI } from '@/di-symbols.js';
describe('FanoutTimelineService', () => {
let app: TestingModule;
let service: FanoutTimelineService;
let redisForTimelines: jest.Mocked<Redis.Redis>;
let idService: IdService;
beforeAll(async () => {
app = await Test.createTestingModule({
providers: [
FanoutTimelineService,
{
provide: IdService,
useValue: {
parse: jest.fn(),
gen: jest.fn(),
},
},
{
provide: DI.redisForTimelines,
useValue: {
eval: jest.fn(),
lpush: jest.fn(),
lrange: jest.fn(),
del: jest.fn(),
pipeline: jest.fn(() => ({
lpush: jest.fn(),
ltrim: jest.fn(),
lrange: jest.fn(),
exec: jest.fn(),
})),
},
},
],
}).compile();
app.enableShutdownHooks();
service = app.get<FanoutTimelineService>(FanoutTimelineService);
redisForTimelines = app.get(DI.redisForTimelines);
idService = app.get<IdService>(IdService);
});
afterAll(async () => {
await app.close();
});
afterEach(async () => {
jest.clearAllMocks();
});
test('injectDummyIfEmpty should call Redis EVAL with correct script', async () => {
redisForTimelines.eval.mockResolvedValue(1);
const result = await service.injectDummyIfEmpty('homeTimeline:123', 'dummyId');
expect(redisForTimelines.eval).toHaveBeenCalledWith(
expect.stringContaining('if redis.call("LLEN", KEYS[1]) == 0 then'),
1,
'list:homeTimeline:123',
'dummyId',
);
expect(result).toBe(true);
});
test('injectDummyIfEmpty should return false if list is not empty', async () => {
redisForTimelines.eval.mockResolvedValue(0);
const result = await service.injectDummyIfEmpty('homeTimeline:123', 'dummyId');
expect(redisForTimelines.eval).toHaveBeenCalled();
expect(result).toBe(false);
});
});