UNPKG

esdf

Version:

a frugal event-sourced domain-driven design framework with elements of cqrs

162 lines (151 loc) 8.43 kB
/** * @module esdf/utils/QueueProcessor */ var when = require('when'); var util = require('util'); var EventEmitter2 = require('eventemitter2').EventEmitter2; /** * Construct a new QueueProcessor instance. A QueueProcessor is a tool for achieving the Serializer pattern - it allows pushing items from one side, while serially executing a provided function on the other, whether it is asynchronous (Promise-based) or synchronous. * Use a QueueProcessor when you need to make sure that subsequent work items are only processed when the previous ones have finished. * Pausing and starting can be used when you need to temporarily suspend processing for some reason (e.g. initially, because the processor function may not be ready yet). * * @constructor * @param {Object} [options] Additional settings to be applied to the queue processor. * @param {Object} [options.queue] An array-like queue object. Must support at least push(), shift(), and the length property. Having indexes is not required. * @param {module:esdf/utils/QueueProcessor#ProcessorFunction} [options.processorFunction] The processor function to use initially. If left out, no function will be assigned and one must be set afterwards, before starting processing. * @param {Boolean} [options.autostart] Whether to run the processor immediately after creating. By default, explicit activation via the start() method is required. Obviously, this requires options.processorFunction to be provided, too. * @param {module:esdf/utils/QueueProcessor#QueueProcessingErrorLabeler} [options.errorLabelFunction] A function that will be used for labeling errors when they occur. * @param {Number} [options.concurrencyLimit] The number of processor functions that can be running "simultaneously" in a single Node thread. The function is not called (and the queue popped) until the worker number is lower than this. */ function QueueProcessor(options){ if(!options){ options = {}; } // Initialize the queue using the provided constructor (Array by default). this._queue = typeof(options.queueConstructor) === 'function' ? (new options.queueConstructor()) : []; // Set starting flags. By default, we are not processing at construction time. this._processing = false; this._paused = true; this._errorLabelFunction = typeof(options.errorLabelFunction) === 'function' ? options.errorLabelFunction : function(workItem, error){}; this._processorFunction = null; this._activeWorkers = 0; this._concurrencyLimit = typeof(options.concurrencyLimit) === 'number' ? options.concurrencyLimit : 1; if(options.processorFunction){ this.setProcessorFunction(options.processorFunction); } if(options.autostart){ this.start(); } this._notifier = new EventEmitter2(); } /** * Main queue processing function. Shifts ("pops") one element from the queue and schedules an execution of the processor function on it via setImmediate. * If the processor function returns something other than a promise, another _process is called recursively. Otherwise, it is called when the promise resolves. * If the promise is rejected instead of resolved, an error processing, user-specified routine (options.errorLabelFunction) is executed (the code does NOT wait for any promise resolutions from the error labeler), * after which the work item is pushed to the queue's back again, to be processed in the near future (after any current events). * @private * @param {module:esdf/interfaces/QueueProcessorFunctionInterface} processorFunction The function to use when processing. Passed down to subsequent recursive _process calls, to preserve uniformity in face of changing function assignment in the meantime. */ QueueProcessor.prototype._process = function _process(processorFunction){ // Check for an end condition. If we have reached the queue's end, or the execution is paused, halt the recursion. if(this._paused || this._queue.length < 1){ if(this._activeWorkers === 0){ this._processing = false; this._notifier.emit('WorkStopped'); } return false; } // Check if the concurrency limit is not exhausted - if it is, just stop and let other "threads" resume processing. if(this._activeWorkers >= this._concurrencyLimit){ return false; } var currentWorkItem = this._queue.shift(); ++this._activeWorkers; // Call the processor function. On promise-or-value resolution, recurse into _process again (it will get the next element then). setImmediate((function(){ // Create a function to be called when this item's processing finishes - its task will be to start processing the next item when ready. var continueProcessing = (function(){ --this._activeWorkers; this._process(processorFunction); }).bind(this); var processError = (function(error){ this._processError(currentWorkItem, error, continueProcessing); }).bind(this); when(this._processorFunction(currentWorkItem)).then(continueProcessing, processError); }).bind(this)); return true; }; QueueProcessor.prototype._processError = function _processError(workItem, error, resumeCallback){ // Use the stored, user-supplied function to label the work item with the encountered error. this._errorLabelFunction(workItem, error); // Requeue the item at the back of the queue, so that it may be processed later. this.push(workItem); resumeCallback(); }; QueueProcessor.prototype._maintainWorkerCount = function _maintainWorkerCount(){ // If not processing and not paused, i.e. awaiting an item to process, start the processing. if(!this._paused){ // Spin up as many threads as allowed by the concurrency limit, compensating for the already-running threads. // Note that this may very well run 0 times, in case we are fully spun up. var taskWasStarted; do{ taskWasStarted = this._process(this._processorFunction); } while(taskWasStarted); } }; /** * Add an element to the queue. Elements added last are processed last, in a FIFO manner. */ QueueProcessor.prototype.push = function push(item){ // Immediately enqueue the item. this._queue.push(item); this._maintainWorkerCount(); }; /** * Start processing the elements by popping them off the stack and passing to the processor function. * A processor function needs to have been set before start()ing. * Starting when already started has no further effect. */ QueueProcessor.prototype.start = function start(){ if(typeof(this._processorFunction) !== 'function'){ throw new Error('Before starting the queue processor, a processor function must be set!'); } this._paused = false; this._maintainWorkerCount(); }; /** * Prevent any further elements from being popped off the queue and any processor functions from starting execution, until start()ed again. * The result is thenable, which allows you to wait until all tasks in progress have been processed and the processing has ceased. * Pausing when already paused has no further effect (when still pausing, many calls to pause() will result in many promises being returned, all of which will act correctly). * * @returns {external:Promise} A promise that will be fulfilled when the processing ceases. */ QueueProcessor.prototype.pause = function pause(){ this._paused = true; if(this._processing){ // Create the promise for delayed processing stop... var stopDeferred = when.defer(); // When work really does stop, fulfil it! this._notifier.once('WorkStopped', function _fulfillStopPromise(){ stopDeferred.resolve(); }); return stopDeferred; } else{ // We are already stopped (not processing anything), so we may as well return an already-resolved promise. return when.resolve(); } }; /** * Set the function used for processing enqueued elements. The function shall get the element as its only argument. * Its return (and the promise's resolution, if applicable) should mark that it is OK to pop another element off the queue and process it. * @param {module:esdf/interfaces/QueueProcessorFunctionInterface} processorFunction The function used for processing. If asynchronous, must return a Promises/A-compliant promise. */ QueueProcessor.prototype.setProcessorFunction = function setProcessorFunction(processorFunction){ if(typeof(processorFunction) !== 'function'){ throw new Error('QueueProcessor needs a function passed to setProcessorFunction, passed type:' + typeof(processorFunction)); } this._processorFunction = processorFunction; }; module.exports.QueueProcessor = QueueProcessor;