wip
This commit is contained in:
parent
0575207463
commit
77498f84d8
7 changed files with 138 additions and 11 deletions
17
packages/backend/migration/1696331570827-hibernation.js
Normal file
17
packages/backend/migration/1696331570827-hibernation.js
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
export class Hibernation1696331570827 {
|
||||||
|
name = 'Hibernation1696331570827'
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query(`DROP INDEX "public"."IDX_d74d8ab5efa7e3bb82825c0fa2"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "user" ADD "isHibernated" boolean NOT NULL DEFAULT false`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "following" ADD "isFollowerHibernated" boolean NOT NULL DEFAULT false`);
|
||||||
|
await queryRunner.query(`CREATE INDEX "IDX_ce62b50d882d4e9dee10ad0d2f" ON "following" ("followeeId", "followerHost", "isFollowerHibernated") `);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
await queryRunner.query(`DROP INDEX "public"."IDX_ce62b50d882d4e9dee10ad0d2f"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "following" DROP COLUMN "isFollowerHibernated"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "isHibernated"`);
|
||||||
|
await queryRunner.query(`CREATE INDEX "IDX_d74d8ab5efa7e3bb82825c0fa2" ON "following" ("followeeId", "followerHost") `);
|
||||||
|
}
|
||||||
|
}
|
|
@ -46,6 +46,7 @@ import { SignupService } from './SignupService.js';
|
||||||
import { WebAuthnService } from './WebAuthnService.js';
|
import { WebAuthnService } from './WebAuthnService.js';
|
||||||
import { UserBlockingService } from './UserBlockingService.js';
|
import { UserBlockingService } from './UserBlockingService.js';
|
||||||
import { CacheService } from './CacheService.js';
|
import { CacheService } from './CacheService.js';
|
||||||
|
import { UserService } from './UserService.js';
|
||||||
import { UserFollowingService } from './UserFollowingService.js';
|
import { UserFollowingService } from './UserFollowingService.js';
|
||||||
import { UserKeypairService } from './UserKeypairService.js';
|
import { UserKeypairService } from './UserKeypairService.js';
|
||||||
import { UserListService } from './UserListService.js';
|
import { UserListService } from './UserListService.js';
|
||||||
|
@ -173,6 +174,7 @@ const $SignupService: Provider = { provide: 'SignupService', useExisting: Signup
|
||||||
const $WebAuthnService: Provider = { provide: 'WebAuthnService', useExisting: WebAuthnService };
|
const $WebAuthnService: Provider = { provide: 'WebAuthnService', useExisting: WebAuthnService };
|
||||||
const $UserBlockingService: Provider = { provide: 'UserBlockingService', useExisting: UserBlockingService };
|
const $UserBlockingService: Provider = { provide: 'UserBlockingService', useExisting: UserBlockingService };
|
||||||
const $CacheService: Provider = { provide: 'CacheService', useExisting: CacheService };
|
const $CacheService: Provider = { provide: 'CacheService', useExisting: CacheService };
|
||||||
|
const $UserService: Provider = { provide: 'UserService', useExisting: UserService };
|
||||||
const $UserFollowingService: Provider = { provide: 'UserFollowingService', useExisting: UserFollowingService };
|
const $UserFollowingService: Provider = { provide: 'UserFollowingService', useExisting: UserFollowingService };
|
||||||
const $UserKeypairService: Provider = { provide: 'UserKeypairService', useExisting: UserKeypairService };
|
const $UserKeypairService: Provider = { provide: 'UserKeypairService', useExisting: UserKeypairService };
|
||||||
const $UserListService: Provider = { provide: 'UserListService', useExisting: UserListService };
|
const $UserListService: Provider = { provide: 'UserListService', useExisting: UserListService };
|
||||||
|
@ -303,6 +305,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||||
WebAuthnService,
|
WebAuthnService,
|
||||||
UserBlockingService,
|
UserBlockingService,
|
||||||
CacheService,
|
CacheService,
|
||||||
|
UserService,
|
||||||
UserFollowingService,
|
UserFollowingService,
|
||||||
UserKeypairService,
|
UserKeypairService,
|
||||||
UserListService,
|
UserListService,
|
||||||
|
@ -426,6 +429,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||||
$WebAuthnService,
|
$WebAuthnService,
|
||||||
$UserBlockingService,
|
$UserBlockingService,
|
||||||
$CacheService,
|
$CacheService,
|
||||||
|
$UserService,
|
||||||
$UserFollowingService,
|
$UserFollowingService,
|
||||||
$UserKeypairService,
|
$UserKeypairService,
|
||||||
$UserListService,
|
$UserListService,
|
||||||
|
@ -550,6 +554,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||||
WebAuthnService,
|
WebAuthnService,
|
||||||
UserBlockingService,
|
UserBlockingService,
|
||||||
CacheService,
|
CacheService,
|
||||||
|
UserService,
|
||||||
UserFollowingService,
|
UserFollowingService,
|
||||||
UserKeypairService,
|
UserKeypairService,
|
||||||
UserListService,
|
UserListService,
|
||||||
|
@ -672,6 +677,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||||
$WebAuthnService,
|
$WebAuthnService,
|
||||||
$UserBlockingService,
|
$UserBlockingService,
|
||||||
$CacheService,
|
$CacheService,
|
||||||
|
$UserService,
|
||||||
$UserFollowingService,
|
$UserFollowingService,
|
||||||
$UserKeypairService,
|
$UserKeypairService,
|
||||||
$UserListService,
|
$UserListService,
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
|
|
||||||
import { setImmediate } from 'node:timers/promises';
|
import { setImmediate } from 'node:timers/promises';
|
||||||
import * as mfm from 'mfm-js';
|
import * as mfm from 'mfm-js';
|
||||||
import { In, DataSource, IsNull } from 'typeorm';
|
import { In, DataSource, IsNull, LessThan } from 'typeorm';
|
||||||
import * as Redis from 'ioredis';
|
import * as Redis from 'ioredis';
|
||||||
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
|
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
|
||||||
import RE2 from 're2';
|
import RE2 from 're2';
|
||||||
|
@ -14,7 +14,7 @@ import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mf
|
||||||
import { extractHashtags } from '@/misc/extract-hashtags.js';
|
import { extractHashtags } from '@/misc/extract-hashtags.js';
|
||||||
import type { IMentionedRemoteUsers } from '@/models/Note.js';
|
import type { IMentionedRemoteUsers } from '@/models/Note.js';
|
||||||
import { MiNote } from '@/models/Note.js';
|
import { MiNote } from '@/models/Note.js';
|
||||||
import type { ChannelFollowingsRepository, ChannelsRepository, FollowingsRepository, InstancesRepository, MutingsRepository, NotesRepository, NoteThreadMutingsRepository, UserListMembershipsRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js';
|
import type { ChannelFollowingsRepository, ChannelsRepository, FollowingsRepository, InstancesRepository, MiFollowing, MutingsRepository, NotesRepository, NoteThreadMutingsRepository, UserListMembershipsRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js';
|
||||||
import type { MiDriveFile } from '@/models/DriveFile.js';
|
import type { MiDriveFile } from '@/models/DriveFile.js';
|
||||||
import type { MiApp } from '@/models/App.js';
|
import type { MiApp } from '@/models/App.js';
|
||||||
import { concat } from '@/misc/prelude/array.js';
|
import { concat } from '@/misc/prelude/array.js';
|
||||||
|
@ -829,13 +829,12 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// TODO: 休眠ユーザーを弾く
|
|
||||||
// TODO: チャンネルフォロー
|
|
||||||
// TODO: キャッシュ?
|
// TODO: キャッシュ?
|
||||||
const followings = await this.followingsRepository.find({
|
const followings = await this.followingsRepository.find({
|
||||||
where: {
|
where: {
|
||||||
followeeId: user.id,
|
followeeId: user.id,
|
||||||
followerHost: IsNull(),
|
followerHost: IsNull(),
|
||||||
|
isFollowerHibernated: false,
|
||||||
},
|
},
|
||||||
select: ['followerId', 'withReplies'],
|
select: ['followerId', 'withReplies'],
|
||||||
});
|
});
|
||||||
|
@ -952,11 +951,55 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (Math.random() < 0.1) {
|
||||||
|
process.nextTick(() => {
|
||||||
|
this.checkHibernation(followings);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
redisPipeline.exec();
|
redisPipeline.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(Date.now() - (1000 * 60 * 60 * 24 * 50))),
|
||||||
|
},
|
||||||
|
select: ['id'],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (hibernatedUsers.length > 0) {
|
||||||
|
this.usersRepository.update({
|
||||||
|
id: In(hibernatedUsers.map(x => x.id)),
|
||||||
|
}, {
|
||||||
|
isHibernated: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.followingsRepository.update({
|
||||||
|
followerId: In(hibernatedUsers.map(x => x.id)),
|
||||||
|
}, {
|
||||||
|
isFollowerHibernated: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public dispose(): void {
|
public dispose(): void {
|
||||||
this.#shutdownController.abort();
|
this.#shutdownController.abort();
|
||||||
|
|
53
packages/backend/src/core/UserService.ts
Normal file
53
packages/backend/src/core/UserService.ts
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import type { FollowingsRepository, UsersRepository } from '@/models/_.js';
|
||||||
|
import type { MiUser } from '@/models/User.js';
|
||||||
|
import { DI } from '@/di-symbols.js';
|
||||||
|
import { bindThis } from '@/decorators.js';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class UserService {
|
||||||
|
constructor(
|
||||||
|
@Inject(DI.usersRepository)
|
||||||
|
private usersRepository: UsersRepository,
|
||||||
|
|
||||||
|
@Inject(DI.followingsRepository)
|
||||||
|
private followingsRepository: FollowingsRepository,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public async updateLastActiveDate(user: MiUser): Promise<void> {
|
||||||
|
if (user.isHibernated) {
|
||||||
|
const result = await this.usersRepository.createQueryBuilder().update()
|
||||||
|
.set({
|
||||||
|
lastActiveDate: new Date(),
|
||||||
|
})
|
||||||
|
.where('id = :id', { id: user.id })
|
||||||
|
.returning('*')
|
||||||
|
.execute()
|
||||||
|
.then((response) => {
|
||||||
|
return response.raw[0];
|
||||||
|
});
|
||||||
|
const wokeUp = result.isHibernated;
|
||||||
|
if (wokeUp) {
|
||||||
|
this.usersRepository.update(user.id, {
|
||||||
|
isHibernated: false,
|
||||||
|
});
|
||||||
|
this.followingsRepository.update({
|
||||||
|
followerId: user.id,
|
||||||
|
}, {
|
||||||
|
isFollowerHibernated: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.usersRepository.update(user.id, {
|
||||||
|
lastActiveDate: new Date(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -9,7 +9,7 @@ import { MiUser } from './User.js';
|
||||||
|
|
||||||
@Entity('following')
|
@Entity('following')
|
||||||
@Index(['followerId', 'followeeId'], { unique: true })
|
@Index(['followerId', 'followeeId'], { unique: true })
|
||||||
@Index(['followeeId', 'followerHost'])
|
@Index(['followeeId', 'followerHost', 'isFollowerHibernated'])
|
||||||
export class MiFollowing {
|
export class MiFollowing {
|
||||||
@PrimaryColumn(id())
|
@PrimaryColumn(id())
|
||||||
public id: string;
|
public id: string;
|
||||||
|
@ -46,6 +46,11 @@ export class MiFollowing {
|
||||||
@JoinColumn()
|
@JoinColumn()
|
||||||
public follower: MiUser | null;
|
public follower: MiUser | null;
|
||||||
|
|
||||||
|
@Column('boolean', {
|
||||||
|
default: false,
|
||||||
|
})
|
||||||
|
public isFollowerHibernated: boolean;
|
||||||
|
|
||||||
// タイムラインにその人のリプライまで含めるかどうか
|
// タイムラインにその人のリプライまで含めるかどうか
|
||||||
@Column('boolean', {
|
@Column('boolean', {
|
||||||
default: false,
|
default: false,
|
||||||
|
|
|
@ -187,6 +187,11 @@ export class MiUser {
|
||||||
})
|
})
|
||||||
public isExplorable: boolean;
|
public isExplorable: boolean;
|
||||||
|
|
||||||
|
@Column('boolean', {
|
||||||
|
default: false,
|
||||||
|
})
|
||||||
|
public isHibernated: boolean;
|
||||||
|
|
||||||
// アカウントが削除されたかどうかのフラグだが、完全に削除される際は物理削除なので実質削除されるまでの「削除が進行しているかどうか」のフラグ
|
// アカウントが削除されたかどうかのフラグだが、完全に削除される際は物理削除なので実質削除されるまでの「削除が進行しているかどうか」のフラグ
|
||||||
@Column('boolean', {
|
@Column('boolean', {
|
||||||
default: false,
|
default: false,
|
||||||
|
|
|
@ -14,6 +14,7 @@ import { NotificationService } from '@/core/NotificationService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { CacheService } from '@/core/CacheService.js';
|
import { CacheService } from '@/core/CacheService.js';
|
||||||
import { MiLocalUser } from '@/models/User.js';
|
import { MiLocalUser } from '@/models/User.js';
|
||||||
|
import { UserService } from '@/core/UserService.js';
|
||||||
import { AuthenticateService, AuthenticationError } from './AuthenticateService.js';
|
import { AuthenticateService, AuthenticationError } from './AuthenticateService.js';
|
||||||
import MainStreamConnection from './stream/Connection.js';
|
import MainStreamConnection from './stream/Connection.js';
|
||||||
import { ChannelsService } from './stream/ChannelsService.js';
|
import { ChannelsService } from './stream/ChannelsService.js';
|
||||||
|
@ -37,6 +38,7 @@ export class StreamingApiServerService {
|
||||||
private authenticateService: AuthenticateService,
|
private authenticateService: AuthenticateService,
|
||||||
private channelsService: ChannelsService,
|
private channelsService: ChannelsService,
|
||||||
private notificationService: NotificationService,
|
private notificationService: NotificationService,
|
||||||
|
private usersService: UserService,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -130,14 +132,10 @@ export class StreamingApiServerService {
|
||||||
this.#connections.set(connection, Date.now());
|
this.#connections.set(connection, Date.now());
|
||||||
|
|
||||||
const userUpdateIntervalId = user ? setInterval(() => {
|
const userUpdateIntervalId = user ? setInterval(() => {
|
||||||
this.usersRepository.update(user.id, {
|
this.usersService.updateLastActiveDate(user);
|
||||||
lastActiveDate: new Date(),
|
|
||||||
});
|
|
||||||
}, 1000 * 60 * 5) : null;
|
}, 1000 * 60 * 5) : null;
|
||||||
if (user) {
|
if (user) {
|
||||||
this.usersRepository.update(user.id, {
|
this.usersService.updateLastActiveDate(user);
|
||||||
lastActiveDate: new Date(),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
connection.once('close', () => {
|
connection.once('close', () => {
|
||||||
|
|
Loading…
Reference in a new issue