Giter Site home page Giter Site logo

localstorage-retry's Introduction

localstorage-retry

Circle CI

Note

Segment has paused maintenance on this project, but may return it to an active status in the future. Issues and pull requests from external contributors are not being considered, although internal contributions may appear from time to time. The project remains available under its open source license for anyone to use.

Provides durable retries with a queue held in localStorage (with graceful fallbacks to memory when necessary).

How It Works

Each page maintains its own list of queued and in-progress tasks, while constantly refreshing its ack time. If a queue goes more than 10s without updating its ack, another page will remove it, claim all queued tasks, and retry all in-progress tasks.

API

new Queue(name, [opts], processFunc(item, done(err, res)))

You can omit the opts argument to initialize the queue with defaults:

var Queue = require('@segment/localstorage-retry');

var queue = new Queue('my_queue_name', function process(item, done) {
  sendAsync(item, function(err, res) {
    if (err) return done(err);
    done(null, res);
  });
});

queue.on('processed', function(err, res, item) {
  if (err) return console.warn('processing %O failed with error %O', item, err);
  console.log('successfully sent %O with response %O', item, res);
});

queue.start();

Options

The queue can be initialized with the following options (defaults shown):

var options = {
  minRetryDelay: 1000,   // min retry delay in ms (used in exp. backoff calcs)
  maxRetryDelay: 30000,  // max retry delay in ms (used in exp. backoff calcs)
  backoffFactor: 2,      // exponential backoff factor (attempts^n)
  backoffJitter: 0,      // jitter factor for backoff calcs (0 is usually fine)
  maxItems: Infinity     // queue high water mark (we suggest 100 as a max)
  maxAttempts: Infinity  // max retry attempts before discarding
};

var queue = new Queue('my_queue_name', options, (item, done) => {
  sendAsync(item, (err, res) => {
    if (err) return done(err);
    done(null, res);
  });
});

queue.start();

.addItem(item)

Adds an item to the queue

queue.addItem({ a: 'b' });

.getDelay (attemptNumber) -> ms

Can be overridden to provide a custom retry delay in ms. You'll likely want to use the queue instance's backoff constants here.

this.backoff = {
  MIN_RETRY_DELAY: opts.minRetryDelay || 1000,
  MAX_RETRY_DELAY: opts.maxRetryDelay || 30000,
  FACTOR: opts.backoffFactor || 2,
  JITTER: opts.backoffJitter || 0
};

Default implementation:

queue.getDelay = function(attemptNumber) {
  var ms = this.backoff.MIN_RETRY_DELAY * Math.pow(this.backoff.FACTOR, attemptNumber);
  if (this.backoff.JITTER) {
    var rand =  Math.random();
    var deviation = Math.floor(rand * this.backoff.JITTER * ms);
    if (Math.floor(rand * 10) < 5) {
      ms -= deviation;
    } else {
      ms += deviation;
    }
  }
  return Number(Math.min(ms, this.backoff.MAX_RETRY_DELAY).toPrecision(1));
};

.shouldRetry (item, attemptNumber, error) -> boolean

Can be overridden to provide custom logic for whether to requeue the item. You'll likely want to use the queue instance's maxAttempts variable (which is overridable via constructor's opts argument).

Default:

queue.shouldRetry = function(item, attemptNumber, error) {
  if (attemptNumber > this.maxAttempts) return false;
  return true;
};

You may also want to selectively retry based on error returned by your process function or something in the item itself.

Override Example:

queue.shouldRetry = function(item, attemptNumber, error) {
  // max attempts
  if (attemptNumber > this.maxAttempts) return false;

  // based on something in the item itself
  if (new Date(item.timestamp) - new Date() > 86400000) return false;

  // selective error handling
  if (error.code === '429') return false;

  return true;
}

.start

Starts the queue processing items. Anything added before calling .start will be queued until .start is called.

queue.start();

.stop

Stops the queue from processing. Any retries queued may be picked claimed by another queue after a timeout.

queue.stop();

Emitter

You can listen for processed events, which are emitted with each invocation of the processFunc and passed any error or response provided along with the item itself.

If a message is discarded entirely because it does not pass your shouldRetry logic upon attempted re-enqueuing, the queue will emit a discard event.

If the queue is reclaiming events from an abandonded queue, and sees duplicate entries, we will keep the first, and discard the rest, emitting a duplication event for each.

processed

queue.on('processed', function(err, res, item) {
  if (err) return console.warn('processing %O failed with error %O', item, err);
  console.log('successfully sent %O with response %O', item, res);
});

discard

queue.on('discard', function(item, attempts) {
  console.error('discarding message %O after %d attempts', item, attempts);
})

duplication

queue.on('duplication', function(item, attempts) {
  console.error('discarding message %O due to duplicate entries', item, attempts);
})

License

Released under the MIT License

localstorage-retry's People

Contributors

bhavanki avatar dahaden avatar f2prateek avatar fathyb avatar juliofarah avatar nettofarah avatar sperand-io avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar

localstorage-retry's Issues

ReferenceError: window is not defined

Attempting to run some tests with jest (CRA) and I get the following error:

Test suite failed to run

    ReferenceError: window is not defined

      at Object.<anonymous> (node_modules/@segment/localstorage-retry/lib/schedule.js:12:9)
      at Object.<anonymous> (node_modules/@segment/localstorage-retry/lib/index.js:6:16)

When looking at the code for lib/schedule.js it references window.setTimeout, window.clearTimeout and window.Date. I think this can conflict with jest's mocking of the window object and timer functions.

Inmemory store cannot clean up localstorage if localstorage is full at the start of the session

Carrying on from #20

When creating an instance of localstorage-retry, Store will grab the defaultEngine and use this for the originalEngine which is used to drive the reclaim mechanism

My assumption inside of the last PR was that defaultEngine would always be localstorage. However, engine runs a check and assigns the store to localstorage or inmemory before the module is resolved.

This means, if localstorage is so full that we cant insert a uuid and "test_value", then we will only ever run reclaim against the inmemory engine.

Event duplication on stressed clients

Setup:

  • A couple of unsent events in localstorage,
  • Multiple tabs of the same tab domain open, running the segment client,
  • Client machine struggling to meet the time intervals for updating ack

Something we have been seeing recently is when the clients machine is resource constrained, the reclaim method between tabs of the same domain can cause duplication of events up to the 10s of thousands.

We believe this issue would have been made much worse due to the the previous issues where the reclaim mechanism can keep hold of the localstorage reference (rather than switching to in memory when full with everything else).

This seems to not only cause the clients to be slower for longer, but it can cause very slow requests to the servers.

We believe this issue comes about when, for some reason, some tabs fail to update ack consistently, but are able to run the reclaim process. This can lead to queues copying each other at the same time, with cyclic reclaims occurring, or even nested reclaims that are 3 or 4 queues deep.

Queues that use in memory storage cannot run reclaim on abandonded localstorage queues

Something we have started to see recently in an extreme case, is when the localstorage is full of events from abandoned queues, localstorage-retry is unable to reclaim these queues.

In theory, queues should be able to be cleaned up as they move to localstorage, but in some extreme cases, we are seeing the clean up halt half way through and all new queues resort to in memory store.

Currently, this leaves localstorage-retry in an unrecoverable state, where is will always use in memory storage over local storage.

To fix this, we should be able to run the reclaim mechanism on localstorage, even when the main queue is in memory.

Deleting "ack" can break reclaiming

There are two places where ack can be deleted:

  1. When switching to in memory,
  2. When a queue is reclaimed.

In a small percentage of cases, browsers delete some of the keys, but not all.
ack should always be deleted last other wise the queue cannot be cleaned up later if only half processed.

Unable to get property 'split' of undefined or null reference

Hi, our error monitoring tool captured the error like in the title by https://cdn.segment.com/analytics.js/v1/KEY/analytics.min.js

Details

Unable to get property 'split' of undefined or null reference

After checking out the stack trace I mapped it to that exact fragment:

for (var i = 0; i < storage.length; i++) {
var k = storage.key(i);
var parts = k.split('.');

Looks like some storage reports length > 0, but .key(0) returns null or undefined.

Occurences

We had around 10 reports during last 2 months, so the issue is marginal

User information:

  • Edge Version:18.18362
  • Windows 10

Actions

The easiest solution would be to add a guard to prevent splitting non-string values, but it would not address the underlying error that the storage implementation may be glitched.

Please let me know if I can help to fix it.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.