diff --git a/packages/backend/src/postgres.ts b/packages/backend/src/postgres.ts index b06895fcc9..be225ef1ed 100644 --- a/packages/backend/src/postgres.ts +++ b/packages/backend/src/postgres.ts @@ -5,7 +5,7 @@ // https://github.com/typeorm/typeorm/issues/2400 import pg from 'pg'; -import { DataSource, Logger, type QueryRunner } from 'typeorm'; +import { DataSource, EntityManager, Logger, type QueryRunner } from 'typeorm'; import * as highlight from 'cli-highlight'; import { entities as charts } from '@/core/chart/entities.js'; import { Config } from '@/config.js'; @@ -262,6 +262,33 @@ export const entities = [ const log = process.env.NODE_ENV !== 'production'; +/** + * Execute query with extended timeout. + * This can be used to execute long-running queries like deleting many rows CASCADE-ly. + * + * TODO: consider remove this function when we removed CASCADE delete. + */ +export async function extendTimeoutQuery(dataSource: DataSource, query: (manager: EntityManager) => Promise): Promise { + const queryRunner = dataSource.createQueryRunner('master'); + const manager = dataSource.createEntityManager(queryRunner); + const extendedTimeout = 1000 * 100; // 100 sec for now. How long should it be? + try { + await queryRunner.connect(); + try { + // it looks postgres doesn't support statement_timeout with placeholder + // await manager.query(`set statement_timeout to $1`, [extendedTimeout]); + await manager.query(`set statement_timeout to ${extendedTimeout}`); + await query(manager); + } finally { + await manager.query(`set statement_timeout to ${default_statement_timeout}`); + } + } finally { + await queryRunner.release(); + } +} + +export const default_statement_timeout = 1000 * 10; + export function createPostgresDataSource(config: Config) { return new DataSource({ type: 'postgres', @@ -271,7 +298,7 @@ export function createPostgresDataSource(config: Config) { password: config.db.pass, database: config.db.db, extra: { - statement_timeout: 1000 * 10, + statement_timeout: default_statement_timeout, ...config.db.extra, }, ...(config.dbReplications ? { diff --git a/packages/backend/src/queue/processors/DeleteAccountProcessorService.ts b/packages/backend/src/queue/processors/DeleteAccountProcessorService.ts index 14a53e0c42..55aaff8cf9 100644 --- a/packages/backend/src/queue/processors/DeleteAccountProcessorService.ts +++ b/packages/backend/src/queue/processors/DeleteAccountProcessorService.ts @@ -4,16 +4,18 @@ */ import { Inject, Injectable } from '@nestjs/common'; -import { MoreThan } from 'typeorm'; +import { DataSource, MoreThan } from 'typeorm'; import { DI } from '@/di-symbols.js'; import type { DriveFilesRepository, NotesRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js'; +import { MiUser } from '@/models/User.js'; import type Logger from '@/logger.js'; import { DriveService } from '@/core/DriveService.js'; import type { MiDriveFile } from '@/models/DriveFile.js'; -import type { MiNote } from '@/models/Note.js'; +import { MiNote } from '@/models/Note.js'; import { EmailService } from '@/core/EmailService.js'; import { bindThis } from '@/decorators.js'; import { SearchService } from '@/core/SearchService.js'; +import { extendTimeoutQuery } from '@/postgres.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; import type * as Bull from 'bullmq'; import type { DbUserDeleteJobData } from '../types.js'; @@ -23,6 +25,9 @@ export class DeleteAccountProcessorService { private logger: Logger; constructor( + @Inject(DI.db) + private db: DataSource, + @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -73,7 +78,9 @@ export class DeleteAccountProcessorService { cursor = notes.at(-1)?.id ?? null; - await this.notesRepository.delete(notes.map(note => note.id)); + await extendTimeoutQuery(this.db, async (manager) => { + await manager.delete(MiNote, notes.map(note => note.id)); + }); for (const note of notes) { await this.searchService.unindexNote(note); @@ -125,7 +132,9 @@ export class DeleteAccountProcessorService { if (job.data.soft) { // nop } else { - await this.usersRepository.delete(job.data.user.id); + await extendTimeoutQuery(this.db, async (manager) => { + await manager.delete(MiUser, job.data.user.id); + }); } return 'Account deleted';