Git Product home page Git Product logo

stable-cache's Introduction

Stable Cache

npm Dependency Status devDependency Status Conventional Commits Test Coverage Maintainability Gitter chat

This lib makes extensive use of a concept called producer. A producer is the source of truth of a cached value. Producers are only called when needed to set a cache, and are usually not needed when there's a cache hit.

The proposal of this library is to make it easier to manage producers and how keys are fetched/saved to redis.

Features:

  • there's easily configurable resilience against producer failures:
  • optionally set redis ttl for produced values
  • optionally ignoring cached values and forcing values refresh
  • optionally refreshing keys in background. Useful when you want keys to be updated before their ttl would evict them.
  • optionally producing values in background, while returning the cached value, even on cache misses
  • builtin RTA for prometheus

Usage

Cache instance

If the circuitBreaker config is not provided this cache instance simply will not have a circuit breaker flow, and will still be usable.

I recommend to instantiate a cache for each service you consume, or else the circuit breaker would be shared. Which in turn could mean that a failing service would open the circuit for working services too.

const Redis = require('ioredis');
const { Cache } = require('stable-cache');

const redis = new Redis({
  port: 6379,
  host: 'redis', // localhost
});

const cache = new Cache({
  redis, // required to inject a redis client
  options: { // everything is optional from here
    name: 'someService', // name of the service the cache will handle, useful for RTA
    circuitBreaker: { // circuit breaker config for the service
      // percentage of failing producer requests to trigger the circuit breaker
      threshold: 0.2,
      // time window to be considered by the threshold, in milliseconds
      duration: 10000,
      // attempt to half open the circuit after this time in milliseconds
      halfOpenAfter: 20000,
      // don't open the circuit if there's less than this amount of RPS.
      // This avoids opening the circuit, during failures in low load periods.
      minimumRps: 5,
    },
  },
});

Cache.get method

Only the key argument is required. Other options are used to augment the producer flow. See more usage examples at lib/examples/cache-get-usage.js

Cache get options are:

NOTE: circuit breaker policy is configured only when instantiating the Cache class.

Option Description Default Behavior Default Value
producer A function that will be called when the source of the truth of the key is needed, has the signature function(): Promise<string> No producer to call null
ttl Time in milliseconds for the redis key ttl, in case the producer resolves No ttl set null
producerRetry Object configuration for the exponential backoff No retry null
producerRetry.maxDelay Max amount of delay between retry attempts, in milliseconds - 30000
producerRetry.maxAttempts Max amount of retries for the producer - 10
producerRetry.initialDelay Initial delay, in milliseconds before the retry - 128
producerTimeout Timeout in milliseconds for the producer. Even if there's a timeout, if the producer eventually resolves, the key will be set in background so it would be best if you configure a greater timeout on the producer itself, together with this config No timeout null
returnEarlyFromCache Whether to return the cached value, and make the producer call on background, on cache miss Await the producer on cache miss false
overrideCache Whether to ignore cached values and request producer, regardless of cache hits Don't ignore cache hits, and call producer only if needed false
shouldRefreshKey A callback that if returns true, calls the producer and sets the key on background. Has the signature function(key, currentTTL, options): boolean No automatic refresh of keys null

Example of all the configs:

function producer() {
  return new Promise((resolve) => {
    setTimeout(() => resolve('some-value'), 2000);
  });
}

cache.get(
  'some^key', // required redis key to get
  { // optional configs
    producer,
    ttl: 1000,
    producerRetry: {
      maxDelay: 30000,
      maxAttempts: 10,
      initialDelay: 128
    },
    producerTimeout: 1000,
    returnEarlyFromCache: false,
    overrideCache: false,
    shouldRefreshKey(key, currentTTL, options) {
      if (!options.ttl || currentTTL <= 0) {
        return false;
      }
      // options.ttl is the usually configured ttl for the given key
      const halfLife = options.ttl / 2;
      // refresh key if currentTTL is 50% below the initial ttl
      return currentTTL <= halfLife;
    }
  },
);

Cache.set method

Sets a key with a value, and receives an optional ttl for the key.

Cache set options:

Option Description Default Behavior Default Value
ttl Time in milliseconds for the redis key ttl No ttl set null

Example:

// sets `some^key` with value `a-value` with ttl of 30 seconds
cache.set(
  'some^key', // required key
  'a-value', // required value
  { // optional configs
    ttl: 30000, // 30 seconds
  }
);

Prometheus Exporter

There's an exporter for prometheus that exposes the following metrics

  • cache_results_count: counts cache hits/misses, with labels: ['service', 'operation', 'result']
  • cache_operations_duration_seconds: histogram for cache RT, with labels: ['service', 'operation']
  • cache_operations_count: counts cache operations made, with labels: ['service', 'operation']
  • producer_operations_duration_seconds: histogram with RT for producer calls, with labels: ['service']
  • producer_operations_result_count: counts the successes/errors for producer calls, with labels: ['service', 'result']
  • producer_circuit_break_state: gauge for the state of a service circuit breaker. A value of 0 means the circuit is closed (working normally), and a value of 1 means the circuit is open (fail fast is activated).

What labels mean?

  • service: is the cache name option
  • cache operation: type of redis operation, e.g. set, get.
  • cache result: cache hit or miss
  • producer result: success, or error

Usage

Prometheus exporter options for the constructor are:

Option Description Default Behavior Default Value
prefix Prefix to be added to every exported metric No prefix added ''
registers Array of prometheus registers to which metrics should be exported Use default prom-client register [Prometheus.register]
cacheBuckets Array of numbers, representing the histogram buckets for cache RT Use default value Prometheus.exponentialBuckets(0.05, 2, 8)
producerBuckets Array of numbers, representing the histogram buckets for producer RT Use default value Prometheus.exponentialBuckets(0.1, 2, 8)

Example:

const Prometheus = require('prom-client');
const { PrometheusExporter } = require('stable-cache');

const exporter = new PrometheusExporter({
  prefix: 'my_app_',
  registers: [Prometheus.register],
  cacheBuckets: Prometheus.exponentialBuckets(0.05, 2, 8),
  producerBuckets: Prometheus.exponentialBuckets(0.1, 2, 8),
});

exporter.collectMetrics(); // start collecting metrics

stable-cache's People

Contributors

joao-fontenele avatar limafm avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

stable-cache's Issues

add minimumRps parameter on Cache instance demonstration (README)

Tried to use the lib, but for some reason the cb was not working propertly.
Reading the code, I noticed that the minimumRps parameter existed and that it was a source of my problem.

So, I'm suggesting to add this parameter just like that:

const cache = new Cache({
  redis, // required to inject a redis client
  options: { // everything is optional from here
    name: 'someService', // name of the service the cache will handle, useful for RTA
    circuitBreaker: { // circuit breaker config for the service
      // percentage of failing producer requests to trigger the circuit breaker
      threshold: 0.2,
      // time window to be considered by the threshold, in milliseconds
      duration: 10000,
      // attempt to half open the circuit after this time in milliseconds
      halfOpenAfter: 20000,
      // don't open the circuit if there's less than this amount of RPS.
      // This avoids opening the circuit, during failures in low load periods.
      minimumRps: 5,
    },
  },
});

(instructions copy-pasted from classes/policies)

Bug: circuit breaker close events aren't being triggered correctly

I believe it's due to the fact that onReset event listeners are cleaned up after the producer promise finishes

listeners setup: https://github.com/joao-fontenele/simple-cache/blob/adcc2ae2768b5789bf0c352a76566245628ac947/lib/classes/producer.js#L41-L43
listerners cleanup: https://github.com/joao-fontenele/simple-cache/blob/adcc2ae2768b5789bf0c352a76566245628ac947/lib/classes/producer.js#L90-L91

one possible solution is to move policy creation and listening to the level of the cache class

Implement a way to RTA

  • have visibility of both cache and producer performance
    • cache hits/misses
    • cache RT
    • producer errors/successes
    • producer RT
    • ?
  • visibility of possible circuit breaks
    • it's likely that there are multiple instances of the cache class, if there's multiple types of producer. Eg. a cache class instance for using Database A and another for Service B. Since it would not make sense to share the same circuitBreaker, that is an attribute of a cache

An idea for enabling this feature is using events. Maybe add another class/lib for exporting these events to prometheus?

redis get epic

as a user, I want to make a simple redis get by key with:

  • have an option to compress producer results? or is it producer responsibility? if it's the producer responsibility, who is responsible for decompressing on read?
  • e2e tests
  • have an option to avoid thundering producer calls, or is it producer responsibility?
  • have an option to set prefixes to the key? (maybe just expect the user to pass the key correctly)
  • have an option to call producer directly in case of redis faults, circuit break redis?
  • ci
  • docs
  • have an option to rta the performance of producers and cache
  • have an option to refresh keys automatically once a ttl threshold is crossed
  • an optional producer
  • set an optional ttl to the key
  • have an option of in case of cache miss, make the producer call and cache set in background
  • have an option to await the producer and cache set before
  • have an option to override the cache and renew the key with producer data
  • have an option to throw a timeout on the whole get, but request producer and set key in background
  • have an option to circuit break the producer in case of errors? or is it producer responsibility

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.