diff options
| author | anatawa12 <anatawa12@icloud.com> | 2025-08-04 18:39:08 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-08-04 18:39:08 +0900 |
| commit | 2f13f923a83c836fe08257f239e4fa34dba9c5e3 (patch) | |
| tree | 74c5620a054c9c9aaf7afe90a990335f246d81b8 /packages/backend/src/queue | |
| parent | New translations ja-jp.yml (Turkish) (#16359) (diff) | |
| download | misskey-2f13f923a83c836fe08257f239e4fa34dba9c5e3.tar.gz misskey-2f13f923a83c836fe08257f239e4fa34dba9c5e3.tar.bz2 misskey-2f13f923a83c836fe08257f239e4fa34dba9c5e3.zip | |
chore: リモートノートの削除条件をデータベース上で確認するように (#16351)
Diffstat (limited to 'packages/backend/src/queue')
| -rw-r--r-- | packages/backend/src/queue/processors/CleanRemoteNotesProcessorService.ts | 112 |
1 files changed, 61 insertions, 51 deletions
diff --git a/packages/backend/src/queue/processors/CleanRemoteNotesProcessorService.ts b/packages/backend/src/queue/processors/CleanRemoteNotesProcessorService.ts index 5b682e20b8..6c64d6aa39 100644 --- a/packages/backend/src/queue/processors/CleanRemoteNotesProcessorService.ts +++ b/packages/backend/src/queue/processors/CleanRemoteNotesProcessorService.ts @@ -5,7 +5,7 @@ import { setTimeout } from 'node:timers/promises'; import { Inject, Injectable } from '@nestjs/common'; -import { And, In, IsNull, LessThan, MoreThan, Not } from 'typeorm'; +import { And, Brackets, In, IsNull, LessThan, MoreThan, Not } from 'typeorm'; import { DI } from '@/di-symbols.js'; import type { MiMeta, MiNote, NoteFavoritesRepository, NotesRepository, UserNotePiningsRepository } from '@/models/_.js'; import type Logger from '@/logger.js'; @@ -67,68 +67,78 @@ export class CleanRemoteNotesProcessorService { newest: null as number | null, }; - let cursor: MiNote['id'] = this.idService.gen(Date.now() - (1000 * 60 * 60 * 24 * this.meta.remoteNotesCleaningExpiryDaysForEachNotes)); + // The date limit for the newest note to be considered for deletion. + // All notes newer than this limit will always be retained. + const newestLimit = this.idService.gen(Date.now() - (1000 * 60 * 60 * 24 * this.meta.remoteNotesCleaningExpiryDaysForEachNotes)); + + let cursor = '0'; // oldest note ID to start from while (true) { const batchBeginAt = Date.now(); - let notes: Pick<MiNote, 'id'>[] = await this.notesRepository.find({ - where: { - id: LessThan(cursor), - userHost: Not(IsNull()), - clippedCount: 0, - renoteCount: 0, - }, - take: MAX_NOTE_COUNT_PER_QUERY, - order: { - // 新しい順 - // https://github.com/misskey-dev/misskey/pull/16292#issuecomment-3139376314 - id: -1, - }, - select: ['id'], - }); + // We use string literals instead of query builder for several reasons: + // - for removeCondition, we need to use it in having clause, which is not supported by Brackets. + // - for recursive part, we need to preserve the order of columns, but typeorm query builder does not guarantee the order of columns in the result query - const fetchedCount = notes.length; + // The condition for removing the notes. + // The note must be: + // - old enough (older than the newestLimit) + // - a remote note (userHost is not null). + // - not have clipped + // - not have pinned on the user profile + // - not has been favorite by any user + const removeCondition = 'note.id < :newestLimit' + + ' AND note."clippedCount" = 0' + + ' AND note."userHost" IS NOT NULL' + // using both userId and noteId instead of just noteId to use index on user_note_pining table. + // This is safe because notes are only pinned by the user who created them. + + ' AND NOT EXISTS(SELECT 1 FROM "user_note_pining" WHERE "noteId" = note."id" AND "userId" = note."userId")' + // We cannot use userId trick because users can favorite notes from other users. + + ' AND NOT EXISTS(SELECT 1 FROM "note_favorite" WHERE "noteId" = note."id")' + ; - for (const note of notes) { - if (note.id < cursor) { - cursor = note.id; - } - } + // The initiator query contains the oldest ${MAX_NOTE_COUNT_PER_QUERY} remote non-clipped notes + const initiatorQuery = ` + SELECT "note"."id" AS "id", "note"."replyId" AS "replyId", "note"."renoteId" AS "renoteId", "note"."id" AS "initiatorId" + FROM "note" "note" WHERE ${removeCondition} AND "note"."id" > :cursor ORDER BY "note"."id" ASC LIMIT ${MAX_NOTE_COUNT_PER_QUERY}`; - const pinings = notes.length === 0 ? [] : await this.userNotePiningsRepository.find({ - where: { - noteId: In(notes.map(note => note.id)), - }, - select: ['noteId'], - }); + // The union query queries the related notes and replies related to the initiator query + const unionQuery = ` + SELECT "note"."id", "note"."replyId", "note"."renoteId", rn."initiatorId" + FROM "note" "note" + INNER JOIN "related_notes" "rn" + ON "note"."replyId" = rn.id + OR "note"."renoteId" = rn.id + OR "note"."id" = rn."replyId" + OR "note"."id" = rn."renoteId" + `; + const recursiveQuery = `(${initiatorQuery}) UNION (${unionQuery})`; - notes = notes.filter(note => { - return !pinings.some(pining => pining.noteId === note.id); - }); + const removableInitiatorNotesQuery = this.notesRepository.createQueryBuilder('note') + .select('rn."initiatorId"') + .innerJoin('related_notes', 'rn', 'note.id = rn.id') + .groupBy('rn."initiatorId"') + .having(`bool_and(${removeCondition})`); - const favorites = notes.length === 0 ? [] : await this.noteFavoritesRepository.find({ - where: { - noteId: In(notes.map(note => note.id)), - }, - select: ['noteId'], - }); + const notesQuery = this.notesRepository.createQueryBuilder('note') + .addCommonTableExpression(recursiveQuery, 'related_notes', { recursive: true }) + .select('note.id', 'id') + .addSelect('rn."initiatorId"') + .innerJoin('related_notes', 'rn', 'note.id = rn.id') + .where(`rn."initiatorId" IN (${ removableInitiatorNotesQuery.getQuery() })`) + .setParameters({ cursor, newestLimit }); - notes = notes.filter(note => { - return !favorites.some(favorite => favorite.noteId === note.id); - }); + const notes: { id: MiNote['id'], initiatorId: MiNote['id'] }[] = await notesQuery.getRawMany(); - const replies = notes.length === 0 ? [] : await this.notesRepository.find({ - where: { - replyId: In(notes.map(note => note.id)), - userHost: IsNull(), - }, - select: ['replyId'], - }); + const fetchedCount = notes.length; - notes = notes.filter(note => { - return !replies.some(reply => reply.replyId === note.id); - }); + // update the cursor to the newest initiatorId found in the fetched notes. + // We don't use 'id' since the note can be newer than the initiator note. + for (const note of notes) { + if (cursor < note.initiatorId) { + cursor = note.initiatorId; + } + } if (notes.length > 0) { await this.notesRepository.delete(notes.map(note => note.id)); |