fix(backend): Atomically mark remote account deletions
Some checks failed
Lint / lint (backend) (push) Blocked by required conditions
Lint / lint (frontend) (push) Blocked by required conditions
Lint / lint (frontend-embed) (push) Blocked by required conditions
Lint / lint (frontend-shared) (push) Blocked by required conditions
Lint / lint (misskey-bubble-game) (push) Blocked by required conditions
Lint / lint (misskey-js) (push) Blocked by required conditions
Lint / lint (misskey-reversi) (push) Blocked by required conditions
Lint / lint (sw) (push) Blocked by required conditions
Lint / typecheck (backend) (push) Blocked by required conditions
Lint / typecheck (misskey-js) (push) Blocked by required conditions
Lint / typecheck (sw) (push) Blocked by required conditions
Lint / pnpm_install (push) Successful in 2m3s
Publish Docker image / Build (push) Successful in 4m27s
Test (backend) / e2e (22.11.0) (push) Has been cancelled
Test (backend) / unit (22.11.0) (push) Has been cancelled
Test (production install and build) / production (22.11.0) (push) Has been cancelled
Some checks failed
Lint / lint (backend) (push) Blocked by required conditions
Lint / lint (frontend) (push) Blocked by required conditions
Lint / lint (frontend-embed) (push) Blocked by required conditions
Lint / lint (frontend-shared) (push) Blocked by required conditions
Lint / lint (misskey-bubble-game) (push) Blocked by required conditions
Lint / lint (misskey-js) (push) Blocked by required conditions
Lint / lint (misskey-reversi) (push) Blocked by required conditions
Lint / lint (sw) (push) Blocked by required conditions
Lint / typecheck (backend) (push) Blocked by required conditions
Lint / typecheck (misskey-js) (push) Blocked by required conditions
Lint / typecheck (sw) (push) Blocked by required conditions
Lint / pnpm_install (push) Successful in 2m3s
Publish Docker image / Build (push) Successful in 4m27s
Test (backend) / e2e (22.11.0) (push) Has been cancelled
Test (backend) / unit (22.11.0) (push) Has been cancelled
Test (production install and build) / production (22.11.0) (push) Has been cancelled
Signed-off-by: eternal-flame-AD <yume@yumechi.jp>
This commit is contained in:
parent
416d71002a
commit
04704598b6
4 changed files with 67 additions and 18 deletions
16
packages/backend/migration/1732071810971-IndexUserDeleted.js
Normal file
16
packages/backend/migration/1732071810971-IndexUserDeleted.js
Normal file
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project and yumechi
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class IndexUserDeleted1732071810971 {
|
||||
name = 'IndexUserDeleted1732071810971'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`CREATE INDEX IF NOT EXISTS "IDX_199b79e682bdc5ba946f491686" ON "user" ("isDeleted")`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`DROP INDEX IF EXISTS "IDX_199b79e682bdc5ba946f491686"`);
|
||||
}
|
||||
}
|
|
@ -509,19 +509,12 @@ export class ApInboxService {
|
|||
return `skip: delete actor ${actor.uri} !== ${uri}`;
|
||||
}
|
||||
|
||||
const user = await this.usersRepository.findOneBy({ id: actor.id });
|
||||
if (user == null) {
|
||||
return 'skip: actor not found';
|
||||
} else if (user.isDeleted) {
|
||||
if (!(await this.usersRepository.update({ id: actor.id, isDeleted: false }, { isDeleted: true })).affected) {
|
||||
return 'skip: already deleted';
|
||||
}
|
||||
|
||||
const job = await this.queueService.createDeleteAccountJob(actor);
|
||||
|
||||
await this.usersRepository.update(actor.id, {
|
||||
isDeleted: true,
|
||||
});
|
||||
|
||||
this.globalEventService.publishInternalEvent('remoteUserUpdated', { id: actor.id });
|
||||
|
||||
return `ok: queued ${job.name} ${job.id}`;
|
||||
|
|
|
@ -557,7 +557,9 @@ export class ApPersonService implements OnModuleInit {
|
|||
if (moving) updates.movedAt = new Date();
|
||||
|
||||
// Update user
|
||||
await this.usersRepository.update(exist.id, updates);
|
||||
if (!(await this.usersRepository.update({ id: exist.id, isDeleted: false }, updates)).affected) {
|
||||
return 'skip';
|
||||
}
|
||||
|
||||
if (person.publicKey) {
|
||||
await this.userPublickeysRepository.update({ userId: exist.id }, {
|
||||
|
@ -662,7 +664,7 @@ export class ApPersonService implements OnModuleInit {
|
|||
|
||||
@bindThis
|
||||
public async updateFeatured(userId: MiUser['id'], resolver?: Resolver): Promise<void> {
|
||||
const user = await this.usersRepository.findOneByOrFail({ id: userId });
|
||||
const user = await this.usersRepository.findOneByOrFail({ id: userId, isDeleted: false });
|
||||
if (!this.userEntityService.isRemoteUser(user)) return;
|
||||
if (!user.featured) return;
|
||||
|
||||
|
|
|
@ -4,9 +4,9 @@
|
|||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { MoreThan } from 'typeorm';
|
||||
import { DataSource, MoreThan, QueryFailedError, TypeORMError } from 'typeorm';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { DriveFilesRepository, NotesRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js';
|
||||
import { MiUser, type DriveFilesRepository, type NotesRepository, type UserProfilesRepository, type UsersRepository } from '@/models/_.js';
|
||||
import type Logger from '@/logger.js';
|
||||
import { DriveService } from '@/core/DriveService.js';
|
||||
import type { MiDriveFile } from '@/models/DriveFile.js';
|
||||
|
@ -26,6 +26,9 @@ export class DeleteAccountProcessorService {
|
|||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
|
||||
@Inject(DI.db)
|
||||
private db: DataSource,
|
||||
|
||||
@Inject(DI.userProfilesRepository)
|
||||
private userProfilesRepository: UserProfilesRepository,
|
||||
|
||||
|
@ -121,13 +124,48 @@ export class DeleteAccountProcessorService {
|
|||
}
|
||||
}
|
||||
|
||||
// soft指定されている場合は物理削除しない
|
||||
if (job.data.soft) {
|
||||
// nop
|
||||
} else {
|
||||
await this.usersRepository.delete(job.data.user.id);
|
||||
// Deadlockが発生した場合にリトライする
|
||||
for (let remaining = 3; remaining > 0; remaining--) {
|
||||
try {
|
||||
// soft指定されている場合は物理削除しない
|
||||
await this.db.transaction(async txn => {
|
||||
// soft指定してもデータをすべで削除する
|
||||
await txn.delete(MiUser, user.id);
|
||||
if (job.data.soft) {
|
||||
await txn.insert(MiUser, {
|
||||
...user,
|
||||
isRoot: false,
|
||||
updatedAt: new Date(),
|
||||
emojis: [],
|
||||
hideOnlineStatus: true,
|
||||
followersCount: 0,
|
||||
followingCount: 0,
|
||||
avatarUrl: null,
|
||||
avatarId: null,
|
||||
notesCount: 0,
|
||||
inbox: null,
|
||||
sharedInbox: null,
|
||||
featured: null,
|
||||
uri: null,
|
||||
followersUri: null,
|
||||
token: null,
|
||||
isDeleted: true,
|
||||
});
|
||||
}
|
||||
});
|
||||
return 'Account deleted';
|
||||
} catch (e) {
|
||||
// 40P01 = deadlock_detected
|
||||
// https://www.postgresql.org/docs/current/errcodes-appendix.html
|
||||
if (remaining > 0 && e instanceof QueryFailedError && e.driverError.code === '40P01') {
|
||||
this.logger.warn(`Deadlock occurred, retrying after 1s... [${remaining - 1} remaining]`);
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
continue;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
return 'Account deleted';
|
||||
throw new Error('Failed to delete account: too many deadlocks');
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue