UNPKG

phantomjscloud

Version:

Official PhantomJs Cloud Client API Library for Node.js

192 lines (152 loc) 7.33 kB
import xlib = require( "xlib" ); //import Promise = xlib.promise.bluebird; import _ = xlib.lodash; //export function debugLog(...args: any[]): void { // if (isDebug !== true) { // return; // } // //console.log("\n"); // console.log("\n====================================="); // console.log.apply(console, args); // //console.log("\n"); //} ///**set to true to enable debug outputs */ //export let isDebug = false; const log = xlib.diagnostics.log; //let log = new xlib.diagnostics.Logger( __filename, xlib.environment.LogLevel.WARN ); log.overrideLogLevel( "WARN" ); /** * options for the AutoscaleConsumer */ export interface IAutoscaleConsumerOptions { /** the minimum number of workers. below this, we will instantly provision new workers for added work. default=8 */ workerMin: number; /** maximum number of parallel workers. default=60 */ workerMax: number; /** if there is pending work, how long (in ms) to wait before increasing our number of workers. This should not be too fast otherwise you can overload the autoscaler. default=4000 (4 seconds), which would result in 15 workers after 1 minute of operation on a very large work queue. */ workersLinearGrowthMs: number; /** how long (in ms) for an idle worker (no work remaining) to wait before attempting to grab new work. default=100 (100 ms) */ workerReaquireMs: number; /** the max time a worker will be idle before disposing itself. default=10000 (10 seconds) */ workerMaxIdleMs: number; } const autoscaleConsumerOptionsDefaults: IAutoscaleConsumerOptions = { /** the minimum number of workers. below this, we will instantly provision new workers for added work. default=8 */ workerMin: 8, /** maximum number of parallel workers. default=60 */ workerMax: 60, /** if there is pending work, how long (in ms) to wait before increasing our number of workers. This should not be too fast otherwise you can overload the autoscaler. default=4000 (4 seconds), which would result in 15 workers after 1 minute of operation on a very large work queue. */ workersLinearGrowthMs: 4000, /** how long (in ms) for an idle worker (no work remaining) to wait before attempting to grab new work. default=100 (100 ms) */ workerReaquireMs: 100, /** the max time a worker will be idle before disposing itself. default=10000 (10 seconds) */ workerMaxIdleMs: 10000, } interface IPendingTask<TInput, TOutput> { input: TInput; resolve: ( result: TOutput ) => void; reject: ( error: Error ) => void; } /** * allows consumption of an autoscaling process. asynchronously executes work, scheduling the work to be executed in a graceful "ramping work up" fashion so to take advantage of the autoscaler increase-in-capacity features. * technical details: enqueues all process requests into a central pool and executes workers on them. if there is additional queued work, increases workers over time. */ export class AutoscaleConsumer<TInput, TOutput>{ public options: IAutoscaleConsumerOptions; private _pendingTasks: IPendingTask<TInput, TOutput>[] = []; private _workerCount: number = 0; private _workerLastAddTime: Date = new Date( 0 ); private __autoTrySpawnHandle: NodeJS.Timer | null; constructor( /** The "WorkerThread", this function processes work. it's execution is automatically managed by this object. */ private _workProcessor: ( input: TInput ) => PromiseLike<TOutput>, _options: Partial<IAutoscaleConsumerOptions> = {} ) { //let defaultOptions = new AutoscaleConsumerOptions(); this.options = _.defaults( _options, autoscaleConsumerOptionsDefaults ); } public process( input: TInput ): PromiseLike<TOutput> { // Promise<TOutput> { let toReturn = new xlib.promise.bluebird<TOutput>( ( resolve, reject ) => { this._pendingTasks.push( { input, resolve, reject } ); } ); this._trySpawnWorker(); return toReturn; } /** inform that the autoscaler should stall growing. we do this by resetting the linearGrowth timer. */ public stall() { this._workerLastAddTime = new Date(); } private _trySpawnWorker() { //debugLog("AutoscaleConsumer._tryStartProcessing called"); if ( this._workerCount >= this.options.workerMax || this._pendingTasks.length === 0 ) { return; } let nextAddTime = this._workerLastAddTime.getTime() + this.options.workersLinearGrowthMs; let now = Date.now(); let timeToAddWorker = false; if ( this._workerCount < this.options.workerMin ) { timeToAddWorker = true; } if ( now >= nextAddTime ) { //if we don't have much work remaining, don't add more workers //if ((this._workerCount * this.options.workerMinimumQueueMultiplier) < this._pendingRequests.length) timeToAddWorker = true; } if ( timeToAddWorker === true ) { this._workerCount++; this._workerLastAddTime = new Date(); setTimeout( () => { this._workerLoop() } ); } if ( this.__autoTrySpawnHandle == null ) { //set a periodic auto-try-spawn worker to kick off this.__autoTrySpawnHandle = setInterval( () => { this._trySpawnWorker(); if ( this._pendingTasks.length == 0 ) { //stop this period attempt because no work to do. clearInterval( this.__autoTrySpawnHandle as any ); this.__autoTrySpawnHandle = null; } }, 100 ); } } /** * recursively loops itself * @param idleMs */ private _workerLoop( idleMs: number = 0 ) { if ( this._pendingTasks.length === 0 ) { //no work to do, dispose or wait //also instantly dispose of the worker if there's the minimum number of workers or less (because we will instantly spawn them up if needed). if ( idleMs > this.options.workerMaxIdleMs || this._workerCount <= this.options.workerMin ) { //already idle too long, dispose this._workerLoop_disposeHelper(); } else { //retry this workerLoop after a short idle time setTimeout( () => { this._workerLoop( idleMs + this.options.workerReaquireMs ) }, this.options.workerReaquireMs ); } return; } let work = this._pendingTasks.shift() as IPendingTask<TInput, TOutput>; if ( work == null ) { throw log.error( "pending task is non existant", { work, pendingCount: this._pendingTasks.length } ); } xlib.promise.bluebird.try( () => { log.debug( "AUTOSCALECONSUMER._workerLoop() starting request processing (workProcessor) concurrent=" + this._workerCount ); return this._workProcessor( work.input ); } ).then( ( output ) => { log.debug( "AUTOSCALECONSUMER._workerLoop() finished workProcessor() SUCCESS. concurrent=" + this._workerCount ); work.resolve( output ); }, ( error ) => { log.debug( "AUTOSCALECONSUMER._workerLoop() finished workProcessor() ERROR. concurrent=" + this._workerCount ); work.reject( error ); } ).finally( () => { //fire another loop next tick setTimeout( () => { this._workerLoop(); } ); //since we had work to do, there might be more work to do/scale up workers for. fire a "try start processing" this._trySpawnWorker(); } ); } private _workerLoop_disposeHelper() { log.debug( "AUTOSCALECONSUMER._workerLoop() already idle too long, dispose. concurrent=" + this._workerCount ); this._workerCount--; } }