UNPKG

@vendure/core

Version:

A modern, headless ecommerce framework

205 lines 9.86 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.PollingJobQueueStrategy = void 0; const generated_types_1 = require("@vendure/common/lib/generated-types"); const shared_utils_1 = require("@vendure/common/lib/shared-utils"); const rxjs_1 = require("rxjs"); const operators_1 = require("rxjs/operators"); const vendure_logger_1 = require("../config/logger/vendure-logger"); const injectable_job_queue_strategy_1 = require("./injectable-job-queue-strategy"); const job_1 = require("./job"); const queue_name_process_storage_1 = require("./queue-name-process-storage"); const STOP_SIGNAL = Symbol('STOP_SIGNAL'); class ActiveQueue { constructor(queueName, process, jobQueueStrategy) { this.queueName = queueName; this.process = process; this.jobQueueStrategy = jobQueueStrategy; this.running = false; this.activeJobs = []; this.errorNotifier$ = new rxjs_1.Subject(); this.queueStopped$ = new rxjs_1.Subject(); this.pollInterval = typeof this.jobQueueStrategy.pollInterval === 'function' ? this.jobQueueStrategy.pollInterval(queueName) : this.jobQueueStrategy.pollInterval; } start() { vendure_logger_1.Logger.debug(`Starting JobQueue "${this.queueName}"`); this.subscription = this.errorNotifier$.pipe((0, operators_1.throttleTime)(3000)).subscribe(([message, stack]) => { vendure_logger_1.Logger.error(message); vendure_logger_1.Logger.debug(stack); }); this.running = true; const runNextJobs = async () => { try { const runningJobsCount = this.activeJobs.length; for (let i = runningJobsCount; i < this.jobQueueStrategy.concurrency; i++) { const nextJob = await this.jobQueueStrategy.next(this.queueName); if (nextJob) { this.activeJobs.push(nextJob); await this.jobQueueStrategy.update(nextJob); const onProgress = (job) => this.jobQueueStrategy.update(job); nextJob.on('progress', onProgress); const cancellationSub = (0, rxjs_1.interval)(this.pollInterval * 5) .pipe( // eslint-disable-next-line @typescript-eslint/no-non-null-assertion (0, operators_1.switchMap)(() => this.jobQueueStrategy.findOne(nextJob.id)), (0, operators_1.filter)(job => (job === null || job === void 0 ? void 0 : job.state) === generated_types_1.JobState.CANCELLED), (0, operators_1.take)(1)) .subscribe(() => { nextJob.cancel(); }); const stopSignal$ = this.queueStopped$.pipe((0, operators_1.take)(1)); (0, rxjs_1.race)((0, rxjs_1.from)(this.process(nextJob)), stopSignal$) .toPromise() .then(result => { if (result === STOP_SIGNAL) { nextJob.defer(); } else if (result instanceof job_1.Job && result.state === generated_types_1.JobState.CANCELLED) { nextJob.cancel(); } else { nextJob.complete(result); } }, err => { nextJob.fail(err); }) .finally(() => { // if (!this.running && nextJob.state !== JobState.PENDING) { // return; // } nextJob.off('progress', onProgress); cancellationSub.unsubscribe(); return this.onFailOrComplete(nextJob); }) .catch((err) => { vendure_logger_1.Logger.warn(`Error updating job info: ${JSON.stringify(err)}`); }); } } } catch (e) { this.errorNotifier$.next([ `Job queue "${this.queueName}" encountered an error (set log level to Debug for trace): ${JSON.stringify(e.message)}`, e.stack, ]); } if (this.running) { this.timer = setTimeout(runNextJobs, this.pollInterval); } }; void runNextJobs(); } async stop(stopActiveQueueTimeout = 20000) { this.running = false; clearTimeout(this.timer); await this.awaitRunningJobsOrTimeout(stopActiveQueueTimeout); vendure_logger_1.Logger.info(`Stopped queue: ${this.queueName}`); this.subscription.unsubscribe(); // Allow any job status changes to be persisted // before we permit the application shutdown to continue. // Otherwise, the DB connection will close before our // changes are persisted. await new Promise(resolve => setTimeout(resolve, 1000)); } awaitRunningJobsOrTimeout(stopActiveQueueTimeout = 20000) { const start = +new Date(); let timeout; return new Promise(resolve => { let lastStatusUpdate = +new Date(); const pollActiveJobs = () => { const now = +new Date(); const timedOut = stopActiveQueueTimeout === undefined ? false : now - start > stopActiveQueueTimeout; if (this.activeJobs.length === 0) { clearTimeout(timeout); resolve(); return; } if (timedOut) { vendure_logger_1.Logger.warn(`Timed out (${stopActiveQueueTimeout}ms) waiting for ${this.activeJobs.length} active jobs in queue "${this.queueName}" to complete. Forcing stop...`); this.queueStopped$.next(STOP_SIGNAL); clearTimeout(timeout); resolve(); return; } if (this.activeJobs.length > 0) { if (now - lastStatusUpdate > 2000) { vendure_logger_1.Logger.info(`Stopping queue: ${this.queueName} - waiting for ${this.activeJobs.length} active jobs to complete...`); lastStatusUpdate = now; } } timeout = setTimeout(pollActiveJobs, 200); }; void pollActiveJobs(); }); } async onFailOrComplete(job) { await this.jobQueueStrategy.update(job); this.removeJobFromActive(job); } removeJobFromActive(job) { const index = this.activeJobs.indexOf(job); if (index !== -1) { this.activeJobs.splice(index, 1); } } } /** * @description * This class allows easier implementation of {@link JobQueueStrategy} in a polling style. * Instead of providing {@link JobQueueStrategy} `start()` you should provide a `next` method. * * This class should be extended by any strategy which does not support a push-based system * to notify on new jobs. It is used by the {@link SqlJobQueueStrategy} and {@link InMemoryJobQueueStrategy}. * * @docsCategory JobQueue */ class PollingJobQueueStrategy extends injectable_job_queue_strategy_1.InjectableJobQueueStrategy { constructor(concurrencyOrConfig, maybePollInterval) { var _a, _b, _c, _d, _e; super(); this.activeQueues = new queue_name_process_storage_1.QueueNameProcessStorage(); if (concurrencyOrConfig && (0, shared_utils_1.isObject)(concurrencyOrConfig)) { this.concurrency = (_a = concurrencyOrConfig.concurrency) !== null && _a !== void 0 ? _a : 1; this.pollInterval = (_b = concurrencyOrConfig.pollInterval) !== null && _b !== void 0 ? _b : 200; this.backOffStrategy = (_c = concurrencyOrConfig.backoffStrategy) !== null && _c !== void 0 ? _c : (() => 1000); this.setRetries = (_d = concurrencyOrConfig.setRetries) !== null && _d !== void 0 ? _d : ((_, job) => job.retries); this.gracefulShutdownTimeout = (_e = concurrencyOrConfig.gracefulShutdownTimeout) !== null && _e !== void 0 ? _e : 20000; } else { this.concurrency = concurrencyOrConfig !== null && concurrencyOrConfig !== void 0 ? concurrencyOrConfig : 1; this.pollInterval = maybePollInterval !== null && maybePollInterval !== void 0 ? maybePollInterval : 200; this.setRetries = (_, job) => job.retries; this.gracefulShutdownTimeout = 20000; } } async start(queueName, process) { if (!this.hasInitialized) { this.started.set(queueName, process); return; } if (this.activeQueues.has(queueName, process)) { return; } const active = new ActiveQueue(queueName, process, this); active.start(); this.activeQueues.set(queueName, process, active); } async stop(queueName, process) { const active = this.activeQueues.getAndDelete(queueName, process); if (!active) { return; } await active.stop(this.gracefulShutdownTimeout); } async cancelJob(jobId) { const job = await this.findOne(jobId); if (job) { job.cancel(); await this.update(job); return job; } } } exports.PollingJobQueueStrategy = PollingJobQueueStrategy; //# sourceMappingURL=polling-job-queue-strategy.js.map