phantomjscloud
Version:
Official PhantomJs Cloud Client API Library for Node.js
192 lines (152 loc) • 7.33 kB
text/typescript
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--;
}
}