-
Notifications
You must be signed in to change notification settings - Fork 231
fix: Bulk export cleanup and notification occasionally not working on job expire #10366
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
Changes from all commits
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 |
---|---|---|
@@ -1,11 +1,13 @@ | ||
import type { PageBulkExportJobDocument } from '../../models/page-bulk-export-job'; | ||
|
||
export class BulkExportJobExpiredError extends Error { | ||
constructor() { | ||
super('Bulk export job has expired'); | ||
constructor(pageBulkExportJob: PageBulkExportJobDocument) { | ||
super(`Bulk export job has expired: ${pageBulkExportJob._id.toString()}`); | ||
} | ||
} | ||
|
||
export class BulkExportJobRestartedError extends Error { | ||
export class BulkExportJobStreamDestroyedByCleanupError extends Error { | ||
constructor() { | ||
super('Bulk export job has restarted'); | ||
super('Bulk export job stream was destroyed by cleanup'); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,6 @@ | ||
import fs from 'node:fs'; | ||
import path from 'node:path'; | ||
import type { Readable } from 'node:stream'; | ||
import type { Readable, Writable } from 'node:stream'; | ||
import type { IUser } from '@growi/core'; | ||
import { getIdForRef, isPopulated } from '@growi/core'; | ||
import mongoose from 'mongoose'; | ||
|
@@ -26,7 +26,7 @@ import PageBulkExportPageSnapshot from '../../models/page-bulk-export-page-snaps | |
|
||
import { | ||
BulkExportJobExpiredError, | ||
BulkExportJobRestartedError, | ||
BulkExportJobStreamDestroyedByCleanupError, | ||
} from './errors'; | ||
import { requestPdfConverter } from './request-pdf-converter'; | ||
import { compressAndUpload } from './steps/compress-and-upload'; | ||
|
@@ -40,7 +40,10 @@ export interface IPageBulkExportJobCronService { | |
pageBatchSize: number; | ||
maxPartSize: number; | ||
compressExtension: string; | ||
setStreamInExecution(jobId: ObjectIdLike, stream: Readable): void; | ||
setStreamsInExecution( | ||
jobId: ObjectIdLike, | ||
...streams: (Readable | Writable)[] | ||
): void; | ||
removeStreamInExecution(jobId: ObjectIdLike): void; | ||
handleError( | ||
err: Error | null, | ||
|
@@ -78,10 +81,10 @@ class PageBulkExportJobCronService | |
// temporal path of local fs to output page files before upload | ||
tmpOutputRootDir = '/tmp/page-bulk-export'; | ||
|
||
// Keep track of the stream executed for PageBulkExportJob to destroy it on job failure. | ||
// The key is the id of a PageBulkExportJob. | ||
// Keep track of all streams executed for PageBulkExportJob to destroy them on job failure. | ||
// The key is the id of a PageBulkExportJob, value is array of streams. | ||
private streamInExecutionMemo: { | ||
[key: string]: Readable; | ||
[key: string]: (Readable | Writable)[]; | ||
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. readable と writable stream 両方をメモリ上に記録して置けるように変更。 |
||
} = {}; | ||
|
||
private parallelExecLimit: number; | ||
|
@@ -133,22 +136,27 @@ class PageBulkExportJobCronService | |
} | ||
|
||
/** | ||
* Get the stream in execution for a job. | ||
* Get all streams in execution for a job. | ||
* A getter method that includes "undefined" in the return type | ||
*/ | ||
getStreamInExecution(jobId: ObjectIdLike): Readable | undefined { | ||
getStreamsInExecution( | ||
jobId: ObjectIdLike, | ||
): (Readable | Writable)[] | undefined { | ||
return this.streamInExecutionMemo[jobId.toString()]; | ||
} | ||
|
||
/** | ||
* Set the stream in execution for a job | ||
* Set streams in execution for a job | ||
*/ | ||
setStreamInExecution(jobId: ObjectIdLike, stream: Readable) { | ||
this.streamInExecutionMemo[jobId.toString()] = stream; | ||
setStreamsInExecution( | ||
jobId: ObjectIdLike, | ||
...streams: (Readable | Writable)[] | ||
) { | ||
this.streamInExecutionMemo[jobId.toString()] = streams; | ||
} | ||
|
||
/** | ||
* Remove the stream in execution for a job | ||
* Remove all streams in execution for a job | ||
*/ | ||
removeStreamInExecution(jobId: ObjectIdLike) { | ||
delete this.streamInExecutionMemo[jobId.toString()]; | ||
|
@@ -161,7 +169,7 @@ class PageBulkExportJobCronService | |
async proceedBulkExportJob(pageBulkExportJob: PageBulkExportJobDocument) { | ||
try { | ||
if (pageBulkExportJob.restartFlag) { | ||
await this.cleanUpExportJobResources(pageBulkExportJob, true); | ||
await this.cleanUpExportJobResources(pageBulkExportJob); | ||
pageBulkExportJob.restartFlag = false; | ||
pageBulkExportJob.status = PageBulkExportJobStatus.initializing; | ||
pageBulkExportJob.statusOnPreviousCronExec = undefined; | ||
|
@@ -226,9 +234,6 @@ class PageBulkExportJobCronService | |
SupportedAction.ACTION_PAGE_BULK_EXPORT_JOB_EXPIRED, | ||
pageBulkExportJob, | ||
); | ||
} else if (err instanceof BulkExportJobRestartedError) { | ||
logger.info(err.message); | ||
await this.cleanUpExportJobResources(pageBulkExportJob); | ||
} else { | ||
logger.error(err); | ||
await this.notifyExportResultAndCleanUp( | ||
|
@@ -269,15 +274,24 @@ class PageBulkExportJobCronService | |
*/ | ||
async cleanUpExportJobResources( | ||
pageBulkExportJob: PageBulkExportJobDocument, | ||
restarted = false, | ||
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. restarted かどうかは stream のエラーを handleError によって処理するときの場合分けに必要だったが、cleanup 時は handleError しなくなったため、不要となった。 |
||
) { | ||
const streamInExecution = this.getStreamInExecution(pageBulkExportJob._id); | ||
if (streamInExecution != null) { | ||
if (restarted) { | ||
streamInExecution.destroy(new BulkExportJobRestartedError()); | ||
} else { | ||
streamInExecution.destroy(new BulkExportJobExpiredError()); | ||
} | ||
const streamsInExecution = this.getStreamsInExecution( | ||
pageBulkExportJob._id, | ||
); | ||
if (streamsInExecution != null && streamsInExecution.length > 0) { | ||
// Wait for all streams to be destroyed before proceeding with cleanup | ||
await Promise.allSettled( | ||
streamsInExecution.map((stream) => { | ||
if (!stream.destroyed) { | ||
return new Promise<void>((resolve) => { | ||
stream.destroy(new BulkExportJobStreamDestroyedByCleanupError()); | ||
stream.once('close', () => resolve()); | ||
}); | ||
} | ||
return Promise.resolve(); | ||
}), | ||
); | ||
|
||
this.removeStreamInExecution(pageBulkExportJob._id); | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -11,6 +11,7 @@ import type { PageBulkExportJobDocument } from '../../../models/page-bulk-export | |
import PageBulkExportJob from '../../../models/page-bulk-export-job'; | ||
import PageBulkExportPageSnapshot from '../../../models/page-bulk-export-page-snapshot'; | ||
import type { IPageBulkExportJobCronService } from '..'; | ||
import { BulkExportJobStreamDestroyedByCleanupError } from '../errors'; | ||
|
||
async function reuseDuplicateExportIfExists( | ||
this: IPageBulkExportJobCronService, | ||
|
@@ -100,9 +101,16 @@ export async function createPageSnapshotsAsync( | |
}, | ||
}); | ||
|
||
this.setStreamInExecution(pageBulkExportJob._id, pagesReadable); | ||
this.setStreamsInExecution( | ||
pageBulkExportJob._id, | ||
pagesReadable, | ||
pageSnapshotsWritable, | ||
); | ||
|
||
pipeline(pagesReadable, pageSnapshotsWritable, (err) => { | ||
this.handleError(err, pageBulkExportJob); | ||
// prevent overlapping cleanup | ||
if (!(err instanceof BulkExportJobStreamDestroyedByCleanupError)) { | ||
this.handleError(err, pageBulkExportJob); | ||
} | ||
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. 元々 cleanup で投げた BulkExportJobRestartedError, BulkExportJobExpiredError もここでキャッチし、handleError を実行することによって通知と、重複した cleanup 処理を実行していた。 (重複した cleanup を実行していたのは、destroy によって stream の処理が終わる前に cleanup が完了してしまう場合をカバーするためだった。これをカバーするために cleanup 関数側で stream close を待つように変更 1d807d0) |
||
}); | ||
} |
Uh oh!
There was an error while loading. Please reload this page.
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.
cleanUpAndDeleteBulkExportJobs ではなく notifyExportResultAndCleanUp を実行し、明示的に期限切れの通知を実行。