UNPKG

ketama

Version:

A hash ring implementation using libketama-like hashing.

115 lines 4.26 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.HashRing = void 0; const crypto_1 = require("crypto"); const hashFunctionForBuiltin = (algorithm) => value => crypto_1.createHash(algorithm).update(value).digest().readInt32BE(); const keyFor = (node) => (typeof node === 'string' ? node : node.key); 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]); } } exports.HashRing = HashRing; /** * 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