-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
feat: お知らせを特定日時で自動アーカイブできるようにする #17704
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
41bead3
90c7139
cb9edda
1b6583a
f3fbc6d
8a9df02
2f4ae54
e34a200
bb46067
9bbc8c6
54c1745
8a6a716
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| /* | ||
| * SPDX-FileCopyrightText: syuilo and misskey-project | ||
| * SPDX-License-Identifier: AGPL-3.0-only | ||
| */ | ||
|
|
||
| export class AutoArchiveAnnouncements1783913268595 { | ||
| name = 'AutoArchiveAnnouncements1783913268595'; | ||
|
|
||
| async up(queryRunner) { | ||
| await queryRunner.query(`ALTER TABLE "announcement" ADD "autoArchiveAt" TIMESTAMP WITH TIME ZONE`); | ||
| await queryRunner.query(`COMMENT ON COLUMN "announcement"."autoArchiveAt" IS 'The date after which the Announcement is automatically archived.'`); | ||
| } | ||
|
|
||
| async down(queryRunner) { | ||
| await queryRunner.query(`COMMENT ON COLUMN "announcement"."autoArchiveAt" IS 'The date after which the Announcement is automatically archived.'`); | ||
| await queryRunner.query(`ALTER TABLE "announcement" DROP COLUMN "autoArchiveAt"`); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,6 +8,7 @@ import { Inject, Injectable } from '@nestjs/common'; | |
| import { MetricsTime, type JobType } from 'bullmq'; | ||
| import type { IActivity } from '@/core/activitypub/type.js'; | ||
| import type { MiDriveFile } from '@/models/DriveFile.js'; | ||
| import type { MiAnnouncement } from '@/models/Announcement.js'; | ||
| import type { MiWebhook, WebhookEventTypes } from '@/models/Webhook.js'; | ||
| import type { MiSystemWebhook, SystemWebhookEventType } from '@/models/SystemWebhook.js'; | ||
| import type { Config } from '@/config.js'; | ||
|
|
@@ -20,6 +21,7 @@ import type { Packed } from '@/misc/json-schema.js'; | |
| import { type UserWebhookPayload } from './UserWebhookService.js'; | ||
| import type { | ||
| DbJobData, | ||
| ArchiveAnnouncementJobData, | ||
| DeliverJobData, | ||
| RelationshipJobData, | ||
| SystemWebhookDeliverJobData, | ||
|
|
@@ -247,6 +249,44 @@ export class QueueService { | |
| }); | ||
| } | ||
|
|
||
| private announcementArchiveJobId(announcementId: MiAnnouncement['id'], autoArchiveAt: Date): string { | ||
| return `archiveAnnouncement-${announcementId}-${autoArchiveAt.getTime()}`; | ||
| } | ||
|
|
||
| @bindThis | ||
| public async scheduleAnnouncementArchive(announcementId: MiAnnouncement['id'], autoArchiveAt: Date): Promise<void> { | ||
| const jobId = this.announcementArchiveJobId(announcementId, autoArchiveAt); | ||
| const existingJob = await this.systemQueue.getJob(jobId); | ||
| if (existingJob != null) { | ||
| const state = await existingJob.getState(); | ||
| if (state !== 'completed' && state !== 'failed') return; | ||
| await this.systemQueue.remove(jobId); | ||
| } | ||
|
|
||
| const data: ArchiveAnnouncementJobData = { | ||
| announcementId, | ||
| autoArchiveAt: autoArchiveAt.getTime(), | ||
| }; | ||
|
|
||
| await this.systemQueue.add('archiveAnnouncement', data, { | ||
| jobId, | ||
| delay: Math.max(0, autoArchiveAt.getTime() - Date.now()), | ||
| removeOnComplete: { | ||
| age: 3600 * 24 * 7, // keep up to 7 days | ||
| count: 30, | ||
| }, | ||
| removeOnFail: { | ||
| age: 3600 * 24 * 7, // keep up to 7 days | ||
| count: 100, | ||
| }, | ||
| }); | ||
|
Comment on lines
+256
to
+282
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
# Locate system-queue defaults and archive processor behavior.
rg -n -C 4 'queue:system|defaultJobOptions|attempts:|archiveAnnouncement' packages/backend
# Locate coverage for scheduling and failed-job recovery.
rg -n -C 3 'scheduleAnnouncementArchive|ArchiveAnnouncement|autoArchiveAt' \
packages/backend/test packages/backend/srcRepository: misskey-dev/misskey Length of output: 50375 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '\n== QueueModule.ts outline ==\n'
ast-grep outline packages/backend/src/core/QueueModule.ts --view expanded || true
printf '\n== QueueModule.ts relevant lines ==\n'
sed -n '1,140p' packages/backend/src/core/QueueModule.ts | cat -n
printf '\n== search baseQueueOptions / defaultJobOptions ==\n'
rg -n -C 4 'function baseQueueOptions|const baseQueueOptions|defaultJobOptions|attempts' packages/backend/src/core packages/backend/src/queue
printf '\n== base queue helper file candidates ==\n'
fd -a 'QueueModule.ts|queue.*ts|baseQueueOptions' packages/backend/src | head -n 50Repository: misskey-dev/misskey Length of output: 17366 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '\n== packages/backend/src/queue/const.ts ==\n'
sed -n '1,220p' packages/backend/src/queue/const.ts | cat -n
printf '\n== search for defaultJobOptions in repository ==\n'
rg -n -C 3 'defaultJobOptions|attempts\s*[:=]' packages/backend/src packages/backend/test
printf '\n== search for system queue add sites ==\n'
rg -n -C 2 "systemQueue\.add\(" packages/backend/srcRepository: misskey-dev/misskey Length of output: 7389 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '\n== QueueProcessorService worker setup ==\n'
sed -n '1,260p' packages/backend/src/queue/QueueProcessorService.ts | cat -n
printf '\n== search for Worker creation / retry-related options ==\n'
rg -n -C 4 'new Bull\.Worker|Worker\(|attempts|backoff|autorun|concurrency|useWorker' packages/backend/src/queue packages/backend/src/core
printf '\n== bullmq version and docs references in repo ==\n'
rg -n -C 2 'bullmq|retrying-failing-jobs|defaultJobOptions' package.json pnpm-lock.yaml packages/backend/package.json README.md packages/backend/srcRepository: misskey-dev/misskey Length of output: 50375 🌐 Web query:
💡 Result: In BullMQ, the default value for the attempts option is 1 [1][2][3]. This means that if the attempts option is omitted, the job will be executed once and will not be retried upon failure [4][3]. To enable automatic retries, you must explicitly set the attempts option to a value greater than 1 [4]. Citations:
Add retries for announcement archive jobs. BullMQ defaults 🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| @bindThis | ||
| public async clearAnnouncementArchive(announcementId: MiAnnouncement['id'], autoArchiveAt: Date): Promise<void> { | ||
| await this.systemQueue.remove(this.announcementArchiveJobId(announcementId, autoArchiveAt)); | ||
| } | ||
|
|
||
| @bindThis | ||
| public createDeleteDriveFilesJob(user: ThinUser) { | ||
| return this.dbQueue.add('deleteDriveFiles', { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| /* | ||
| * SPDX-FileCopyrightText: syuilo and misskey-project | ||
| * SPDX-License-Identifier: AGPL-3.0-only | ||
| */ | ||
|
|
||
| import { Injectable } from '@nestjs/common'; | ||
| import type * as Bull from 'bullmq'; | ||
| import type Logger from '@/logger.js'; | ||
| import { bindThis } from '@/decorators.js'; | ||
| import { AnnouncementService } from '@/core/AnnouncementService.js'; | ||
| import type { ArchiveAnnouncementJobData } from '../types.js'; | ||
| import { QueueLoggerService } from '../QueueLoggerService.js'; | ||
|
|
||
| @Injectable() | ||
| export class ArchiveAnnouncementProcessorService { | ||
| private logger: Logger; | ||
|
|
||
| constructor( | ||
| private announcementService: AnnouncementService, | ||
| private queueLoggerService: QueueLoggerService, | ||
| ) { | ||
| this.logger = this.queueLoggerService.logger.createSubLogger('archive-announcement'); | ||
| } | ||
|
|
||
| @bindThis | ||
| public async process(job: Bull.Job<ArchiveAnnouncementJobData>): Promise<void> { | ||
| const archived = await this.announcementService.archiveAnnouncement( | ||
| job.data.announcementId, | ||
| new Date(job.data.autoArchiveAt), | ||
| ); | ||
|
|
||
| if (archived) { | ||
| this.logger.succ(`Archived announcement ${job.data.announcementId}.`); | ||
| } else { | ||
| this.logger.debug(`Announcement ${job.data.announcementId} no longer matches the scheduled archive job.`); | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Promise.allinonModuleInitcan abort bootstrap on a single failure.If
scheduleAnnouncementArchiverejects for any one announcement (e.g. transient Redis issue),Promise.allrejects the whole batch, and since this runs inOnModuleInit, it can fail application bootstrap for an unrelated transient error, risking a crash-loop/outage on deploy or restart.🔧 Suggested fix: isolate per-item failures
📝 Committable suggestion
🤖 Prompt for AI Agents