ketama
Version:
A hash ring implementation using libketama-like hashing.
111 lines • 4.13 kB
JavaScript
import { createHash } from 'crypto';
const hashFunctionForBuiltin = (algorithm) => value => createHash(algorithm).update(value).digest().readInt32BE();
const keyFor = (node) => (typeof node === 'string' ? node : node.key);
export class HashRing {
constructor(initialNodes = [], hashFn = 'sha1') {
this.clock = [];
this.nodes = new Map();
this.hashFn = typeof hashFn === 'string' ? hashFunctionForBuiltin(hashFn) : hashFn;
for (const node of initialNodes) {
if (typeof node === 'object' && 'weight' in node && 'node' in node) {
this.addNode(node.node, node.weight);
}
else {
this.addNode(node);
}
}
}
/**
* Add a new node to the ring. If the node already exists in the ring, it
* will be updated. For example, you can use this to update the node's weight.
*/
addNode(node, weight = 1) {
if (weight === 0) {
this.removeNode(node);
}
else if (weight < 0) {
throw new RangeError(`Cannot add a node to the hashring with weight < 0`);
}
else {
this.removeNode(node);
const key = keyFor(node);
this.nodes.set(key, node);
this.addNodeToClock(key, Math.round(weight * HashRing.baseWeight));
}
}
/**
* Removes th enode from the ring. No-op's if the node does not exist.
*/
removeNode(node) {
const key = keyFor(node);
if (this.nodes.delete(key)) {
this.clock = this.clock.filter(([, n]) => n !== key);
}
}
/**
* Gets the node which should handle the given input. Returns undefined if
* the hashring has no elements with weight.
*/
getNode(input) {
if (this.clock.length === 0) {
return undefined;
}
const index = this.getIndexForInput(input);
const key = index === this.clock.length ? this.clock[0][1] : this.clock[index][1];
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return this.nodes.get(key);
}
getIndexForInput(input) {
const hash = this.hashFn(typeof input === 'string' ? Buffer.from(input) : input);
return binarySearchRing(this.clock, hash);
}
/**
* Gets the "replicas" number of nodes that should handle the input. The
* returned array length wiill equal the number of replicas, except if
* there are fewer nodes available than replicas requested.
*/
getNodes(input, replicas) {
if (replicas >= this.nodes.size) {
return [...this.nodes.values()];
}
const chosen = new Set();
// We know this will terminate, since we know there are at least as many
// unique nodes to be chosen as there are replicas
for (let i = this.getIndexForInput(input); chosen.size < replicas; i++) {
chosen.add(this.clock[i % this.clock.length][1]);
}
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return [...chosen].map(c => this.nodes.get(c));
}
addNodeToClock(key, weight) {
for (let i = weight; i > 0; i--) {
const hash = this.hashFn(Buffer.from(key + `\0` + i));
this.clock.push([hash, key]);
}
this.clock.sort((a, b) => a[0] - b[0]);
}
}
/**
* Base weight of each node in the hash ring. Having a base weight of 1 is
* not very desirable, since then, due to the ketama-style "clock", it's
* possible to end up with a clock that's very skewed when dealing with a
* small number of nodes. Setting to 50 nodes seems to give a better
* distrubtion, so that load is spread roughly evenly to +/- 5%.
*/
HashRing.baseWeight = 50;
function binarySearchRing(ring, hash) {
let mid;
let lo = 0;
let hi = ring.length - 1;
while (lo <= hi) {
mid = Math.floor((lo + hi) / 2);
if (ring[mid][0] >= hash) {
hi = mid - 1;
}
else {
lo = mid + 1;
}
}
return lo;
}
//# sourceMappingURL=index.js.map