Rate Limiter Script

Viewing 0 reply threads
  • Author
    Posts
  • November 25, 2019 at 9:13 PM #38298

    Sam Moffatt
    Participant

    Based on a reply I added on the time delay post I wrote a while back, I decided to clean up and better document the script.

    There are three functions:

    • delay – simple spin lock delay mechanism for blocking execution for duration milliseconds.
    • rateLimitedDelay – ensure that a script blocks for at least minTime milliseconds since the last execution tracked by key.
    • rateLimitedCallback – non-blocking method to execute callback no more frequently than minTime milliseconds since the last execution tracked by key.

    The first use case is pretty simple: call it and wait until the duration expires. The second one is for ensuring you don’t go through a code path too quickly but if you haven’t called it lately it will immediately call it. The third one is used to optionally execute callback if you haven’t executed it in the last minTime milliseconds which is useful for avoiding spamming log messages or other similar events. It enables you to put in a callback but if it’s recently executed to immediately skip it (it’s not blocking). I built this to support a REST progress interface for ensuring it didn’t post updates more frequently than once every few seconds.

    This script uses a pattern I’m adopting of including a simple test case at the end. If you execute the script directly, it’ll run the test case as a sample of how to run. This means when you import you need to also define a variable called PARENT_SCRIPT which is used to disable the test. Technically you can set PARENT_SCRIPT to any value but I use the format FORM NAME::SCRIPT NAME:

    var PARENT_SCRIPT = 'Products::Update SKUs for Product';
    document.getFormNamed('Script Manager').runScriptNamed('Rate Limiter');
    

    Test case:

    	console.log('Message 1 at ' + new Date());
    	rateLimitedDelay('test');
    	rateLimitedDelay('test');
    	rateLimitedCallback('callback', function() { console.log('Callback 1: ' + new Date()) }); 
    	console.log('Message 2 at ' + new Date());
    	delay(3000);
    	rateLimitedCallback('callback', function() { console.log('Callback 2: ' + new Date()) });
    	rateLimitedDelay('test');
    	console.log('Message 3 at ' + new Date());
    	delay(6000);
    	rateLimitedCallback('callback', function() { console.log('Callback 3: ' + new Date()) });
    	console.log('Message 4 at ' + new Date());
    	rateLimitedDelay('test');
    	console.log('Message 5 at ' + new Date());
    	rateLimitedCallback('callback', function() { console.log('Callback 4: ' + new Date()) });
    

    Here’s the full script:

    // ========== Rate Limiter Start ========== //
    // NAME: Rate Limiter
    // VERSION: 1.0.0
    // CHANGELOG:
    //   1.0.0: Initial release.
    /**
     * Rate Limiter module provides utilities to limit and delay
     * over time.
     */
    if (typeof rateLimiter === 'undefined')
    {
    
    	var rateLimiter = {};
    	
    	/**
    	 * Spin lock delay mechanism.
    	 *
    	 * This will block execution until the time limit.
    	 *
    	 * @param {integer} duration - The length of the delay in milliseconds.
    	 */ 
    	function delay(duration)
    	{
    		let now = new Date();
    		let future = now.getTime() + duration;
    		while((new Date()).getTime() < future) { }
    	}
    	
    	/**
    	 * Blocking rate limited delay mechanism.
    	 *
    	 * This will block a request until a minimum time has been elapsed.
    	 * If based on the last execution of the `key`, `minTime` milliseconds
    	 * have not elapsed, this will block until that time has elapsed.
    	 *
    	 * `key` is shared with rateLimitedCallback.
    	 *
    	 * @param {string}  key - The key to validate the last execution.
    	 * @param {integer} minTime - The minimum amount of time between execution.
    	 */
    	function rateLimitedDelay(key, minTime = 5000)
    	{
    		if (typeof rateLimiter[key] === 'undefined')
    		{
    			rateLimiter[key] = 0;
    		}
    		let now = new Date().getTime();
    		let nextExecution = rateLimiter[key] + minTime;
    		if (now < nextExecution)
    		{
    			delay(nextExecution - now);
    		}
    		rateLimiter[key] = new Date().getTime();
    	}
    
    	/** 
    	 * Non-blocking rate limited callback executor.
    	 *
    	 * This will execute `callback` only if `callback` hasn't been 
    	 * executed as `key` for at least `minTime` milliseconds since
    	 * the last execution. If it has been executed then it will not
    	 * execute this instance.
    	 *
    	 * `key` is shared with `rateLimitedDelay`.
    	 *
    	 * @param {string}   key - The key to validate the last execution.
    	 * @param {function} callback - Callback to execute.
    	 * @param {integer}  minTime - The minimum amount of time between executions.
    	 */
    	function rateLimitedCallback(key, callback, minTime = 5000)
    	{
    		if (typeof rateLimiter[key] === 'undefined')
    		{
    			rateLimiter[key] = 0;
    		}
    		let now = new Date().getTime();
    		let nextExecution = rateLimiter[key] + minTime;
    		if (now > nextExecution)
    		{
    			callback();
    			rateLimiter[key] = new Date().getTime();
    		}
    	}
    }
    
    // Tests
    if (typeof PARENT_SCRIPT === 'undefined')
    {
    	console.log('Message 1 at ' + new Date());
    	rateLimitedDelay('test');
    	rateLimitedDelay('test');
    	rateLimitedCallback('callback', function() { console.log('Callback 1: ' + new Date()) }); 
    	console.log('Message 2 at ' + new Date());
    	delay(3000);
    	rateLimitedCallback('callback', function() { console.log('Callback 2: ' + new Date()) });
    	rateLimitedDelay('test');
    	console.log('Message 3 at ' + new Date());
    	delay(6000);
    	rateLimitedCallback('callback', function() { console.log('Callback 3: ' + new Date()) });
    	console.log('Message 4 at ' + new Date());
    	rateLimitedDelay('test');
    	console.log('Message 5 at ' + new Date());
    	rateLimitedCallback('callback', function() { console.log('Callback 4: ' + new Date()) });	
    }
    // ========== Rate Limiter End ========== //
    
Viewing 0 reply threads

You must be logged in to reply to this topic.