Skip to content

Commit

Permalink
Fix: チャンネルのフォロー・アンフォローの反映速度を改善 (misskey-dev#12149)
Browse files Browse the repository at this point in the history
* チャンネルのフォロー・アンフォローの反映速度を改善

* fix lint

* userFollowingChannelsCacheの場所をCacheServiceからChannelFollowingServiceに移動

---------

Co-authored-by: osamu <46447427+sam-osamu@users.noreply.github.com>
  • Loading branch information
2 people authored and kanarikanaru committed Nov 9, 2023
1 parent d15ce4a commit 17ca35a
Show file tree
Hide file tree
Showing 7 changed files with 126 additions and 35 deletions.
15 changes: 2 additions & 13 deletions packages/backend/src/core/CacheService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import { Inject, Injectable } from '@nestjs/common';
import * as Redis from 'ioredis';
import type { BlockingsRepository, ChannelFollowingsRepository, FollowingsRepository, MutingsRepository, RenoteMutingsRepository, MiUserProfile, UserProfilesRepository, UsersRepository } from '@/models/_.js';
import type { BlockingsRepository, FollowingsRepository, MutingsRepository, RenoteMutingsRepository, MiUserProfile, UserProfilesRepository, UsersRepository } from '@/models/_.js';
import { MemoryKVCache, RedisKVCache } from '@/misc/cache.js';
import type { MiLocalUser, MiUser } from '@/models/User.js';
import { DI } from '@/di-symbols.js';
Expand All @@ -26,7 +26,6 @@ export class CacheService implements OnApplicationShutdown {
public userBlockedCache: RedisKVCache<Set<string>>; // NOTE: 「被」Blockキャッシュ
public renoteMutingsCache: RedisKVCache<Set<string>>;
public userFollowingsCache: RedisKVCache<Set<string>>;
public userFollowingChannelsCache: RedisKVCache<Set<string>>;

constructor(
@Inject(DI.redis)
Expand All @@ -53,9 +52,6 @@ export class CacheService implements OnApplicationShutdown {
@Inject(DI.followingsRepository)
private followingsRepository: FollowingsRepository,

@Inject(DI.channelFollowingsRepository)
private channelFollowingsRepository: ChannelFollowingsRepository,

private userEntityService: UserEntityService,
) {
//this.onMessage = this.onMessage.bind(this);
Expand Down Expand Up @@ -144,13 +140,7 @@ export class CacheService implements OnApplicationShutdown {
fromRedisConverter: (value) => new Set(JSON.parse(value)),
});

this.userFollowingChannelsCache = new RedisKVCache<Set<string>>(this.redisClient, 'userFollowingChannels', {
lifetime: 1000 * 60 * 30, // 30m
memoryCacheLifetime: 1000 * 60, // 1m
fetcher: (key) => this.channelFollowingsRepository.find({ where: { followerId: key }, select: ['followeeId'] }).then(xs => new Set(xs.map(x => x.followeeId))),
toRedisConverter: (value) => JSON.stringify(Array.from(value)),
fromRedisConverter: (value) => new Set(JSON.parse(value)),
});
// NOTE: チャンネルのフォロー状況キャッシュはChannelFollowingServiceで行っている

this.redisForSub.on('message', this.onMessage);
}
Expand Down Expand Up @@ -214,7 +204,6 @@ export class CacheService implements OnApplicationShutdown {
this.userBlockedCache.dispose();
this.renoteMutingsCache.dispose();
this.userFollowingsCache.dispose();
this.userFollowingChannelsCache.dispose();
}

@bindThis
Expand Down
104 changes: 104 additions & 0 deletions packages/backend/src/core/ChannelFollowingService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { Inject, Injectable, OnModuleInit } from '@nestjs/common';
import Redis from 'ioredis';
import { DI } from '@/di-symbols.js';
import type { ChannelFollowingsRepository } from '@/models/_.js';
import { MiChannel } from '@/models/_.js';
import { IdService } from '@/core/IdService.js';
import { GlobalEvents, GlobalEventService } from '@/core/GlobalEventService.js';
import { bindThis } from '@/decorators.js';
import type { MiLocalUser } from '@/models/User.js';
import { RedisKVCache } from '@/misc/cache.js';

@Injectable()
export class ChannelFollowingService implements OnModuleInit {
public userFollowingChannelsCache: RedisKVCache<Set<string>>;

constructor(
@Inject(DI.redis)
private redisClient: Redis.Redis,
@Inject(DI.redisForSub)
private redisForSub: Redis.Redis,
@Inject(DI.channelFollowingsRepository)
private channelFollowingsRepository: ChannelFollowingsRepository,
private idService: IdService,
private globalEventService: GlobalEventService,
) {
this.userFollowingChannelsCache = new RedisKVCache<Set<string>>(this.redisClient, 'userFollowingChannels', {
lifetime: 1000 * 60 * 30, // 30m
memoryCacheLifetime: 1000 * 60, // 1m
fetcher: (key) => this.channelFollowingsRepository.find({
where: { followerId: key },
select: ['followeeId'],
}).then(xs => new Set(xs.map(x => x.followeeId))),
toRedisConverter: (value) => JSON.stringify(Array.from(value)),
fromRedisConverter: (value) => new Set(JSON.parse(value)),
});

this.redisForSub.on('message', this.onMessage);
}

onModuleInit() {
}

@bindThis
public async follow(
requestUser: MiLocalUser,
targetChannel: MiChannel,
): Promise<void> {
await this.channelFollowingsRepository.insert({
id: this.idService.gen(),
followerId: requestUser.id,
followeeId: targetChannel.id,
});

this.globalEventService.publishInternalEvent('followChannel', {
userId: requestUser.id,
channelId: targetChannel.id,
});
}

@bindThis
public async unfollow(
requestUser: MiLocalUser,
targetChannel: MiChannel,
): Promise<void> {
await this.channelFollowingsRepository.delete({
followerId: requestUser.id,
followeeId: targetChannel.id,
});

this.globalEventService.publishInternalEvent('unfollowChannel', {
userId: requestUser.id,
channelId: targetChannel.id,
});
}

@bindThis
private async onMessage(_: string, data: string): Promise<void> {
const obj = JSON.parse(data);

if (obj.channel === 'internal') {
const { type, body } = obj.message as GlobalEvents['internal']['payload'];
switch (type) {
case 'followChannel': {
this.userFollowingChannelsCache.refresh(body.userId);
break;
}
case 'unfollowChannel': {
this.userFollowingChannelsCache.delete(body.userId);
break;
}
}
}
}

@bindThis
public dispose(): void {
this.userFollowingChannelsCache.dispose();
}

@bindThis
public onApplicationShutdown(signal?: string | undefined): void {
this.dispose();
}
}
6 changes: 6 additions & 0 deletions packages/backend/src/core/CoreModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ import { UtilityService } from './UtilityService.js';
import { FileInfoService } from './FileInfoService.js';
import { SearchService } from './SearchService.js';
import { ClipService } from './ClipService.js';
import { ChannelFollowingService } from './ChannelFollowingService.js';
import { ChartLoggerService } from './chart/ChartLoggerService.js';
import FederationChart from './chart/charts/federation.js';
import NotesChart from './chart/charts/notes.js';
Expand Down Expand Up @@ -187,6 +188,7 @@ const $UtilityService: Provider = { provide: 'UtilityService', useExisting: Util
const $FileInfoService: Provider = { provide: 'FileInfoService', useExisting: FileInfoService };
const $SearchService: Provider = { provide: 'SearchService', useExisting: SearchService };
const $ClipService: Provider = { provide: 'ClipService', useExisting: ClipService };
const $ChannelFollowingService: Provider = { provide: 'ChannelFollowingService', useExisting: ChannelFollowingService };

const $ChartLoggerService: Provider = { provide: 'ChartLoggerService', useExisting: ChartLoggerService };
const $FederationChart: Provider = { provide: 'FederationChart', useExisting: FederationChart };
Expand Down Expand Up @@ -318,6 +320,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
FileInfoService,
SearchService,
ClipService,
ChannelFollowingService,
ChartLoggerService,
FederationChart,
NotesChart,
Expand Down Expand Up @@ -442,6 +445,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$FileInfoService,
$SearchService,
$ClipService,
$ChannelFollowingService,
$ChartLoggerService,
$FederationChart,
$NotesChart,
Expand Down Expand Up @@ -567,6 +571,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
FileInfoService,
SearchService,
ClipService,
ChannelFollowingService,
FederationChart,
NotesChart,
UsersChart,
Expand Down Expand Up @@ -690,6 +695,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$FileInfoService,
$SearchService,
$ClipService,
$ChannelFollowingService,
$FederationChart,
$NotesChart,
$UsersChart,
Expand Down
3 changes: 3 additions & 0 deletions packages/backend/src/server/api/StreamingApiServerService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { NotificationService } from '@/core/NotificationService.js';
import { bindThis } from '@/decorators.js';
import { CacheService } from '@/core/CacheService.js';
import { MiLocalUser } from '@/models/User.js';
import { ChannelFollowingService } from '@/core/ChannelFollowingService.js';
import { AuthenticateService, AuthenticationError } from './AuthenticateService.js';
import MainStreamConnection from './stream/Connection.js';
import { ChannelsService } from './stream/ChannelsService.js';
Expand All @@ -37,6 +38,7 @@ export class StreamingApiServerService {
private authenticateService: AuthenticateService,
private channelsService: ChannelsService,
private notificationService: NotificationService,
private channelFollowingService: ChannelFollowingService,
) {
}

Expand Down Expand Up @@ -91,6 +93,7 @@ export class StreamingApiServerService {
this.noteReadService,
this.notificationService,
this.cacheService,
this.channelFollowingService,
user, app,
);

Expand Down
17 changes: 4 additions & 13 deletions packages/backend/src/server/api/endpoints/channels/follow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@

import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { ChannelFollowingsRepository, ChannelsRepository } from '@/models/_.js';
import { IdService } from '@/core/IdService.js';
import type { ChannelsRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { ChannelFollowingService } from '@/core/ChannelFollowingService.js';
import { ApiError } from '../../error.js';

export const meta = {
Expand Down Expand Up @@ -41,11 +41,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
constructor(
@Inject(DI.channelsRepository)
private channelsRepository: ChannelsRepository,

@Inject(DI.channelFollowingsRepository)
private channelFollowingsRepository: ChannelFollowingsRepository,

private idService: IdService,
private channelFollowingService: ChannelFollowingService,
) {
super(meta, paramDef, async (ps, me) => {
const channel = await this.channelsRepository.findOneBy({
Expand All @@ -56,12 +52,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new ApiError(meta.errors.noSuchChannel);
}

await this.channelFollowingsRepository.insert({
id: this.idService.genId(),
createdAt: new Date(),
followerId: me.id,
followeeId: channel.id,
});
await this.channelFollowingService.follow(me, channel);
});
}
}
12 changes: 4 additions & 8 deletions packages/backend/src/server/api/endpoints/channels/unfollow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@

import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { ChannelFollowingsRepository, ChannelsRepository } from '@/models/_.js';
import type { ChannelsRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { ChannelFollowingService } from '@/core/ChannelFollowingService.js';
import { ApiError } from '../../error.js';

export const meta = {
Expand Down Expand Up @@ -40,9 +41,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
constructor(
@Inject(DI.channelsRepository)
private channelsRepository: ChannelsRepository,

@Inject(DI.channelFollowingsRepository)
private channelFollowingsRepository: ChannelFollowingsRepository,
private channelFollowingService: ChannelFollowingService,
) {
super(meta, paramDef, async (ps, me) => {
const channel = await this.channelsRepository.findOneBy({
Expand All @@ -53,10 +52,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new ApiError(meta.errors.noSuchChannel);
}

await this.channelFollowingsRepository.delete({
followerId: me.id,
followeeId: channel.id,
});
await this.channelFollowingService.unfollow(me, channel);
});
}
}
4 changes: 3 additions & 1 deletion packages/backend/src/server/api/stream/Connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { bindThis } from '@/decorators.js';
import { CacheService } from '@/core/CacheService.js';
import { MiUserProfile } from '@/models/_.js';
import type { StreamEventEmitter, GlobalEvents } from '@/core/GlobalEventService.js';
import { ChannelFollowingService } from '@/core/ChannelFollowingService.js';
import type { ChannelsService } from './ChannelsService.js';
import type { EventEmitter } from 'events';
import type Channel from './channel.js';
Expand Down Expand Up @@ -42,6 +43,7 @@ export default class Connection {
private noteReadService: NoteReadService,
private notificationService: NotificationService,
private cacheService: CacheService,
private channelFollowingService: ChannelFollowingService,

user: MiUser | null | undefined,
token: MiAccessToken | null | undefined,
Expand All @@ -56,7 +58,7 @@ export default class Connection {
const [userProfile, following, followingChannels, userIdsWhoMeMuting, userIdsWhoBlockingMe, userIdsWhoMeMutingRenotes] = await Promise.all([
this.cacheService.userProfileCache.fetch(this.user.id),
this.cacheService.userFollowingsCache.fetch(this.user.id),
this.cacheService.userFollowingChannelsCache.fetch(this.user.id),
this.channelFollowingService.userFollowingChannelsCache.fetch(this.user.id),
this.cacheService.userMutingsCache.fetch(this.user.id),
this.cacheService.userBlockedCache.fetch(this.user.id),
this.cacheService.renoteMutingsCache.fetch(this.user.id),
Expand Down

0 comments on commit 17ca35a

Please sign in to comment.