enhance: Remote Notes Cleaning timeout bailout logic (#16752)

* enhance: Remote Notes Cleaning timeout bailout logic

Signed-off-by: eternal-flame-AD <yume@yumechi.jp>

* fix: dynamic limit did not propagate

Signed-off-by: eternal-flame-AD <yume@yumechi.jp>

* enhance: reload parameters each batch

Signed-off-by: eternal-flame-AD <yume@yumechi.jp>

---------

Signed-off-by: eternal-flame-AD <yume@yumechi.jp>
This commit is contained in:
饺子w (Yumechi)
2025-11-06 03:38:56 +00:00
committed by GitHub
parent 290fd8c7cc
commit 37a21cf54e
2 changed files with 49 additions and 13 deletions

View File

@@ -12,8 +12,7 @@
- Fix: ページのタイトルが長いとき、はみ出る問題を修正 - Fix: ページのタイトルが長いとき、はみ出る問題を修正
### Server ### Server
- - Enhance: Remote Notes Cleaningが複雑度が高いートの処理を中断せずに次のートから再開するように
## 2025.10.2 ## 2025.10.2

View File

@@ -51,6 +51,17 @@ export class CleanRemoteNotesProcessorService {
skipped: boolean; skipped: boolean;
transientErrors: number; transientErrors: number;
}> { }> {
const getConfig = () => {
return {
enabled: this.meta.enableRemoteNotesCleaning,
maxDuration: this.meta.remoteNotesCleaningMaxProcessingDurationInMinutes * 60 * 1000, // Convert minutes to milliseconds
// The date limit for the newest note to be considered for deletion.
// All notes newer than this limit will always be retained.
newestLimit: this.idService.gen(Date.now() - (1000 * 60 * 60 * 24 * this.meta.remoteNotesCleaningExpiryDaysForEachNotes)),
};
};
const initialConfig = getConfig();
if (!this.meta.enableRemoteNotesCleaning) { if (!this.meta.enableRemoteNotesCleaning) {
this.logger.info('Remote notes cleaning is disabled, skipping...'); this.logger.info('Remote notes cleaning is disabled, skipping...');
return { return {
@@ -64,13 +75,9 @@ export class CleanRemoteNotesProcessorService {
this.logger.info('cleaning remote notes...'); this.logger.info('cleaning remote notes...');
const maxDuration = this.meta.remoteNotesCleaningMaxProcessingDurationInMinutes * 60 * 1000; // Convert minutes to milliseconds
const startAt = Date.now(); const startAt = Date.now();
//#region queries //#region queries
// 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));
// The condition for removing the notes. // The condition for removing the notes.
// The note must be: // The note must be:
@@ -92,7 +99,7 @@ export class CleanRemoteNotesProcessorService {
const minId = (await this.notesRepository.createQueryBuilder('note') const minId = (await this.notesRepository.createQueryBuilder('note')
.select('MIN(note.id)', 'minId') .select('MIN(note.id)', 'minId')
.where({ .where({
id: LessThan(newestLimit), id: LessThan(initialConfig.newestLimit),
userHost: Not(IsNull()), userHost: Not(IsNull()),
replyId: IsNull(), replyId: IsNull(),
renoteId: IsNull(), renoteId: IsNull(),
@@ -155,12 +162,12 @@ export class CleanRemoteNotesProcessorService {
// | fff | fff | TRUE | // | fff | fff | TRUE |
// | ggg | ggg | FALSE | // | ggg | ggg | FALSE |
// //
const candidateNotesQuery = this.db.createQueryBuilder() const candidateNotesQuery = ({ limit }: { limit: number }) => this.db.createQueryBuilder()
.select(`"${candidateNotesCteName}"."id"`, 'id') .select(`"${candidateNotesCteName}"."id"`, 'id')
.addSelect('unremovable."id" IS NULL', 'isRemovable') .addSelect('unremovable."id" IS NULL', 'isRemovable')
.addSelect(`BOOL_OR("${candidateNotesCteName}"."isBase")`, 'isBase') .addSelect(`BOOL_OR("${candidateNotesCteName}"."isBase")`, 'isBase')
.addCommonTableExpression( .addCommonTableExpression(
`((SELECT "base".* FROM (${candidateNotesQueryBase.orderBy('note.id', 'ASC').limit(currentLimit).getQuery()}) AS "base") UNION ${candidateNotesQueryInductive.getQuery()})`, `((SELECT "base".* FROM (${candidateNotesQueryBase.orderBy('note.id', 'ASC').limit(limit).getQuery()}) AS "base") UNION ${candidateNotesQueryInductive.getQuery()})`,
candidateNotesCteName, candidateNotesCteName,
{ recursive: true }, { recursive: true },
) )
@@ -178,6 +185,11 @@ export class CleanRemoteNotesProcessorService {
let lowThroughputWarned = false; let lowThroughputWarned = false;
let transientErrors = 0; let transientErrors = 0;
for (;;) { for (;;) {
const { enabled, maxDuration, newestLimit } = getConfig();
if (!enabled) {
this.logger.info('Remote notes cleaning is disabled, processing stopped...');
break;
}
//#region check time //#region check time
const batchBeginAt = Date.now(); const batchBeginAt = Date.now();
@@ -205,13 +217,38 @@ export class CleanRemoteNotesProcessorService {
let noteIds = null; let noteIds = null;
try { try {
noteIds = await candidateNotesQuery.setParameters( noteIds = await candidateNotesQuery({ limit: currentLimit }).setParameters(
{ newestLimit, cursorLeft }, { newestLimit, cursorLeft },
).getRawMany<{ id: MiNote['id'], isRemovable: boolean, isBase: boolean }>(); ).getRawMany<{ id: MiNote['id'], isRemovable: boolean, isBase: boolean }>();
} catch (e) { } catch (e) {
if (currentLimit > minimumLimit && e instanceof QueryFailedError && e.driverError?.code === '57014') { if (e instanceof QueryFailedError && e.driverError?.code === '57014') {
// Statement timeout (maybe suddenly hit a large note tree), reduce the limit and try again // Statement timeout (maybe suddenly hit a large note tree), if possible, reduce the limit and try again
// continuous failures will eventually converge to currentLimit == minimumLimit and then throw // if not possible, skip the current batch of notes and find the next root note
if (currentLimit <= minimumLimit) {
job.log('Local note tree complexity is too high, finding next root note...');
const idWindow = await this.notesRepository.createQueryBuilder('note')
.select('id')
.where('note.id > :cursorLeft')
.andWhere(removalCriteria)
.andWhere({ replyId: IsNull(), renoteId: IsNull() })
.orderBy('note.id', 'ASC')
.limit(minimumLimit + 1)
.setParameters({ cursorLeft, newestLimit })
.getRawMany<{ id?: MiNote['id'] }>();
job.log(`Skipped note IDs: ${idWindow.slice(0, minimumLimit).map(id => id.id).join(', ')}`);
const lastId = idWindow.at(minimumLimit)?.id;
if (!lastId) {
job.log('No more notes to clean.');
break;
}
cursorLeft = lastId;
continue;
}
currentLimit = Math.max(minimumLimit, Math.floor(currentLimit * 0.25)); currentLimit = Math.max(minimumLimit, Math.floor(currentLimit * 0.25));
continue; continue;
} }