enhance(backend): sentry integration for job queues

This commit is contained in:
syuilo 2024-06-06 09:32:04 +09:00
parent 43cccaaee9
commit d4a8c63264

View File

@ -5,6 +5,7 @@
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
import * as Bull from 'bullmq'; import * as Bull from 'bullmq';
import * as Sentry from '@sentry/node';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type Logger from '@/logger.js'; import type Logger from '@/logger.js';
@ -135,199 +136,271 @@ export class QueueProcessorService implements OnApplicationShutdown {
} }
//#region system //#region system
this.systemQueueWorker = new Bull.Worker(QUEUE.SYSTEM, (job) => { {
switch (job.name) { const processer = (job: Bull.Job) => {
case 'tickCharts': return this.tickChartsProcessorService.process(); switch (job.name) {
case 'resyncCharts': return this.resyncChartsProcessorService.process(); case 'tickCharts': return this.tickChartsProcessorService.process();
case 'cleanCharts': return this.cleanChartsProcessorService.process(); case 'resyncCharts': return this.resyncChartsProcessorService.process();
case 'aggregateRetention': return this.aggregateRetentionProcessorService.process(); case 'cleanCharts': return this.cleanChartsProcessorService.process();
case 'checkExpiredMutings': return this.checkExpiredMutingsProcessorService.process(); case 'aggregateRetention': return this.aggregateRetentionProcessorService.process();
case 'clean': return this.cleanProcessorService.process(); case 'checkExpiredMutings': return this.checkExpiredMutingsProcessorService.process();
default: throw new Error(`unrecognized job type ${job.name} for system`); case 'clean': return this.cleanProcessorService.process();
} default: throw new Error(`unrecognized job type ${job.name} for system`);
}, { }
...baseQueueOptions(this.config, QUEUE.SYSTEM), };
autorun: false,
});
const systemLogger = this.logger.createSubLogger('system'); this.systemQueueWorker = new Bull.Worker(QUEUE.SYSTEM, (job) => {
if (this.config.sentryForBackend) {
return Sentry.startSpan({ name: 'Queue: System: ' + job.name }, () => processer(job));
} else {
return processer(job);
}
}, {
...baseQueueOptions(this.config, QUEUE.SYSTEM),
autorun: false,
});
this.systemQueueWorker const systemLogger = this.logger.createSubLogger('system');
.on('active', (job) => systemLogger.debug(`active id=${job.id}`))
.on('completed', (job, result) => systemLogger.debug(`completed(${result}) id=${job.id}`)) this.systemQueueWorker
.on('failed', (job, err) => systemLogger.warn(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) })) .on('active', (job) => systemLogger.debug(`active id=${job.id}`))
.on('error', (err: Error) => systemLogger.error(`error ${err.stack}`, { e: renderError(err) })) .on('completed', (job, result) => systemLogger.debug(`completed(${result}) id=${job.id}`))
.on('stalled', (jobId) => systemLogger.warn(`stalled id=${jobId}`)); .on('failed', (job, err) => systemLogger.warn(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) }))
.on('error', (err: Error) => systemLogger.error(`error ${err.stack}`, { e: renderError(err) }))
.on('stalled', (jobId) => systemLogger.warn(`stalled id=${jobId}`));
}
//#endregion //#endregion
//#region db //#region db
this.dbQueueWorker = new Bull.Worker(QUEUE.DB, (job) => { {
switch (job.name) { const processer = (job: Bull.Job) => {
case 'deleteDriveFiles': return this.deleteDriveFilesProcessorService.process(job); switch (job.name) {
case 'exportCustomEmojis': return this.exportCustomEmojisProcessorService.process(job); case 'deleteDriveFiles': return this.deleteDriveFilesProcessorService.process(job);
case 'exportNotes': return this.exportNotesProcessorService.process(job); case 'exportCustomEmojis': return this.exportCustomEmojisProcessorService.process(job);
case 'exportClips': return this.exportClipsProcessorService.process(job); case 'exportNotes': return this.exportNotesProcessorService.process(job);
case 'exportFavorites': return this.exportFavoritesProcessorService.process(job); case 'exportClips': return this.exportClipsProcessorService.process(job);
case 'exportFollowing': return this.exportFollowingProcessorService.process(job); case 'exportFavorites': return this.exportFavoritesProcessorService.process(job);
case 'exportMuting': return this.exportMutingProcessorService.process(job); case 'exportFollowing': return this.exportFollowingProcessorService.process(job);
case 'exportBlocking': return this.exportBlockingProcessorService.process(job); case 'exportMuting': return this.exportMutingProcessorService.process(job);
case 'exportUserLists': return this.exportUserListsProcessorService.process(job); case 'exportBlocking': return this.exportBlockingProcessorService.process(job);
case 'exportAntennas': return this.exportAntennasProcessorService.process(job); case 'exportUserLists': return this.exportUserListsProcessorService.process(job);
case 'importFollowing': return this.importFollowingProcessorService.process(job); case 'exportAntennas': return this.exportAntennasProcessorService.process(job);
case 'importFollowingToDb': return this.importFollowingProcessorService.processDb(job); case 'importFollowing': return this.importFollowingProcessorService.process(job);
case 'importMuting': return this.importMutingProcessorService.process(job); case 'importFollowingToDb': return this.importFollowingProcessorService.processDb(job);
case 'importBlocking': return this.importBlockingProcessorService.process(job); case 'importMuting': return this.importMutingProcessorService.process(job);
case 'importBlockingToDb': return this.importBlockingProcessorService.processDb(job); case 'importBlocking': return this.importBlockingProcessorService.process(job);
case 'importUserLists': return this.importUserListsProcessorService.process(job); case 'importBlockingToDb': return this.importBlockingProcessorService.processDb(job);
case 'importCustomEmojis': return this.importCustomEmojisProcessorService.process(job); case 'importUserLists': return this.importUserListsProcessorService.process(job);
case 'importAntennas': return this.importAntennasProcessorService.process(job); case 'importCustomEmojis': return this.importCustomEmojisProcessorService.process(job);
case 'deleteAccount': return this.deleteAccountProcessorService.process(job); case 'importAntennas': return this.importAntennasProcessorService.process(job);
default: throw new Error(`unrecognized job type ${job.name} for db`); case 'deleteAccount': return this.deleteAccountProcessorService.process(job);
} default: throw new Error(`unrecognized job type ${job.name} for db`);
}, { }
...baseQueueOptions(this.config, QUEUE.DB), };
autorun: false,
});
const dbLogger = this.logger.createSubLogger('db'); this.dbQueueWorker = new Bull.Worker(QUEUE.DB, (job) => {
if (this.config.sentryForBackend) {
return Sentry.startSpan({ name: 'Queue: DB: ' + job.name }, () => processer(job));
} else {
return processer(job);
}
}, {
...baseQueueOptions(this.config, QUEUE.DB),
autorun: false,
});
this.dbQueueWorker const dbLogger = this.logger.createSubLogger('db');
.on('active', (job) => dbLogger.debug(`active id=${job.id}`))
.on('completed', (job, result) => dbLogger.debug(`completed(${result}) id=${job.id}`)) this.dbQueueWorker
.on('failed', (job, err) => dbLogger.warn(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) })) .on('active', (job) => dbLogger.debug(`active id=${job.id}`))
.on('error', (err: Error) => dbLogger.error(`error ${err.stack}`, { e: renderError(err) })) .on('completed', (job, result) => dbLogger.debug(`completed(${result}) id=${job.id}`))
.on('stalled', (jobId) => dbLogger.warn(`stalled id=${jobId}`)); .on('failed', (job, err) => dbLogger.warn(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) }))
.on('error', (err: Error) => dbLogger.error(`error ${err.stack}`, { e: renderError(err) }))
.on('stalled', (jobId) => dbLogger.warn(`stalled id=${jobId}`));
}
//#endregion //#endregion
//#region deliver //#region deliver
this.deliverQueueWorker = new Bull.Worker(QUEUE.DELIVER, (job) => this.deliverProcessorService.process(job), { {
...baseQueueOptions(this.config, QUEUE.DELIVER), this.deliverQueueWorker = new Bull.Worker(QUEUE.DELIVER, (job) => {
autorun: false, if (this.config.sentryForBackend) {
concurrency: this.config.deliverJobConcurrency ?? 128, return Sentry.startSpan({ name: 'Queue: Deliver' }, () => this.deliverProcessorService.process(job));
limiter: { } else {
max: this.config.deliverJobPerSec ?? 128, return this.deliverProcessorService.process(job);
duration: 1000, }
}, }, {
settings: { ...baseQueueOptions(this.config, QUEUE.DELIVER),
backoffStrategy: httpRelatedBackoff, autorun: false,
}, concurrency: this.config.deliverJobConcurrency ?? 128,
}); limiter: {
max: this.config.deliverJobPerSec ?? 128,
duration: 1000,
},
settings: {
backoffStrategy: httpRelatedBackoff,
},
});
const deliverLogger = this.logger.createSubLogger('deliver'); const deliverLogger = this.logger.createSubLogger('deliver');
this.deliverQueueWorker this.deliverQueueWorker
.on('active', (job) => deliverLogger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`)) .on('active', (job) => deliverLogger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`))
.on('completed', (job, result) => deliverLogger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`)) .on('completed', (job, result) => deliverLogger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`))
.on('failed', (job, err) => deliverLogger.warn(`failed(${err.stack}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`)) .on('failed', (job, err) => deliverLogger.warn(`failed(${err.stack}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`))
.on('error', (err: Error) => deliverLogger.error(`error ${err.stack}`, { e: renderError(err) })) .on('error', (err: Error) => deliverLogger.error(`error ${err.stack}`, { e: renderError(err) }))
.on('stalled', (jobId) => deliverLogger.warn(`stalled id=${jobId}`)); .on('stalled', (jobId) => deliverLogger.warn(`stalled id=${jobId}`));
}
//#endregion //#endregion
//#region inbox //#region inbox
this.inboxQueueWorker = new Bull.Worker(QUEUE.INBOX, (job) => this.inboxProcessorService.process(job), { {
...baseQueueOptions(this.config, QUEUE.INBOX), this.inboxQueueWorker = new Bull.Worker(QUEUE.INBOX, (job) => {
autorun: false, if (this.config.sentryForBackend) {
concurrency: this.config.inboxJobConcurrency ?? 16, return Sentry.startSpan({ name: 'Queue: Inbox' }, () => this.inboxProcessorService.process(job));
limiter: { } else {
max: this.config.inboxJobPerSec ?? 32, return this.inboxProcessorService.process(job);
duration: 1000, }
}, }, {
settings: { ...baseQueueOptions(this.config, QUEUE.INBOX),
backoffStrategy: httpRelatedBackoff, autorun: false,
}, concurrency: this.config.inboxJobConcurrency ?? 16,
}); limiter: {
max: this.config.inboxJobPerSec ?? 32,
duration: 1000,
},
settings: {
backoffStrategy: httpRelatedBackoff,
},
});
const inboxLogger = this.logger.createSubLogger('inbox'); const inboxLogger = this.logger.createSubLogger('inbox');
this.inboxQueueWorker this.inboxQueueWorker
.on('active', (job) => inboxLogger.debug(`active ${getJobInfo(job, true)}`)) .on('active', (job) => inboxLogger.debug(`active ${getJobInfo(job, true)}`))
.on('completed', (job, result) => inboxLogger.debug(`completed(${result}) ${getJobInfo(job, true)}`)) .on('completed', (job, result) => inboxLogger.debug(`completed(${result}) ${getJobInfo(job, true)}`))
.on('failed', (job, err) => inboxLogger.warn(`failed(${err.stack}) ${getJobInfo(job)} activity=${job ? (job.data.activity ? job.data.activity.id : 'none') : '-'}`, { job, e: renderError(err) })) .on('failed', (job, err) => inboxLogger.warn(`failed(${err.stack}) ${getJobInfo(job)} activity=${job ? (job.data.activity ? job.data.activity.id : 'none') : '-'}`, { job, e: renderError(err) }))
.on('error', (err: Error) => inboxLogger.error(`error ${err.stack}`, { e: renderError(err) })) .on('error', (err: Error) => inboxLogger.error(`error ${err.stack}`, { e: renderError(err) }))
.on('stalled', (jobId) => inboxLogger.warn(`stalled id=${jobId}`)); .on('stalled', (jobId) => inboxLogger.warn(`stalled id=${jobId}`));
}
//#endregion //#endregion
//#region webhook deliver //#region webhook deliver
this.webhookDeliverQueueWorker = new Bull.Worker(QUEUE.WEBHOOK_DELIVER, (job) => this.webhookDeliverProcessorService.process(job), { {
...baseQueueOptions(this.config, QUEUE.WEBHOOK_DELIVER), this.webhookDeliverQueueWorker = new Bull.Worker(QUEUE.WEBHOOK_DELIVER, (job) => {
autorun: false, if (this.config.sentryForBackend) {
concurrency: 64, return Sentry.startSpan({ name: 'Queue: WebhookDeliver' }, () => this.webhookDeliverProcessorService.process(job));
limiter: { } else {
max: 64, return this.webhookDeliverProcessorService.process(job);
duration: 1000, }
}, }, {
settings: { ...baseQueueOptions(this.config, QUEUE.WEBHOOK_DELIVER),
backoffStrategy: httpRelatedBackoff, autorun: false,
}, concurrency: 64,
}); limiter: {
max: 64,
duration: 1000,
},
settings: {
backoffStrategy: httpRelatedBackoff,
},
});
const webhookLogger = this.logger.createSubLogger('webhook'); const webhookLogger = this.logger.createSubLogger('webhook');
this.webhookDeliverQueueWorker this.webhookDeliverQueueWorker
.on('active', (job) => webhookLogger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`)) .on('active', (job) => webhookLogger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`))
.on('completed', (job, result) => webhookLogger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`)) .on('completed', (job, result) => webhookLogger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`))
.on('failed', (job, err) => webhookLogger.warn(`failed(${err.stack}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`)) .on('failed', (job, err) => webhookLogger.warn(`failed(${err.stack}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`))
.on('error', (err: Error) => webhookLogger.error(`error ${err.stack}`, { e: renderError(err) })) .on('error', (err: Error) => webhookLogger.error(`error ${err.stack}`, { e: renderError(err) }))
.on('stalled', (jobId) => webhookLogger.warn(`stalled id=${jobId}`)); .on('stalled', (jobId) => webhookLogger.warn(`stalled id=${jobId}`));
}
//#endregion //#endregion
//#region relationship //#region relationship
this.relationshipQueueWorker = new Bull.Worker(QUEUE.RELATIONSHIP, (job) => { {
switch (job.name) { const processer = (job: Bull.Job) => {
case 'follow': return this.relationshipProcessorService.processFollow(job); switch (job.name) {
case 'unfollow': return this.relationshipProcessorService.processUnfollow(job); case 'follow': return this.relationshipProcessorService.processFollow(job);
case 'block': return this.relationshipProcessorService.processBlock(job); case 'unfollow': return this.relationshipProcessorService.processUnfollow(job);
case 'unblock': return this.relationshipProcessorService.processUnblock(job); case 'block': return this.relationshipProcessorService.processBlock(job);
default: throw new Error(`unrecognized job type ${job.name} for relationship`); case 'unblock': return this.relationshipProcessorService.processUnblock(job);
} default: throw new Error(`unrecognized job type ${job.name} for relationship`);
}, { }
...baseQueueOptions(this.config, QUEUE.RELATIONSHIP), };
autorun: false,
concurrency: this.config.relationshipJobConcurrency ?? 16,
limiter: {
max: this.config.relationshipJobPerSec ?? 64,
duration: 1000,
},
});
const relationshipLogger = this.logger.createSubLogger('relationship'); this.relationshipQueueWorker = new Bull.Worker(QUEUE.RELATIONSHIP, (job) => {
if (this.config.sentryForBackend) {
return Sentry.startSpan({ name: 'Queue: Relationship: ' + job.name }, () => processer(job));
} else {
return processer(job);
}
}, {
...baseQueueOptions(this.config, QUEUE.RELATIONSHIP),
autorun: false,
concurrency: this.config.relationshipJobConcurrency ?? 16,
limiter: {
max: this.config.relationshipJobPerSec ?? 64,
duration: 1000,
},
});
this.relationshipQueueWorker const relationshipLogger = this.logger.createSubLogger('relationship');
.on('active', (job) => relationshipLogger.debug(`active id=${job.id}`))
.on('completed', (job, result) => relationshipLogger.debug(`completed(${result}) id=${job.id}`)) this.relationshipQueueWorker
.on('failed', (job, err) => relationshipLogger.warn(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) })) .on('active', (job) => relationshipLogger.debug(`active id=${job.id}`))
.on('error', (err: Error) => relationshipLogger.error(`error ${err.stack}`, { e: renderError(err) })) .on('completed', (job, result) => relationshipLogger.debug(`completed(${result}) id=${job.id}`))
.on('stalled', (jobId) => relationshipLogger.warn(`stalled id=${jobId}`)); .on('failed', (job, err) => relationshipLogger.warn(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) }))
.on('error', (err: Error) => relationshipLogger.error(`error ${err.stack}`, { e: renderError(err) }))
.on('stalled', (jobId) => relationshipLogger.warn(`stalled id=${jobId}`));
}
//#endregion //#endregion
//#region object storage //#region object storage
this.objectStorageQueueWorker = new Bull.Worker(QUEUE.OBJECT_STORAGE, (job) => { {
switch (job.name) { const processer = (job: Bull.Job) => {
case 'deleteFile': return this.deleteFileProcessorService.process(job); switch (job.name) {
case 'cleanRemoteFiles': return this.cleanRemoteFilesProcessorService.process(job); case 'deleteFile': return this.deleteFileProcessorService.process(job);
default: throw new Error(`unrecognized job type ${job.name} for objectStorage`); case 'cleanRemoteFiles': return this.cleanRemoteFilesProcessorService.process(job);
} default: throw new Error(`unrecognized job type ${job.name} for objectStorage`);
}, { }
...baseQueueOptions(this.config, QUEUE.OBJECT_STORAGE), };
autorun: false,
concurrency: 16,
});
const objectStorageLogger = this.logger.createSubLogger('objectStorage'); this.objectStorageQueueWorker = new Bull.Worker(QUEUE.OBJECT_STORAGE, (job) => {
if (this.config.sentryForBackend) {
return Sentry.startSpan({ name: 'Queue: ObjectStorage: ' + job.name }, () => processer(job));
} else {
return processer(job);
}
}, {
...baseQueueOptions(this.config, QUEUE.OBJECT_STORAGE),
autorun: false,
concurrency: 16,
});
this.objectStorageQueueWorker const objectStorageLogger = this.logger.createSubLogger('objectStorage');
.on('active', (job) => objectStorageLogger.debug(`active id=${job.id}`))
.on('completed', (job, result) => objectStorageLogger.debug(`completed(${result}) id=${job.id}`)) this.objectStorageQueueWorker
.on('failed', (job, err) => objectStorageLogger.warn(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) })) .on('active', (job) => objectStorageLogger.debug(`active id=${job.id}`))
.on('error', (err: Error) => objectStorageLogger.error(`error ${err.stack}`, { e: renderError(err) })) .on('completed', (job, result) => objectStorageLogger.debug(`completed(${result}) id=${job.id}`))
.on('stalled', (jobId) => objectStorageLogger.warn(`stalled id=${jobId}`)); .on('failed', (job, err) => objectStorageLogger.warn(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) }))
.on('error', (err: Error) => objectStorageLogger.error(`error ${err.stack}`, { e: renderError(err) }))
.on('stalled', (jobId) => objectStorageLogger.warn(`stalled id=${jobId}`));
}
//#endregion //#endregion
//#region ended poll notification //#region ended poll notification
this.endedPollNotificationQueueWorker = new Bull.Worker(QUEUE.ENDED_POLL_NOTIFICATION, (job) => this.endedPollNotificationProcessorService.process(job), { {
...baseQueueOptions(this.config, QUEUE.ENDED_POLL_NOTIFICATION), this.endedPollNotificationQueueWorker = new Bull.Worker(QUEUE.ENDED_POLL_NOTIFICATION, (job) => {
autorun: false, if (this.config.sentryForBackend) {
}); return Sentry.startSpan({ name: 'Queue: EndedPollNotification' }, () => this.endedPollNotificationProcessorService.process(job));
} else {
return this.endedPollNotificationProcessorService.process(job);
}
}, {
...baseQueueOptions(this.config, QUEUE.ENDED_POLL_NOTIFICATION),
autorun: false,
});
}
//#endregion //#endregion
} }