1
0
mirror of https://github.com/MisskeyIO/misskey synced 2024-11-27 14:28:49 +09:00

fix(backend): アカウントの作成と削除の途中でリトライが発生しても無視するように (MisskeyIO#580)

This commit is contained in:
まっちゃとーにゅ 2024-03-30 15:55:16 +09:00 committed by GitHub
parent 1fb7fb8187
commit acc10c0709
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 105 additions and 59 deletions

View File

@ -4,6 +4,7 @@
*/ */
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import * as Redis from 'ioredis';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type Logger from '@/logger.js'; import type Logger from '@/logger.js';
@ -20,6 +21,8 @@ export class DeleteAccountService {
public logger: Logger; public logger: Logger;
constructor( constructor(
@Inject(DI.redis)
private redisClient: Redis.Redis,
@Inject(DI.usersRepository) @Inject(DI.usersRepository)
private usersRepository: UsersRepository, private usersRepository: UsersRepository,
@ -29,7 +32,7 @@ export class DeleteAccountService {
private globalEventService: GlobalEventService, private globalEventService: GlobalEventService,
private loggerService: LoggerService, private loggerService: LoggerService,
) { ) {
this.logger = this.loggerService.getLogger('delete-account'); this.logger = this.loggerService.getLogger('account:delete');
} }
@bindThis @bindThis
@ -39,9 +42,20 @@ export class DeleteAccountService {
const _user = await this.usersRepository.findOneByOrFail({ id: user.id }); const _user = await this.usersRepository.findOneByOrFail({ id: user.id });
if (_user.isRoot) throw new Error('cannot delete a root account'); if (_user.isRoot) throw new Error('cannot delete a root account');
// 5分間の間に同じアカウントに対して削除リクエストが複数回来た場合、最初のリクエストのみを処理する
const lock = await this.redisClient.set(`account:delete:lock:${user.id}`, Date.now(), 'EX', 60 * 5, 'NX');
if (lock === null) {
this.logger.warn(`Delete account is already in progress for ${user.id}`);
return;
}
// noinspection ES6MissingAwait APIで呼び出される際にタイムアウトされないように
(async () => {
try {
// 物理削除する前にDelete activityを送信する // 物理削除する前にDelete activityを送信する
await this.userSuspendService.doPostSuspend(user).catch(err => this.logger.error(err)); await this.userSuspendService.doPostSuspend(user).catch(err => this.logger.error(err));
// noinspection ES6MissingAwait
this.queueService.createDeleteAccountJob(user, { this.queueService.createDeleteAccountJob(user, {
force: me ? await this.roleService.isModerator(me) : false, force: me ? await this.roleService.isModerator(me) : false,
soft: soft, soft: soft,
@ -52,6 +66,14 @@ export class DeleteAccountService {
}); });
this.globalEventService.publishInternalEvent('userChangeDeletedState', { id: user.id, isDeleted: true }); this.globalEventService.publishInternalEvent('userChangeDeletedState', { id: user.id, isDeleted: true });
} catch (err) {
this.logger.error(`Failed to delete account ${user.id}, request by ${me ? me.id : 'remote'} (soft: ${soft})`, { error: err });
// すでにcallstackから離れてるので、ここでエラーをthrowしても意味がない
} finally {
// 成功・失敗に関わらずロックを解除
await this.redisClient.unlink(`account:delete:lock:${user.id}`);
}
})();
} }
@bindThis @bindThis

View File

@ -41,11 +41,12 @@ export class FetchInstanceMetadataService {
private logger: Logger; private logger: Logger;
constructor( constructor(
@Inject(DI.redis)
private redisClient: Redis.Redis,
private httpRequestService: HttpRequestService, private httpRequestService: HttpRequestService,
private loggerService: LoggerService, private loggerService: LoggerService,
private federatedInstanceService: FederatedInstanceService, private federatedInstanceService: FederatedInstanceService,
@Inject(DI.redis)
private redisClient: Redis.Redis,
) { ) {
this.logger = this.loggerService.getLogger('metadata', 'cyan'); this.logger = this.loggerService.getLogger('metadata', 'cyan');
} }

View File

@ -6,27 +6,34 @@
import { generateKeyPair } from 'node:crypto'; import { generateKeyPair } from 'node:crypto';
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import bcrypt from 'bcryptjs'; import bcrypt from 'bcryptjs';
import * as Redis from 'ioredis';
import { DataSource, IsNull } from 'typeorm'; import { DataSource, IsNull } from 'typeorm';
import { bindThis } from '@/decorators.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type Logger from '@/logger.js';
import generateUserToken from '@/misc/generate-native-user-token.js';
import type { UsedUsernamesRepository, UsersRepository } from '@/models/_.js'; import type { UsedUsernamesRepository, UsersRepository } from '@/models/_.js';
import { MiUser } from '@/models/User.js'; import { MiUser } from '@/models/User.js';
import { MiUserProfile } from '@/models/UserProfile.js'; import { MiUserProfile } from '@/models/UserProfile.js';
import { IdService } from '@/core/IdService.js';
import { MiUserKeypair } from '@/models/UserKeypair.js'; import { MiUserKeypair } from '@/models/UserKeypair.js';
import { MiUsedUsername } from '@/models/UsedUsername.js'; import { MiUsedUsername } from '@/models/UsedUsername.js';
import generateUserToken from '@/misc/generate-native-user-token.js'; import { IdService } from '@/core/IdService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { InstanceActorService } from '@/core/InstanceActorService.js';
import { bindThis } from '@/decorators.js';
import UsersChart from '@/core/chart/charts/users.js';
import { UtilityService } from '@/core/UtilityService.js';
import { MetaService } from '@/core/MetaService.js'; import { MetaService } from '@/core/MetaService.js';
import { UtilityService } from '@/core/UtilityService.js';
import { LoggerService } from '@/core/LoggerService.js';
import { InstanceActorService } from '@/core/InstanceActorService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import UsersChart from '@/core/chart/charts/users.js';
@Injectable() @Injectable()
export class SignupService { export class SignupService {
public logger: Logger;
constructor( constructor(
@Inject(DI.db) @Inject(DI.db)
private db: DataSource, private db: DataSource,
@Inject(DI.redis)
private redisClient: Redis.Redis,
@Inject(DI.usersRepository) @Inject(DI.usersRepository)
private usersRepository: UsersRepository, private usersRepository: UsersRepository,
@ -34,13 +41,15 @@ export class SignupService {
@Inject(DI.usedUsernamesRepository) @Inject(DI.usedUsernamesRepository)
private usedUsernamesRepository: UsedUsernamesRepository, private usedUsernamesRepository: UsedUsernamesRepository,
private utilityService: UtilityService,
private userEntityService: UserEntityService,
private idService: IdService, private idService: IdService,
private metaService: MetaService, private metaService: MetaService,
private utilityService: UtilityService,
private loggerService: LoggerService,
private instanceActorService: InstanceActorService, private instanceActorService: InstanceActorService,
private userEntityService: UserEntityService,
private usersChart: UsersChart, private usersChart: UsersChart,
) { ) {
this.logger = this.loggerService.getLogger('account:create');
} }
@bindThis @bindThis
@ -110,6 +119,13 @@ export class SignupService {
err ? rej(err) : res([publicKey, privateKey]), err ? rej(err) : res([publicKey, privateKey]),
)); ));
// 5分間のロックを取得
const lock = await this.redisClient.set(`account:create:lock:${username.toLowerCase()}`, Date.now(), 'EX', 60 * 5, 'NX');
if (lock === null) {
throw new Error('ALREADY_IN_PROGRESS');
}
try {
let account!: MiUser; let account!: MiUser;
// Start transaction // Start transaction
@ -151,6 +167,13 @@ export class SignupService {
this.usersChart.update(account, true); this.usersChart.update(account, true);
return { account, secret }; return { account, secret };
} catch (err) {
this.logger.error(`Failed to create account ${username}`, { error: err });
throw err;
} finally {
// 成功・失敗に関わらずロックを解除
await this.redisClient.unlink(`account:create:lock:${username.toLowerCase()}`);
}
} }
} }