blue-rings

Blue Rings: distributed counters and registers ----------------------------------------------

Usage no npm install needed!

<script type="module">
  import blueRings from 'https://cdn.skypack.dev/blue-rings';
</script>

README

Blue Rings: distributed counters and registers

The goal is to provide distributed counters usable for billing, with:

  • no centralization / single point of failure
  • "light" real-time updates
  • able to detect and handle network splits

The underlying protocol is currently Axon, a lightweight, native alternative to ZeroMQ on Node.js.

The module also provides "last-writer wins" text registers.

API

const BlueRings = require('blue-rings')
const ring = BlueRings(options);

The options parameter is required; available parameters are:

  • options.host (required), a string uniquely identifying this host (but normally shorter than the hostname, to reduce memory and bandwidth usage);
  • options.subscribe_to, an Array containing one or more tcp://ip:port/ strings suitable to connect to remote Axon/Blue-Rings servers (default remote port is 4000, see below);
  • options.pub, a port number or tcp://0.0.0.0:port/ string suitable to bind the local Axon publisher (the default will bind on port 4000 on all interfaces);
  • options.forward_delay, the number of milliseconds to wait for no updates on a counter before forwarding an update (default: 1000ms);
  • options.connect_delay, the number of milliseconds to wait for no updates on a counter before sending a full update, when a remote server connects (default: 1500ms);
  • options.Value, an object describing how numerical values are interpreted and transmitted.

Constraints on subscribe_to

At this time it is recommended that if server A has an entry subscribe_to pointing to server B, server B should have an entry subscribe_to pointing to server A. In other words subscriptions should be symmetrical. Not doing so would allow you to implement fun topologies (such as a ring) which would also prove very inefficient. This limitation might be removed at some future point if the underlying protocol is changed from Axon to a custom-crafted protocol.

You can also for example implement receiver-only schemes by not setting subscribe_to; the server will receive updates but not propagate any local changes.

Also note that a server might be subscribed-to other servers and not update counters on its own; in this case it is used only as a message router.

Timers

The timers forward_delay and connect_delay are set by default to values adequate for a full-mesh or near-full-mesh setup. If your topology of choice is different, which is probably the case beyond a handful of servers since full-mesh will not scale much, the timers will need to be adapted based on which role you give each server; there are examples in the test suite, and here are some guidelines:

  • on a server that does a lot of message forwarding (for example an apex server in a star topology), the forward_delay should be kept very low (i.e. 0 or 1ms) to ensure quick propagation of updates;
  • in a setup with limited connections between servers, flood_delay should be kept relatively low (200ms for example) to ease convergence;
  • conversely, on a full-mesh network, all timers should be kept high so that individual updates (which will flood the network) are the primary source of updates;
  • obviously the topology is yours to decide; for example you can set up multiple, redundant star topologies on the same network; and even implement a hierarchy, for example redundant stars on each site and full-mesh between the stars' apex servers.

The Value option defaults to providing EcmaScript integers as numerical values. Since Node.js 10.7.0 BigInt is also supported natively, and can be activated by using options.Value = BlueRings.bigint (the default is the equivalent of options.Value = BlueRings.integer).

Here is an example for a service storing Big Integers (arbitrary precision integers).

    options.Value = BlueRings.bigint
    const ring = BlueRings(options);

Methods

ring.setup_counter(name,expire) → ring.update_counter(name,amount) → [coherent,new_value] ring.get_counter(name) → [coherent,value]

This implements a counter name by adding value amount, keeping it until expire. Returns a boolean indicating whether the network is coherent (not-split etc.) and a number representing the new value of the counter.

Note that amount, new_value, value are of the type specified by the Value option; by default they are native Javascript numbers but might be BigInt, bigRat, etc.

ring.setup_text(name,expire) → ring.update_text (name,text) → [coherent,new_value] ring.get_text(name) → [coherent,value]

This implements a Last Writer Wins text register, keeping it until expire.

ring.statistics() -> {recv,recv_tickets,sent,sent_tickets}

ring.end() stops all connections and cleans up.

All methods are synchronous.

Promises

ring.bound is a Promise that resolves once the server is bound. ring.connected is a Promise that resolves the first time all the remote connections (in options.subscribe_to) are successfully established.

Internals

Tickets

Each counter is treated as an independent database; the database contains a series of tickets which represents changes to the counter's value.

Each API request is stored uniquely in the distributed database for counter name as

ticket(timestamp,host,amount)

Each ticket must be globally unique: tickets with identical contents are considered identical.

Network Protocol

The protocol uses two packet types:

  • ping() is used to detect failures in remotes (and compute the coherent boolean flag);
  • new-tickets(name,expire,hash,array-of-tickets) is used to transmit changes to the database.