diff --git a/packages/backend/package.json b/packages/backend/package.json index 0554b2942a..6791c39dad 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -161,7 +161,6 @@ "ratelimiter": "3.4.1", "re2": "1.21.5", "redis-info": "3.1.0", - "redis-lock": "1.0.0", "reflect-metadata": "0.2.2", "rename": "1.0.4", "rss-parser": "3.13.0", diff --git a/packages/backend/src/@types/redis-lock.d.ts b/packages/backend/src/@types/redis-lock.d.ts deleted file mode 100644 index b037cde5ee..0000000000 --- a/packages/backend/src/@types/redis-lock.d.ts +++ /dev/null @@ -1,13 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -declare module 'redis-lock' { - import type Redis from 'ioredis'; - - type Lock = (lockName: string, timeout?: number, taskToPerform?: () => Promise) => void; - function redisLock(client: Redis.Redis, retryDelay: number): Lock; - - export = redisLock; -} diff --git a/packages/backend/src/core/AppLockService.ts b/packages/backend/src/core/AppLockService.ts deleted file mode 100644 index bd2749cb87..0000000000 --- a/packages/backend/src/core/AppLockService.ts +++ /dev/null @@ -1,44 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { promisify } from 'node:util'; -import { Inject, Injectable } from '@nestjs/common'; -import redisLock from 'redis-lock'; -import * as Redis from 'ioredis'; -import { DI } from '@/di-symbols.js'; -import { bindThis } from '@/decorators.js'; - -/** - * Retry delay (ms) for lock acquisition - */ -const retryDelay = 100; - -@Injectable() -export class AppLockService { - private lock: (key: string, timeout?: number, _?: (() => Promise) | undefined) => Promise<() => void>; - - constructor( - @Inject(DI.redis) - private redisClient: Redis.Redis, - ) { - this.lock = promisify(redisLock(this.redisClient, retryDelay)); - } - - /** - * Get AP Object lock - * @param uri AP object ID - * @param timeout Lock timeout (ms), The timeout releases previous lock. - * @returns Unlock function - */ - @bindThis - public getApLock(uri: string, timeout = 30 * 1000): Promise<() => void> { - return this.lock(`ap-object:${uri}`, timeout); - } - - @bindThis - public getChartInsertLock(lockKey: string, timeout = 30 * 1000): Promise<() => void> { - return this.lock(`chart-insert:${lockKey}`, timeout); - } -} diff --git a/packages/backend/src/core/CoreModule.ts b/packages/backend/src/core/CoreModule.ts index d8617e343c..1b8c1c1fda 100644 --- a/packages/backend/src/core/CoreModule.ts +++ b/packages/backend/src/core/CoreModule.ts @@ -20,7 +20,6 @@ import { AccountUpdateService } from './AccountUpdateService.js'; import { AiService } from './AiService.js'; import { AnnouncementService } from './AnnouncementService.js'; import { AntennaService } from './AntennaService.js'; -import { AppLockService } from './AppLockService.js'; import { AchievementService } from './AchievementService.js'; import { AvatarDecorationService } from './AvatarDecorationService.js'; import { CaptchaService } from './CaptchaService.js'; @@ -162,7 +161,6 @@ const $AccountUpdateService: Provider = { provide: 'AccountUpdateService', useEx const $AiService: Provider = { provide: 'AiService', useExisting: AiService }; const $AnnouncementService: Provider = { provide: 'AnnouncementService', useExisting: AnnouncementService }; const $AntennaService: Provider = { provide: 'AntennaService', useExisting: AntennaService }; -const $AppLockService: Provider = { provide: 'AppLockService', useExisting: AppLockService }; const $AchievementService: Provider = { provide: 'AchievementService', useExisting: AchievementService }; const $AvatarDecorationService: Provider = { provide: 'AvatarDecorationService', useExisting: AvatarDecorationService }; const $CaptchaService: Provider = { provide: 'CaptchaService', useExisting: CaptchaService }; @@ -312,7 +310,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting AiService, AnnouncementService, AntennaService, - AppLockService, AchievementService, AvatarDecorationService, CaptchaService, @@ -458,7 +455,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $AiService, $AnnouncementService, $AntennaService, - $AppLockService, $AchievementService, $AvatarDecorationService, $CaptchaService, @@ -605,7 +601,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting AiService, AnnouncementService, AntennaService, - AppLockService, AchievementService, AvatarDecorationService, CaptchaService, @@ -750,7 +745,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $AiService, $AnnouncementService, $AntennaService, - $AppLockService, $AchievementService, $AvatarDecorationService, $CaptchaService, diff --git a/packages/backend/src/core/activitypub/ApInboxService.ts b/packages/backend/src/core/activitypub/ApInboxService.ts index e88f60b806..4acd64a40c 100644 --- a/packages/backend/src/core/activitypub/ApInboxService.ts +++ b/packages/backend/src/core/activitypub/ApInboxService.ts @@ -5,6 +5,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { In } from 'typeorm'; +import * as Redis from 'ioredis'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; import { UserFollowingService } from '@/core/UserFollowingService.js'; @@ -14,8 +15,8 @@ import { NotePiningService } from '@/core/NotePiningService.js'; import { UserBlockingService } from '@/core/UserBlockingService.js'; import { NoteDeleteService } from '@/core/NoteDeleteService.js'; import { NoteCreateService } from '@/core/NoteCreateService.js'; +import { acquireApObjectLock } from '@/misc/distributed-lock.js'; import { concat, toArray, toSingle, unique } from '@/misc/prelude/array.js'; -import { AppLockService } from '@/core/AppLockService.js'; import type Logger from '@/logger.js'; import { IdService } from '@/core/IdService.js'; import { StatusError } from '@/misc/status-error.js'; @@ -48,8 +49,8 @@ export class ApInboxService { @Inject(DI.config) private config: Config, - @Inject(DI.meta) - private meta: MiMeta, + @Inject(DI.redisForTimelines) + private redisForTimelines: Redis.Redis, @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -76,7 +77,6 @@ export class ApInboxService { private userBlockingService: UserBlockingService, private noteCreateService: NoteCreateService, private noteDeleteService: NoteDeleteService, - private appLockService: AppLockService, private apResolverService: ApResolverService, private apDbResolverService: ApDbResolverService, private apLoggerService: ApLoggerService, @@ -311,7 +311,7 @@ export class ApInboxService { // アナウンス先が許可されているかチェック if (!this.utilityService.isFederationAllowedUri(uri)) return; - const unlock = await this.appLockService.getApLock(uri); + const unlock = await acquireApObjectLock(this.redisForTimelines, uri); try { // 既に同じURIを持つものが登録されていないかチェック @@ -438,7 +438,7 @@ export class ApInboxService { } } - const unlock = await this.appLockService.getApLock(uri); + const unlock = await acquireApObjectLock(this.redisForTimelines, uri); try { const exist = await this.apNoteService.fetchNote(note); @@ -522,7 +522,7 @@ export class ApInboxService { private async deleteNote(actor: MiRemoteUser, uri: string): Promise { this.logger.info(`Deleting the Note: ${uri}`); - const unlock = await this.appLockService.getApLock(uri); + const unlock = await acquireApObjectLock(this.redisForTimelines, uri); try { const note = await this.apDbResolverService.getNoteFromApId(uri); diff --git a/packages/backend/src/core/activitypub/models/ApNoteService.ts b/packages/backend/src/core/activitypub/models/ApNoteService.ts index 8abacd293f..3283e88623 100644 --- a/packages/backend/src/core/activitypub/models/ApNoteService.ts +++ b/packages/backend/src/core/activitypub/models/ApNoteService.ts @@ -5,14 +5,15 @@ import { forwardRef, Inject, Injectable } from '@nestjs/common'; import { In } from 'typeorm'; +import * as Redis from 'ioredis'; import { DI } from '@/di-symbols.js'; import type { PollsRepository, EmojisRepository, MiMeta } from '@/models/_.js'; import type { Config } from '@/config.js'; import type { MiRemoteUser } from '@/models/User.js'; import type { MiNote } from '@/models/Note.js'; +import { acquireApObjectLock } from '@/misc/distributed-lock.js'; import { toArray, toSingle, unique } from '@/misc/prelude/array.js'; import type { MiEmoji } from '@/models/Emoji.js'; -import { AppLockService } from '@/core/AppLockService.js'; import type { MiDriveFile } from '@/models/DriveFile.js'; import { NoteCreateService } from '@/core/NoteCreateService.js'; import type Logger from '@/logger.js'; @@ -48,6 +49,9 @@ export class ApNoteService { @Inject(DI.meta) private meta: MiMeta, + @Inject(DI.redisForTimelines) + private redisForTimelines: Redis.Redis, + @Inject(DI.pollsRepository) private pollsRepository: PollsRepository, @@ -67,7 +71,6 @@ export class ApNoteService { private apMentionService: ApMentionService, private apImageService: ApImageService, private apQuestionService: ApQuestionService, - private appLockService: AppLockService, private pollService: PollService, private noteCreateService: NoteCreateService, private apDbResolverService: ApDbResolverService, @@ -354,7 +357,7 @@ export class ApNoteService { throw new StatusError('blocked host', 451); } - const unlock = await this.appLockService.getApLock(uri); + const unlock = await acquireApObjectLock(this.redisForTimelines, uri); try { //#region このサーバーに既に登録されていたらそれを返す diff --git a/packages/backend/src/core/chart/charts/active-users.ts b/packages/backend/src/core/chart/charts/active-users.ts index 05905f3782..0f118ef7f7 100644 --- a/packages/backend/src/core/chart/charts/active-users.ts +++ b/packages/backend/src/core/chart/charts/active-users.ts @@ -5,11 +5,12 @@ import { Injectable, Inject } from '@nestjs/common'; import { DataSource } from 'typeorm'; -import { AppLockService } from '@/core/AppLockService.js'; +import * as Redis from 'ioredis'; import type { MiUser } from '@/models/User.js'; import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; import { IdService } from '@/core/IdService.js'; +import { acquireChartInsertLock } from '@/misc/distributed-lock.js'; import Chart from '../core.js'; import { ChartLoggerService } from '../ChartLoggerService.js'; import { name, schema } from './entities/active-users.js'; @@ -28,11 +29,13 @@ export default class ActiveUsersChart extends Chart { // eslint-d @Inject(DI.db) private db: DataSource, - private appLockService: AppLockService, + @Inject(DI.redisForTimelines) + private redisForTimelines: Redis.Redis, + private chartLoggerService: ChartLoggerService, private idService: IdService, ) { - super(db, (k) => appLockService.getChartInsertLock(k), chartLoggerService.logger, name, schema); + super(db, (k) => acquireChartInsertLock(redisForTimelines, k), chartLoggerService.logger, name, schema); } protected async tickMajor(): Promise>> { diff --git a/packages/backend/src/core/chart/charts/ap-request.ts b/packages/backend/src/core/chart/charts/ap-request.ts index 04e771a95b..401bcdca64 100644 --- a/packages/backend/src/core/chart/charts/ap-request.ts +++ b/packages/backend/src/core/chart/charts/ap-request.ts @@ -5,9 +5,10 @@ import { Injectable, Inject } from '@nestjs/common'; import { DataSource } from 'typeorm'; -import { AppLockService } from '@/core/AppLockService.js'; +import * as Redis from 'ioredis'; import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; +import { acquireChartInsertLock } from '@/misc/distributed-lock.js'; import Chart from '../core.js'; import { ChartLoggerService } from '../ChartLoggerService.js'; import { name, schema } from './entities/ap-request.js'; @@ -22,10 +23,12 @@ export default class ApRequestChart extends Chart { // eslint-dis @Inject(DI.db) private db: DataSource, - private appLockService: AppLockService, + @Inject(DI.redisForTimelines) + private redisForTimelines: Redis.Redis, + private chartLoggerService: ChartLoggerService, ) { - super(db, (k) => appLockService.getChartInsertLock(k), chartLoggerService.logger, name, schema); + super(db, (k) => acquireChartInsertLock(redisForTimelines, k), chartLoggerService.logger, name, schema); } protected async tickMajor(): Promise>> { diff --git a/packages/backend/src/core/chart/charts/drive.ts b/packages/backend/src/core/chart/charts/drive.ts index 613e074a9f..552afec892 100644 --- a/packages/backend/src/core/chart/charts/drive.ts +++ b/packages/backend/src/core/chart/charts/drive.ts @@ -5,10 +5,11 @@ import { Injectable, Inject } from '@nestjs/common'; import { DataSource } from 'typeorm'; +import * as Redis from 'ioredis'; import type { MiDriveFile } from '@/models/DriveFile.js'; -import { AppLockService } from '@/core/AppLockService.js'; import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; +import { acquireChartInsertLock } from '@/misc/distributed-lock.js'; import Chart from '../core.js'; import { ChartLoggerService } from '../ChartLoggerService.js'; import { name, schema } from './entities/drive.js'; @@ -23,10 +24,12 @@ export default class DriveChart extends Chart { // eslint-disable @Inject(DI.db) private db: DataSource, - private appLockService: AppLockService, + @Inject(DI.redisForTimelines) + private redisForTimelines: Redis.Redis, + private chartLoggerService: ChartLoggerService, ) { - super(db, (k) => appLockService.getChartInsertLock(k), chartLoggerService.logger, name, schema); + super(db, (k) => acquireChartInsertLock(redisForTimelines, k), chartLoggerService.logger, name, schema); } protected async tickMajor(): Promise>> { diff --git a/packages/backend/src/core/chart/charts/federation.ts b/packages/backend/src/core/chart/charts/federation.ts index c9b43cc66d..b9057b3e07 100644 --- a/packages/backend/src/core/chart/charts/federation.ts +++ b/packages/backend/src/core/chart/charts/federation.ts @@ -5,10 +5,11 @@ import { Injectable, Inject } from '@nestjs/common'; import { DataSource } from 'typeorm'; +import * as Redis from 'ioredis'; import type { FollowingsRepository, InstancesRepository, MiMeta } from '@/models/_.js'; -import { AppLockService } from '@/core/AppLockService.js'; import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; +import { acquireChartInsertLock } from '@/misc/distributed-lock.js'; import Chart from '../core.js'; import { ChartLoggerService } from '../ChartLoggerService.js'; import { name, schema } from './entities/federation.js'; @@ -26,16 +27,18 @@ export default class FederationChart extends Chart { // eslint-di @Inject(DI.meta) private meta: MiMeta, + @Inject(DI.redisForTimelines) + private redisForTimelines: Redis.Redis, + @Inject(DI.followingsRepository) private followingsRepository: FollowingsRepository, @Inject(DI.instancesRepository) private instancesRepository: InstancesRepository, - private appLockService: AppLockService, private chartLoggerService: ChartLoggerService, ) { - super(db, (k) => appLockService.getChartInsertLock(k), chartLoggerService.logger, name, schema); + super(db, (k) => acquireChartInsertLock(redisForTimelines, k), chartLoggerService.logger, name, schema); } protected async tickMajor(): Promise>> { diff --git a/packages/backend/src/core/chart/charts/instance.ts b/packages/backend/src/core/chart/charts/instance.ts index 97f3bc6f2b..b031d9257d 100644 --- a/packages/backend/src/core/chart/charts/instance.ts +++ b/packages/backend/src/core/chart/charts/instance.ts @@ -5,13 +5,14 @@ import { Injectable, Inject } from '@nestjs/common'; import { DataSource } from 'typeorm'; +import * as Redis from 'ioredis'; import type { DriveFilesRepository, FollowingsRepository, UsersRepository, NotesRepository } from '@/models/_.js'; import type { MiDriveFile } from '@/models/DriveFile.js'; import type { MiNote } from '@/models/Note.js'; -import { AppLockService } from '@/core/AppLockService.js'; import { DI } from '@/di-symbols.js'; import { UtilityService } from '@/core/UtilityService.js'; import { bindThis } from '@/decorators.js'; +import { acquireChartInsertLock } from '@/misc/distributed-lock.js'; import Chart from '../core.js'; import { ChartLoggerService } from '../ChartLoggerService.js'; import { name, schema } from './entities/instance.js'; @@ -26,6 +27,9 @@ export default class InstanceChart extends Chart { // eslint-disa @Inject(DI.db) private db: DataSource, + @Inject(DI.redisForTimelines) + private redisForTimelines: Redis.Redis, + @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -39,10 +43,9 @@ export default class InstanceChart extends Chart { // eslint-disa private followingsRepository: FollowingsRepository, private utilityService: UtilityService, - private appLockService: AppLockService, private chartLoggerService: ChartLoggerService, ) { - super(db, (k) => appLockService.getChartInsertLock(k), chartLoggerService.logger, name, schema, true); + super(db, (k) => acquireChartInsertLock(redisForTimelines, k), chartLoggerService.logger, name, schema, true); } protected async tickMajor(group: string): Promise>> { diff --git a/packages/backend/src/core/chart/charts/notes.ts b/packages/backend/src/core/chart/charts/notes.ts index f763b5fffa..c6258ccc53 100644 --- a/packages/backend/src/core/chart/charts/notes.ts +++ b/packages/backend/src/core/chart/charts/notes.ts @@ -5,11 +5,12 @@ import { Injectable, Inject } from '@nestjs/common'; import { Not, IsNull, DataSource } from 'typeorm'; +import * as Redis from 'ioredis'; import type { NotesRepository } from '@/models/_.js'; import type { MiNote } from '@/models/Note.js'; -import { AppLockService } from '@/core/AppLockService.js'; import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; +import { acquireChartInsertLock } from '@/misc/distributed-lock.js'; import Chart from '../core.js'; import { ChartLoggerService } from '../ChartLoggerService.js'; import { name, schema } from './entities/notes.js'; @@ -24,13 +25,15 @@ export default class NotesChart extends Chart { // eslint-disable @Inject(DI.db) private db: DataSource, + @Inject(DI.redisForTimelines) + private redisForTimelines: Redis.Redis, + @Inject(DI.notesRepository) private notesRepository: NotesRepository, - private appLockService: AppLockService, private chartLoggerService: ChartLoggerService, ) { - super(db, (k) => appLockService.getChartInsertLock(k), chartLoggerService.logger, name, schema); + super(db, (k) => acquireChartInsertLock(redisForTimelines, k), chartLoggerService.logger, name, schema); } protected async tickMajor(): Promise>> { diff --git a/packages/backend/src/core/chart/charts/per-user-drive.ts b/packages/backend/src/core/chart/charts/per-user-drive.ts index 404964d8b7..070ae055f3 100644 --- a/packages/backend/src/core/chart/charts/per-user-drive.ts +++ b/packages/backend/src/core/chart/charts/per-user-drive.ts @@ -5,12 +5,13 @@ import { Injectable, Inject } from '@nestjs/common'; import { DataSource } from 'typeorm'; +import * as Redis from 'ioredis'; import type { DriveFilesRepository } from '@/models/_.js'; import type { MiDriveFile } from '@/models/DriveFile.js'; -import { AppLockService } from '@/core/AppLockService.js'; import { DI } from '@/di-symbols.js'; import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; import { bindThis } from '@/decorators.js'; +import { acquireChartInsertLock } from '@/misc/distributed-lock.js'; import Chart from '../core.js'; import { ChartLoggerService } from '../ChartLoggerService.js'; import { name, schema } from './entities/per-user-drive.js'; @@ -25,14 +26,16 @@ export default class PerUserDriveChart extends Chart { // eslint- @Inject(DI.db) private db: DataSource, + @Inject(DI.redisForTimelines) + private redisForTimelines: Redis.Redis, + @Inject(DI.driveFilesRepository) private driveFilesRepository: DriveFilesRepository, - private appLockService: AppLockService, private driveFileEntityService: DriveFileEntityService, private chartLoggerService: ChartLoggerService, ) { - super(db, (k) => appLockService.getChartInsertLock(k), chartLoggerService.logger, name, schema, true); + super(db, (k) => acquireChartInsertLock(redisForTimelines, k), chartLoggerService.logger, name, schema, true); } protected async tickMajor(group: string): Promise>> { diff --git a/packages/backend/src/core/chart/charts/per-user-following.ts b/packages/backend/src/core/chart/charts/per-user-following.ts index 588ac638de..cbac5cfb1f 100644 --- a/packages/backend/src/core/chart/charts/per-user-following.ts +++ b/packages/backend/src/core/chart/charts/per-user-following.ts @@ -5,12 +5,13 @@ import { Injectable, Inject } from '@nestjs/common'; import { Not, IsNull, DataSource } from 'typeorm'; +import * as Redis from 'ioredis'; import type { MiUser } from '@/models/User.js'; -import { AppLockService } from '@/core/AppLockService.js'; import { DI } from '@/di-symbols.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import type { FollowingsRepository } from '@/models/_.js'; import { bindThis } from '@/decorators.js'; +import { acquireChartInsertLock } from '@/misc/distributed-lock.js'; import Chart from '../core.js'; import { ChartLoggerService } from '../ChartLoggerService.js'; import { name, schema } from './entities/per-user-following.js'; @@ -25,14 +26,16 @@ export default class PerUserFollowingChart extends Chart { // esl @Inject(DI.db) private db: DataSource, + @Inject(DI.redisForTimelines) + private redisForTimelines: Redis.Redis, + @Inject(DI.followingsRepository) private followingsRepository: FollowingsRepository, - private appLockService: AppLockService, private userEntityService: UserEntityService, private chartLoggerService: ChartLoggerService, ) { - super(db, (k) => appLockService.getChartInsertLock(k), chartLoggerService.logger, name, schema, true); + super(db, (k) => acquireChartInsertLock(redisForTimelines, k), chartLoggerService.logger, name, schema, true); } protected async tickMajor(group: string): Promise>> { diff --git a/packages/backend/src/core/chart/charts/per-user-notes.ts b/packages/backend/src/core/chart/charts/per-user-notes.ts index e4900772bb..e66dcd9743 100644 --- a/packages/backend/src/core/chart/charts/per-user-notes.ts +++ b/packages/backend/src/core/chart/charts/per-user-notes.ts @@ -5,12 +5,13 @@ import { Injectable, Inject } from '@nestjs/common'; import { DataSource } from 'typeorm'; +import * as Redis from 'ioredis'; import type { MiUser } from '@/models/User.js'; import type { MiNote } from '@/models/Note.js'; -import { AppLockService } from '@/core/AppLockService.js'; import { DI } from '@/di-symbols.js'; import type { NotesRepository } from '@/models/_.js'; import { bindThis } from '@/decorators.js'; +import { acquireChartInsertLock } from '@/misc/distributed-lock.js'; import Chart from '../core.js'; import { ChartLoggerService } from '../ChartLoggerService.js'; import { name, schema } from './entities/per-user-notes.js'; @@ -25,13 +26,15 @@ export default class PerUserNotesChart extends Chart { // eslint- @Inject(DI.db) private db: DataSource, + @Inject(DI.redisForTimelines) + private redisForTimelines: Redis.Redis, + @Inject(DI.notesRepository) private notesRepository: NotesRepository, - private appLockService: AppLockService, private chartLoggerService: ChartLoggerService, ) { - super(db, (k) => appLockService.getChartInsertLock(k), chartLoggerService.logger, name, schema, true); + super(db, (k) => acquireChartInsertLock(redisForTimelines, k), chartLoggerService.logger, name, schema, true); } protected async tickMajor(group: string): Promise>> { diff --git a/packages/backend/src/core/chart/charts/per-user-pv.ts b/packages/backend/src/core/chart/charts/per-user-pv.ts index 31708fefa8..6b7ace4497 100644 --- a/packages/backend/src/core/chart/charts/per-user-pv.ts +++ b/packages/backend/src/core/chart/charts/per-user-pv.ts @@ -5,10 +5,11 @@ import { Injectable, Inject } from '@nestjs/common'; import { DataSource } from 'typeorm'; +import * as Redis from 'ioredis'; import type { MiUser } from '@/models/User.js'; -import { AppLockService } from '@/core/AppLockService.js'; import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; +import { acquireChartInsertLock } from '@/misc/distributed-lock.js'; import Chart from '../core.js'; import { ChartLoggerService } from '../ChartLoggerService.js'; import { name, schema } from './entities/per-user-pv.js'; @@ -23,10 +24,12 @@ export default class PerUserPvChart extends Chart { // eslint-dis @Inject(DI.db) private db: DataSource, - private appLockService: AppLockService, + @Inject(DI.redisForTimelines) + private redisForTimelines: Redis.Redis, + private chartLoggerService: ChartLoggerService, ) { - super(db, (k) => appLockService.getChartInsertLock(k), chartLoggerService.logger, name, schema, true); + super(db, (k) => acquireChartInsertLock(redisForTimelines, k), chartLoggerService.logger, name, schema, true); } protected async tickMajor(): Promise>> { diff --git a/packages/backend/src/core/chart/charts/per-user-reactions.ts b/packages/backend/src/core/chart/charts/per-user-reactions.ts index c29c4d2870..a07945710b 100644 --- a/packages/backend/src/core/chart/charts/per-user-reactions.ts +++ b/packages/backend/src/core/chart/charts/per-user-reactions.ts @@ -5,12 +5,13 @@ import { Injectable, Inject } from '@nestjs/common'; import { DataSource } from 'typeorm'; +import * as Redis from 'ioredis'; import type { MiUser } from '@/models/User.js'; import type { MiNote } from '@/models/Note.js'; -import { AppLockService } from '@/core/AppLockService.js'; import { DI } from '@/di-symbols.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { bindThis } from '@/decorators.js'; +import { acquireChartInsertLock } from '@/misc/distributed-lock.js'; import Chart from '../core.js'; import { ChartLoggerService } from '../ChartLoggerService.js'; import { name, schema } from './entities/per-user-reactions.js'; @@ -25,11 +26,13 @@ export default class PerUserReactionsChart extends Chart { // esl @Inject(DI.db) private db: DataSource, - private appLockService: AppLockService, + @Inject(DI.redisForTimelines) + private redisForTimelines: Redis.Redis, + private userEntityService: UserEntityService, private chartLoggerService: ChartLoggerService, ) { - super(db, (k) => appLockService.getChartInsertLock(k), chartLoggerService.logger, name, schema, true); + super(db, (k) => acquireChartInsertLock(redisForTimelines, k), chartLoggerService.logger, name, schema, true); } protected async tickMajor(group: string): Promise>> { diff --git a/packages/backend/src/core/chart/charts/test-grouped.ts b/packages/backend/src/core/chart/charts/test-grouped.ts index 7a2844f4ed..7492e59141 100644 --- a/packages/backend/src/core/chart/charts/test-grouped.ts +++ b/packages/backend/src/core/chart/charts/test-grouped.ts @@ -5,10 +5,11 @@ import { Injectable, Inject } from '@nestjs/common'; import { DataSource } from 'typeorm'; -import { AppLockService } from '@/core/AppLockService.js'; +import * as Redis from 'ioredis'; import { DI } from '@/di-symbols.js'; import Logger from '@/logger.js'; import { bindThis } from '@/decorators.js'; +import { acquireChartInsertLock } from '@/misc/distributed-lock.js'; import Chart from '../core.js'; import { name, schema } from './entities/test-grouped.js'; import type { KVs } from '../core.js'; @@ -24,10 +25,12 @@ export default class TestGroupedChart extends Chart { // eslint-d @Inject(DI.db) private db: DataSource, - private appLockService: AppLockService, + @Inject(DI.redisForTimelines) + private redisForTimelines: Redis.Redis, + logger: Logger, ) { - super(db, (k) => appLockService.getChartInsertLock(k), logger, name, schema, true); + super(db, (k) => acquireChartInsertLock(redisForTimelines, k), logger, name, schema, true); } protected async tickMajor(group: string): Promise>> { diff --git a/packages/backend/src/core/chart/charts/test-intersection.ts b/packages/backend/src/core/chart/charts/test-intersection.ts index b8d0556c9f..cf96622d17 100644 --- a/packages/backend/src/core/chart/charts/test-intersection.ts +++ b/packages/backend/src/core/chart/charts/test-intersection.ts @@ -5,10 +5,11 @@ import { Injectable, Inject } from '@nestjs/common'; import { DataSource } from 'typeorm'; -import { AppLockService } from '@/core/AppLockService.js'; +import * as Redis from 'ioredis'; import { DI } from '@/di-symbols.js'; import Logger from '@/logger.js'; import { bindThis } from '@/decorators.js'; +import { acquireChartInsertLock } from '@/misc/distributed-lock.js'; import Chart from '../core.js'; import { name, schema } from './entities/test-intersection.js'; import type { KVs } from '../core.js'; @@ -22,10 +23,12 @@ export default class TestIntersectionChart extends Chart { // esl @Inject(DI.db) private db: DataSource, - private appLockService: AppLockService, + @Inject(DI.redisForTimelines) + private redisForTimelines: Redis.Redis, + logger: Logger, ) { - super(db, (k) => appLockService.getChartInsertLock(k), logger, name, schema); + super(db, (k) => acquireChartInsertLock(redisForTimelines, k), logger, name, schema); } protected async tickMajor(): Promise>> { diff --git a/packages/backend/src/core/chart/charts/test-unique.ts b/packages/backend/src/core/chart/charts/test-unique.ts index f94e008059..7c8b651b39 100644 --- a/packages/backend/src/core/chart/charts/test-unique.ts +++ b/packages/backend/src/core/chart/charts/test-unique.ts @@ -5,10 +5,11 @@ import { Injectable, Inject } from '@nestjs/common'; import { DataSource } from 'typeorm'; -import { AppLockService } from '@/core/AppLockService.js'; +import * as Redis from 'ioredis'; import { DI } from '@/di-symbols.js'; import Logger from '@/logger.js'; import { bindThis } from '@/decorators.js'; +import { acquireChartInsertLock } from '@/misc/distributed-lock.js'; import Chart from '../core.js'; import { name, schema } from './entities/test-unique.js'; import type { KVs } from '../core.js'; @@ -22,10 +23,12 @@ export default class TestUniqueChart extends Chart { // eslint-di @Inject(DI.db) private db: DataSource, - private appLockService: AppLockService, + @Inject(DI.redisForTimelines) + private redisForTimelines: Redis.Redis, + logger: Logger, ) { - super(db, (k) => appLockService.getChartInsertLock(k), logger, name, schema); + super(db, (k) => acquireChartInsertLock(redisForTimelines, k), logger, name, schema); } protected async tickMajor(): Promise>> { diff --git a/packages/backend/src/core/chart/charts/test.ts b/packages/backend/src/core/chart/charts/test.ts index a90dc8f99b..4dee2ccefe 100644 --- a/packages/backend/src/core/chart/charts/test.ts +++ b/packages/backend/src/core/chart/charts/test.ts @@ -5,10 +5,11 @@ import { Injectable, Inject } from '@nestjs/common'; import { DataSource } from 'typeorm'; -import { AppLockService } from '@/core/AppLockService.js'; +import * as Redis from 'ioredis'; import { DI } from '@/di-symbols.js'; import Logger from '@/logger.js'; import { bindThis } from '@/decorators.js'; +import { acquireChartInsertLock } from '@/misc/distributed-lock.js'; import Chart from '../core.js'; import { name, schema } from './entities/test.js'; import type { KVs } from '../core.js'; @@ -24,10 +25,12 @@ export default class TestChart extends Chart { // eslint-disable- @Inject(DI.db) private db: DataSource, - private appLockService: AppLockService, + @Inject(DI.redisForTimelines) + private redisForTimelines: Redis.Redis, + logger: Logger, ) { - super(db, (k) => appLockService.getChartInsertLock(k), logger, name, schema); + super(db, (k) => acquireChartInsertLock(redisForTimelines, k), logger, name, schema); } protected async tickMajor(): Promise>> { diff --git a/packages/backend/src/core/chart/charts/users.ts b/packages/backend/src/core/chart/charts/users.ts index d148fc629b..0c7c95f429 100644 --- a/packages/backend/src/core/chart/charts/users.ts +++ b/packages/backend/src/core/chart/charts/users.ts @@ -5,12 +5,13 @@ import { Injectable, Inject } from '@nestjs/common'; import { Not, IsNull, DataSource } from 'typeorm'; +import * as Redis from 'ioredis'; import type { MiUser } from '@/models/User.js'; -import { AppLockService } from '@/core/AppLockService.js'; import { DI } from '@/di-symbols.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import type { UsersRepository } from '@/models/_.js'; import { bindThis } from '@/decorators.js'; +import { acquireChartInsertLock } from '@/misc/distributed-lock.js'; import Chart from '../core.js'; import { ChartLoggerService } from '../ChartLoggerService.js'; import { name, schema } from './entities/users.js'; @@ -25,14 +26,16 @@ export default class UsersChart extends Chart { // eslint-disable @Inject(DI.db) private db: DataSource, + @Inject(DI.redisForTimelines) + private redisForTimelines: Redis.Redis, + @Inject(DI.usersRepository) private usersRepository: UsersRepository, - private appLockService: AppLockService, private userEntityService: UserEntityService, private chartLoggerService: ChartLoggerService, ) { - super(db, (k) => appLockService.getChartInsertLock(k), chartLoggerService.logger, name, schema); + super(db, (k) => acquireChartInsertLock(redisForTimelines, k), chartLoggerService.logger, name, schema); } protected async tickMajor(): Promise>> { diff --git a/packages/backend/src/misc/distributed-lock.ts b/packages/backend/src/misc/distributed-lock.ts new file mode 100644 index 0000000000..0d7dfb1592 --- /dev/null +++ b/packages/backend/src/misc/distributed-lock.ts @@ -0,0 +1,44 @@ +import * as Redis from 'ioredis'; + +export async function acquireDistributedLock( + redis: Redis.Redis, + name: string, + timeout: number, + maxRetries: number, + retryInterval: number, +): Promise<() => Promise> { + const lockKey = `lock:${name}`; + const identifier = Math.random().toString(36).slice(2); + + let retries = 0; + while (retries < maxRetries) { + const result = await redis.set(lockKey, identifier, 'PX', timeout, 'NX'); + if (result === 'OK') { + return async () => { + const currentIdentifier = await redis.get(lockKey); + if (currentIdentifier === identifier) { + await redis.del(lockKey); + } + }; + } + + await new Promise(resolve => setTimeout(resolve, retryInterval)); + retries++; + } + + throw new Error(`Failed to acquire lock ${name}`); +} + +export function acquireApObjectLock( + redis: Redis.Redis, + uri: string, +): Promise<() => Promise> { + return acquireDistributedLock(redis, `ap-object:${uri}`, 30 * 1000, 600, 100); +} + +export function acquireChartInsertLock( + redis: Redis.Redis, + name: string, +): Promise<() => Promise> { + return acquireDistributedLock(redis, `chart-insert:${name}`, 30 * 1000, 120, 500); +} diff --git a/packages/backend/test/unit/chart.ts b/packages/backend/test/unit/chart.ts index 9dedd3a79d..1261352d36 100644 --- a/packages/backend/test/unit/chart.ts +++ b/packages/backend/test/unit/chart.ts @@ -9,6 +9,7 @@ import * as assert from 'assert'; import { jest } from '@jest/globals'; import * as lolex from '@sinonjs/fake-timers'; import { DataSource } from 'typeorm'; +import * as Redis from 'ioredis'; import TestChart from '@/core/chart/charts/test.js'; import TestGroupedChart from '@/core/chart/charts/test-grouped.js'; import TestUniqueChart from '@/core/chart/charts/test-unique.js'; @@ -18,16 +19,16 @@ import { entity as TestGroupedChartEntity } from '@/core/chart/charts/entities/t import { entity as TestUniqueChartEntity } from '@/core/chart/charts/entities/test-unique.js'; import { entity as TestIntersectionChartEntity } from '@/core/chart/charts/entities/test-intersection.js'; import { loadConfig } from '@/config.js'; -import type { AppLockService } from '@/core/AppLockService.js'; import Logger from '@/logger.js'; describe('Chart', () => { const config = loadConfig(); - const appLockService = { - getChartInsertLock: () => () => Promise.resolve(() => {}), - } as unknown as jest.Mocked; let db: DataSource | undefined; + let redisForTimelines = { + set: () => Promise.resolve('OK'), + get: () => Promise.resolve(null), + } as unknown as jest.Mocked; let testChart: TestChart; let testGroupedChart: TestGroupedChart; @@ -64,10 +65,10 @@ describe('Chart', () => { await db.initialize(); const logger = new Logger('chart'); // TODO: モックにする - testChart = new TestChart(db, appLockService, logger); - testGroupedChart = new TestGroupedChart(db, appLockService, logger); - testUniqueChart = new TestUniqueChart(db, appLockService, logger); - testIntersectionChart = new TestIntersectionChart(db, appLockService, logger); + testChart = new TestChart(db, redisForTimelines, logger); + testGroupedChart = new TestGroupedChart(db, redisForTimelines, logger); + testUniqueChart = new TestUniqueChart(db, redisForTimelines, logger); + testIntersectionChart = new TestIntersectionChart(db, redisForTimelines, logger); clock = lolex.install({ now: new Date(Date.UTC(2000, 0, 1, 0, 0, 0)), diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c0b47065e1..d46e0d8f64 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -369,9 +369,6 @@ importers: redis-info: specifier: 3.1.0 version: 3.1.0 - redis-lock: - specifier: 1.0.0 - version: 1.0.0 reflect-metadata: specifier: 0.2.2 version: 0.2.2 @@ -9160,10 +9157,6 @@ packages: redis-info@3.1.0: resolution: {integrity: sha512-ER4L9Sh/vm63DkIE0bkSjxluQlioBiBgf5w1UuldaW/3vPcecdljVDisZhmnCMvsxHNiARTTDDHGg9cGwTfrKg==} - redis-lock@1.0.0: - resolution: {integrity: sha512-zfI+Il36jXwRT/W8SBsG132Bc2yp3tMuf3KTGjSzXimadI17NEGBvb/KrDkCuAC2hzVxW5uR5ns/rxuqiWeV3Q==} - engines: {node: '>=12'} - redis-parser@3.0.0: resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==} engines: {node: '>=4'} @@ -20217,8 +20210,6 @@ snapshots: dependencies: lodash: 4.17.21 - redis-lock@1.0.0: {} - redis-parser@3.0.0: dependencies: redis-errors: 1.2.0