Skip to content

QUESTION: cancel removeTokens() #33

@daldridge-cs

Description

@daldridge-cs

I'm wondering how folks are gracefully cancelling/aborting calls to RateLimiter.removeTokens()?

Here's a sample use case where I'd want to cancel:

  1. User registers with my app to pull Fitbit data as it becomes available
  2. App adds a listener for notifications from Fitbit that new data is available
  3. App instantiates a subscription handler that queues inbound updates and throttles requests to FB via a RateLimiter instance):
class SubscriptionHandler {
  constructor(key, handlerFn) {
    this.key = key;
    this.handlerFn = handlerFn;
    this.rateLimiter = new limiter.RateLimiter(FITBIT_LIMIT_PER_USER_PER_HOUR, 'hour');
    this.queue = async.queue((notificationId, callback) => {
      this.rateLimiter.removeTokens(1, (error, remainingRequests) => {
        this._processNotification(notificationId, callback);
      });
    });

  enqueue(data, completionCallback) {
    this.queue.push(data.id);
  }

const myHandler = new SubscriptionHandler(myKey, myFBDataFetch);
  1. Upon receiving such a notification, the app enqueues its handling, which occurs when RateLimiter::removeTokens() successfully drains its token bucket.
  2. User de-registers before any number of enqueued notifications have been processed (correctly on-hold/pending by the rate limiter)
  3. ???

Step (5) is what's in question. I'd like to cancel the wait on removeTokens() and allow the handler instance to be garbage collected:

class SubscriptionHandler {
  constructor(key, handlerFn) {
    this.key = key;
    this.handlerFn = handlerFn;
    this.rateLimiter = new limiter.RateLimiter(FITBIT_LIMIT_PER_USER_PER_HOUR, 'hour');
    this.queue = async.queue((notificationId, callback) => {
      this.tokenTimeoutID = this.rateLimiter.removeTokens(1, (error, remainingRequests) => {
        this._processNotification(notificationId, callback);
      });
    });

  release() {
    // Maybe something like this?
    clearTimeout(this.tokenTimeoutID);
    delete this.rateLimiter;
    this.queue.kill();
    delete this.queue;
  }
}

myHandler.release();
delete myHandler;

Unfortunately, with the existing implementation of RateLimiter and its internal TokenBucket, there is no way to obtain the timeoutID assigned when TokenBucket invokes its internal comeBackLater() method. (See https://github.com/jhurliman/node-rate-limiter/blob/master/lib/tokenBucket.js, line 108). Thus, there is always an outstanding reference to the SubscriptionHandler instance, and i cannot get it garbage collected.

If the timer ID allocated by setTimeout() were returned from comeBackLater() (rather than the false value currently returned) and bubbled back out as the return value from RateLimiter::removeTokens(), then I could cancel it.

Obviously that breaks encapsulation, and perhaps the RateLimiter or its internal TokenBucket could keep rack of the timer ID and have methods for cancelling it, but I'm simply trying to get my idea across...

Given the above scenario, what would you recommend? @jhurliman ?

(Note that I prefer the semantics of the asynchronous removeTokens() method, and would rather not switch to my own periodic retry calling tryRemoveTokens()...)

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions