From 4a341a53d0f942c813641a624b2f182b58b1fe12 Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Thu, 26 Jun 2025 11:36:54 -0400 Subject: [PATCH] move user hibernation into HibernateUsersProcessorService.ts --- .../backend/src/core/NoteCreateService.ts | 46 +----------- packages/backend/src/core/NoteEditService.ts | 46 +----------- packages/backend/src/core/QueueService.ts | 13 ++++ .../backend/src/queue/QueueProcessorModule.ts | 2 + .../src/queue/QueueProcessorService.ts | 3 + .../HibernateUsersProcessorService.ts | 71 +++++++++++++++++++ 6 files changed, 93 insertions(+), 88 deletions(-) create mode 100644 packages/backend/src/queue/processors/HibernateUsersProcessorService.ts diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts index e7cfbd136b..fd55c33bfb 100644 --- a/packages/backend/src/core/NoteCreateService.ts +++ b/packages/backend/src/core/NoteCreateService.ts @@ -1037,55 +1037,13 @@ export class NoteCreateService implements OnApplicationShutdown { } } - if (Math.random() < 0.1) { - process.nextTick(() => { - this.checkHibernation(followings); - }); - } + // checkHibernation moved to HibernateUsersProcessorService } r.exec(); } - @bindThis - public async checkHibernation(followings: MiFollowing[]) { - if (followings.length === 0) return; - - const shuffle = (array: MiFollowing[]) => { - for (let i = array.length - 1; i > 0; i--) { - const j = Math.floor(Math.random() * (i + 1)); - [array[i], array[j]] = [array[j], array[i]]; - } - return array; - }; - - // ランダムに最大1000件サンプリング - const samples = shuffle(followings).slice(0, Math.min(followings.length, 1000)); - - const hibernatedUsers = await this.usersRepository.find({ - where: { - id: In(samples.map(x => x.followerId)), - lastActiveDate: LessThan(new Date(this.timeService.now - (1000 * 60 * 60 * 24 * 50))), - }, - select: ['id'], - }); - - if (hibernatedUsers.length > 0) { - await Promise.all([ - this.usersRepository.update({ - id: In(hibernatedUsers.map(x => x.id)), - }, { - isHibernated: true, - }), - this.followingsRepository.update({ - followerId: In(hibernatedUsers.map(x => x.id)), - }, { - isFollowerHibernated: true, - }), - this.cacheService.hibernatedUserCache.setMany(hibernatedUsers.map(x => [x.id, true])), - ]); - } - } + // checkHibernation moved to HibernateUsersProcessorService public checkProhibitedWordsContain(content: Parameters[0], prohibitedWords?: string[]) { if (prohibitedWords == null) { diff --git a/packages/backend/src/core/NoteEditService.ts b/packages/backend/src/core/NoteEditService.ts index 17daf386d6..20afc4e63c 100644 --- a/packages/backend/src/core/NoteEditService.ts +++ b/packages/backend/src/core/NoteEditService.ts @@ -909,55 +909,13 @@ export class NoteEditService implements OnApplicationShutdown { } } - if (Math.random() < 0.1) { - process.nextTick(() => { - this.checkHibernation(followings); - }); - } + // checkHibernation moved to HibernateUsersProcessorService } r.exec(); } - @bindThis - public async checkHibernation(followings: MiFollowing[]) { - if (followings.length === 0) return; - - const shuffle = (array: MiFollowing[]) => { - for (let i = array.length - 1; i > 0; i--) { - const j = Math.floor(Math.random() * (i + 1)); - [array[i], array[j]] = [array[j], array[i]]; - } - return array; - }; - - // ランダムに最大1000件サンプリング - const samples = shuffle(followings).slice(0, Math.min(followings.length, 1000)); - - const hibernatedUsers = await this.usersRepository.find({ - where: { - id: In(samples.map(x => x.followerId)), - lastActiveDate: LessThan(new Date(this.timeService.now - (1000 * 60 * 60 * 24 * 50))), - }, - select: ['id'], - }); - - if (hibernatedUsers.length > 0) { - await Promise.all([ - this.usersRepository.update({ - id: In(hibernatedUsers.map(x => x.id)), - }, { - isHibernated: true, - }), - this.followingsRepository.update({ - followerId: In(hibernatedUsers.map(x => x.id)), - }, { - isFollowerHibernated: true, - }), - this.cacheService.hibernatedUserCache.setMany(hibernatedUsers.map(x => [x.id, true])), - ]); - } - } + // checkHibernation moved to HibernateUsersProcessorService @bindThis private collapseNotesCount(oldValue: number, newValue: number) { diff --git a/packages/backend/src/core/QueueService.ts b/packages/backend/src/core/QueueService.ts index 7e17114641..d0b2ae9f2f 100644 --- a/packages/backend/src/core/QueueService.ts +++ b/packages/backend/src/core/QueueService.ts @@ -177,6 +177,19 @@ export class QueueService implements OnModuleInit { removeOnFail: 30, }, }); + + await this.systemQueue.upsertJobScheduler( + 'hibernateUsers-scheduler', + { pattern: '40 1 * * *' }, + { + name: 'hibernateUsers', + opts: { + removeOnComplete: 10, + removeOnFail: 30, + }, + }); + + // Slot '50 1 * * *' is available for future work } @bindThis diff --git a/packages/backend/src/queue/QueueProcessorModule.ts b/packages/backend/src/queue/QueueProcessorModule.ts index 792d128560..b6469229d2 100644 --- a/packages/backend/src/queue/QueueProcessorModule.ts +++ b/packages/backend/src/queue/QueueProcessorModule.ts @@ -45,6 +45,7 @@ import { ExportFavoritesProcessorService } from './processors/ExportFavoritesPro import { RelationshipProcessorService } from './processors/RelationshipProcessorService.js'; import { ScheduleNotePostProcessorService } from './processors/ScheduleNotePostProcessorService.js'; import { CleanupApLogsProcessorService } from './processors/CleanupApLogsProcessorService.js'; +import { HibernateUsersProcessorService } from './processors/HibernateUsersProcessorService.js'; @Module({ imports: [ @@ -91,6 +92,7 @@ import { CleanupApLogsProcessorService } from './processors/CleanupApLogsProcess QueueProcessorService, ScheduleNotePostProcessorService, CleanupApLogsProcessorService, + HibernateUsersProcessorService, ], exports: [ QueueProcessorService, diff --git a/packages/backend/src/queue/QueueProcessorService.ts b/packages/backend/src/queue/QueueProcessorService.ts index b71544061f..dd94fffb36 100644 --- a/packages/backend/src/queue/QueueProcessorService.ts +++ b/packages/backend/src/queue/QueueProcessorService.ts @@ -52,6 +52,7 @@ import { QueueLoggerService } from './QueueLoggerService.js'; import { QUEUE, baseWorkerOptions } from './const.js'; import { ImportNotesProcessorService } from './processors/ImportNotesProcessorService.js'; import { CleanupApLogsProcessorService } from './processors/CleanupApLogsProcessorService.js'; +import { HibernateUsersProcessorService } from './processors/HibernateUsersProcessorService.js'; // ref. https://github.com/misskey-dev/misskey/pull/7635#issue-971097019 function httpRelatedBackoff(attemptsMade: number) { @@ -138,6 +139,7 @@ export class QueueProcessorService implements OnApplicationShutdown { private scheduleNotePostProcessorService: ScheduleNotePostProcessorService, private readonly timeService: TimeService, private readonly cleanupApLogsProcessorService: CleanupApLogsProcessorService, + private readonly hibernateUsersProcessorService: HibernateUsersProcessorService, ) { this.logger = this.queueLoggerService.logger; @@ -159,6 +161,7 @@ export class QueueProcessorService implements OnApplicationShutdown { case 'checkModeratorsActivity': return this.checkModeratorsActivityProcessorService.process(); case 'clean': return this.cleanProcessorService.process(); case 'cleanupApLogs': return this.cleanupApLogsProcessorService.process(); + case 'hibernateUsers': return this.hibernateUsersProcessorService.process(); default: throw new Error(`unrecognized job type ${job.name} for system`); } }; diff --git a/packages/backend/src/queue/processors/HibernateUsersProcessorService.ts b/packages/backend/src/queue/processors/HibernateUsersProcessorService.ts new file mode 100644 index 0000000000..e4c0ee11ae --- /dev/null +++ b/packages/backend/src/queue/processors/HibernateUsersProcessorService.ts @@ -0,0 +1,71 @@ +/* + * SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import { In, LessThan } from 'typeorm'; +import { QueueLoggerService } from '@/queue/QueueLoggerService.js'; +import type Logger from '@/logger.js'; +import { bindThis } from '@/decorators.js'; +import { renderInlineError } from '@/misc/render-inline-error.js'; +import { CacheService } from '@/core/CacheService.js'; +import type { FollowingsRepository, UsersRepository } from '@/models/_.js'; +import { DI } from '@/di-symbols.js'; + +@Injectable() +export class HibernateUsersProcessorService { + private readonly logger: Logger; + + constructor( + @Inject(DI.usersRepository) + private readonly usersRepository: UsersRepository, + + @Inject(DI.followingsRepository) + private readonly followingsRepository: FollowingsRepository, + + private readonly cacheService: CacheService, + + queueLoggerService: QueueLoggerService, + ) { + this.logger = queueLoggerService.logger.createSubLogger('hibernate-users'); + } + + @bindThis + public async process() { + try { + let totalHibernated = 0; + + // Any users last active *before* this date should be hibernated + const hibernationThreshold = new Date(Date.now() - (1000 * 60 * 60 * 24 * 50)); + + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + while (true) { + // Work in batches of 100 + const page = await this.usersRepository.find({ + where: { isHibernated: false, lastActiveDate: LessThan(hibernationThreshold) }, + select: { id: true }, + take: 100, + }) as { id: string }[]; + const ids = page.map(u => u.id); + + // Stop when we get them all + if (ids.length < 1) break; + + await this.usersRepository.update({ id: In(ids) }, { isHibernated: true }); + await this.followingsRepository.update({ followerId: In(ids) }, { isFollowerHibernated: true }); + await this.cacheService.hibernatedUserCache.refreshMany(ids); + + totalHibernated += ids.length; + } + + if (totalHibernated > 0) { + this.logger.info(`Hibernated ${totalHibernated} inactive users`); + } else { + this.logger.debug('Skipping hibernation: nothing to do'); + } + } catch (err) { + this.logger.error(`Error hibernating users: ${renderInlineError(err)}`); + } + } +}