1 of 3
Post Made Community Wiki
Sky Sanders
  • 12.1k
  • 3
  • 31
  • 60

###JavaScript

This is a governed sliding window throttled queue that conforms to the published usage guidelines regarding request rate.

It works by executing queued callbacks at a nominal rate.

Similar to the C# version, but without threading.

It provides:

  • maximum throughput for isolated bursts
  • reliable execution for mass requests.

Usage:

// create a common Throttle
window.RequestThrottle = new Throttle(5000,30,15);

// route every request through the throttle

RequestThrottle.enqueue(function(){ 

    /* do something*/

    // but NO MATTER WHAT you need to signal when complete
    // regardless of state, so get your exception handling hat on.
    RequestThrottle.signal();    
});

Throttle.js

Throttle = function(throttleWindowTime, throttleWindowCount, maxActiveRequests) {
    /// <summary>
    /// The purpose of Throttle is to queue requests in the interest of
    /// preventing inadvertant DOS attacks.
    /// 
    /// This throttle is intended to work with a sliding window and a governor.
    ///        
    /// The criteria are - X requests per Y seconds while < z requests are pending.
    /// where X: throttleWindowCount
    ///       Y: throttleWindowTime
    ///       X: maxActiveRequests
    /// 
    /// </summary>
    /// <param name="throttleWindowTime" type="Number">The amount of time, in ms,
    /// that defines the throttle window.<param>
    /// <param name="throttleWindowCount" type="Number">The number of requests
    /// allowed in any throttle window<param>
    /// <param name="maxActiveRequests" type="Number">The maximum allowed number
    /// of active request<param>
    /// <remarks>
    /// Not sure why I care anymore, but it just seems the thing to do.
    /// 
    /// If you have another idea, better or otherwise, please share.
    /// </remarks>

    var times = new Queue(),
    requests = new Queue(),
    active = 0,
    windowTime = throttleWindowTime,
    windowCount = throttleWindowCount,
    maxActive = maxActiveRequests,
    timer = false,
    that = this;

    var lastRequestTime = 0; // for debugging

    this.processQueue = function() {
        /// <summary>
        /// there are several criteria that need to be met in order
        /// for a request to be executed:
        ///
        /// 1 - that the active request count < max allowed active count
        ///
        /// 2 - that the difference in time between Now and the
        ///     throttleWindowCount-nth request is greater than
        ///     throttleWindowTime.
        /// </summary>

        if (requests.count() === 0 || (active === maxActive)) {
            return;
        };

        var head = new Date().getTime();

        var queueCount = times.count()

        if (queueCount >= windowCount) {
            // check the difference
            var tail = times.peek() || 0;

            if (head - tail < windowTime) {
                // not time yet
                return;
            };
            // we are a go, snap the tail off
            times.dequeue();
        }


        // Metrics: the interval since last request
        //var interval = head - lastRequestTime;
        
        lastRequestTime = head;

        var request = requests.dequeue();
        
        if (typeof request == 'function') {
            active++;
            times.enqueue(head);
            request();
        }
    };

    this.signal = function() {
        /// <summary>
        /// This method simply decrements the active request count.
        /// It simply MUST be called once for each request issued
        /// regardless of the final status of the request.
        /// This means that you MUST NOT swallow execution errors.
        /// </summary>
        active = active - 1;
        if (active < 0) {
            throw Error.invalidOperation("Request count negative.");
        };
    };

    this.enqueue = function(callback) {
        /// <summary>
        /// Adds a callback to the throttle queue.
        /// </summary>
        /// <param name="callback" type="Function">The function to be executed in due time.<param>
        requests.enqueue(callback);
    };

    this.clear = function() {
        /// <summary>
        /// Suspends processing and clears queues and counts.
        /// </summary>
        that.suspend();
        times = new Queue();
        requests = new Queue();
        active = 0;
    };

    this.start = function(pollingInterval) {
        /// <summary>
        /// Resumes processing of the throttle queue at the specified
        /// polling interval;
        /// 
        /// If the throttle is running, calls to this method are ignored.
        /// </summary>
        /// <param name="" type="Number">The desired polling interval in ms</param>
        if (!timer) {
            timer = window
            .setInterval(that.processQueue, pollingInterval);
        };
    };

    this.suspend = function() {
        /// <summary>
        /// Suspends processing of the throttle queue.
        /// 
        /// If the throttle is not running, calls to this method are ignored.    
        /// </summary>
        if (timer) {
            window.clearInterval(timer);
            timer = false;
        };
    };

    this.start(75);
};




function Queue() {
    this._queue = [];
    this._queueSpace = 0;
}

Queue.prototype = {
    enqueue: function(element) {
        this._queue.push(element);
    },
    dequeue: function() {
        var element = undefined;
        if (this._queue.length) {
            element = this._queue[this._queueSpace];
            if (++this._queueSpace * 2 >= this._queue.length) {
                this._queue = this._queue.slice(this._queueSpace);
                this._queueSpace = 0;
            }
        }
        return element;
    },
    count: function() {
        return this._queue.length - this._queueSpace;
    },
    peek: function() {
        var element = undefined;
        if (this._queue.length) element = this._queue[this._queueSpace];
        return element;
    }
};
Sky Sanders
  • 12.1k
  • 3
  • 31
  • 60