move user hibernation into HibernateUsersProcessorService.ts

This commit is contained in:
Hazelnoot 2025-06-26 11:36:54 -04:00
parent 7050583b86
commit 4a341a53d0
6 changed files with 93 additions and 88 deletions

View file

@ -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<UtilityService['concatNoteContentsForKeyWordCheck']>[0], prohibitedWords?: string[]) {
if (prohibitedWords == null) {

View file

@ -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) {

View file

@ -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

View file

@ -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,

View file

@ -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`);
}
};

View file

@ -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)}`);
}
}
}