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 * as Redis from 'ioredis';
import { bindThis } from '@/decorators.js';
import { DI } from '@/di-symbols.js';
import type Logger from '@/logger.js';
@ -20,6 +21,8 @@ export class DeleteAccountService {
public logger: Logger;
constructor(
@Inject(DI.redis)
private redisClient: Redis.Redis,
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@ -29,7 +32,7 @@ export class DeleteAccountService {
private globalEventService: GlobalEventService,
private loggerService: LoggerService,
) {
this.logger = this.loggerService.getLogger('delete-account');
this.logger = this.loggerService.getLogger('account:delete');
}
@bindThis
@ -39,19 +42,38 @@ export class DeleteAccountService {
const _user = await this.usersRepository.findOneByOrFail({ id: user.id });
if (_user.isRoot) throw new Error('cannot delete a root account');
// 物理削除する前にDelete activityを送信する
await this.userSuspendService.doPostSuspend(user).catch(err => this.logger.error(err));
// 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;
}
this.queueService.createDeleteAccountJob(user, {
force: me ? await this.roleService.isModerator(me) : false,
soft: soft,
});
// noinspection ES6MissingAwait APIで呼び出される際にタイムアウトされないように
(async () => {
try {
// 物理削除する前にDelete activityを送信する
await this.userSuspendService.doPostSuspend(user).catch(err => this.logger.error(err));
await this.usersRepository.update(user.id, {
isDeleted: true,
});
// noinspection ES6MissingAwait
this.queueService.createDeleteAccountJob(user, {
force: me ? await this.roleService.isModerator(me) : false,
soft: soft,
});
this.globalEventService.publishInternalEvent('userChangeDeletedState', { id: user.id, isDeleted: true });
await this.usersRepository.update(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

View file

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

View file

@ -6,27 +6,34 @@
import { generateKeyPair } from 'node:crypto';
import { Inject, Injectable } from '@nestjs/common';
import bcrypt from 'bcryptjs';
import * as Redis from 'ioredis';
import { DataSource, IsNull } from 'typeorm';
import { bindThis } from '@/decorators.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 { MiUser } from '@/models/User.js';
import { MiUserProfile } from '@/models/UserProfile.js';
import { IdService } from '@/core/IdService.js';
import { MiUserKeypair } from '@/models/UserKeypair.js';
import { MiUsedUsername } from '@/models/UsedUsername.js';
import generateUserToken from '@/misc/generate-native-user-token.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 { IdService } from '@/core/IdService.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()
export class SignupService {
public logger: Logger;
constructor(
@Inject(DI.db)
private db: DataSource,
@Inject(DI.redis)
private redisClient: Redis.Redis,
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@ -34,13 +41,15 @@ export class SignupService {
@Inject(DI.usedUsernamesRepository)
private usedUsernamesRepository: UsedUsernamesRepository,
private utilityService: UtilityService,
private userEntityService: UserEntityService,
private idService: IdService,
private metaService: MetaService,
private utilityService: UtilityService,
private loggerService: LoggerService,
private instanceActorService: InstanceActorService,
private userEntityService: UserEntityService,
private usersChart: UsersChart,
) {
this.logger = this.loggerService.getLogger('account:create');
}
@bindThis
@ -110,47 +119,61 @@ export class SignupService {
err ? rej(err) : res([publicKey, privateKey]),
));
let account!: MiUser;
// 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');
}
// Start transaction
await this.db.transaction(async transactionalEntityManager => {
const exist = await transactionalEntityManager.findOneBy(MiUser, {
usernameLower: username.toLowerCase(),
host: IsNull(),
try {
let account!: MiUser;
// Start transaction
await this.db.transaction(async transactionalEntityManager => {
const exist = await transactionalEntityManager.findOneBy(MiUser, {
usernameLower: username.toLowerCase(),
host: IsNull(),
});
if (exist) throw new Error(' the username is already used');
account = await transactionalEntityManager.save(new MiUser({
id: this.idService.gen(),
username: username,
usernameLower: username.toLowerCase(),
host: this.utilityService.toPunyNullable(host),
token: secret,
isRoot: isTheFirstUser,
}));
await transactionalEntityManager.save(new MiUserKeypair({
publicKey: keyPair[0],
privateKey: keyPair[1],
userId: account.id,
}));
await transactionalEntityManager.save(new MiUserProfile({
userId: account.id,
autoAcceptFollowed: true,
password: hash,
}));
await transactionalEntityManager.save(new MiUsedUsername({
createdAt: new Date(),
username: username.toLowerCase(),
}));
});
if (exist) throw new Error(' the username is already used');
this.usersChart.update(account, true);
account = await transactionalEntityManager.save(new MiUser({
id: this.idService.gen(),
username: username,
usernameLower: username.toLowerCase(),
host: this.utilityService.toPunyNullable(host),
token: secret,
isRoot: isTheFirstUser,
}));
await transactionalEntityManager.save(new MiUserKeypair({
publicKey: keyPair[0],
privateKey: keyPair[1],
userId: account.id,
}));
await transactionalEntityManager.save(new MiUserProfile({
userId: account.id,
autoAcceptFollowed: true,
password: hash,
}));
await transactionalEntityManager.save(new MiUsedUsername({
createdAt: new Date(),
username: username.toLowerCase(),
}));
});
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()}`);
}
}
}