-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathconsistent-hash.js
More file actions
168 lines (156 loc) · 5.09 KB
/
Copy pathconsistent-hash.js
File metadata and controls
168 lines (156 loc) · 5.09 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
import crypto from 'node:crypto'
/**
* ConsistentHash provides an implementation of consistent hashing using virtual nodes
* for improved key distribution. It maintains a ring structure of 32-bit hashed virtual
* node values, mapping them to their corresponding real nodes.
*
* This module is designed to be published on NPM and used as a standalone library.
*
* Example usage:
* import ConsistentHash from 'fast-hashring'
* const ch = new ConsistentHash({ virtualNodes: 100 })
* ch.addNode('server1')
* const node = ch.getNode('my-key')
*
* @module consistent-hash
*/
const UINT32_MAX = 0xffffffff
class ConsistentHash {
/**
* Create a new ConsistentHash instance.
*
* @param {Object} [options={}] - Configuration options.
* @param {number} [options.virtualNodes=100] - Number of virtual nodes per real node (positive integer).
*/
constructor(options = {}) {
const virtualNodes = options.virtualNodes ?? 100;
if (!Number.isInteger(virtualNodes) || virtualNodes < 1) {
throw new Error('virtualNodes must be a positive integer');
}
this.virtualNodes = virtualNodes;
this.ring = []; // Sorted array of 32-bit virtual node hashes.
this.nodes = new Set(); // Set of real node identifiers.
this.virtualToReal = new Map(); // Maps 32-bit virtual node hash to real node.
}
/**
* Generate a 32-bit hash for a given key (first 4 bytes of MD5).
*
* @private
* @param {string} key - The key to hash.
* @returns {number} The unsigned 32-bit hash.
*/
_hash(key) {
return crypto.createHash('md5').update(key).digest().readUInt32BE(0);
}
/**
* Add a new node to the hash ring.
*
* @param {string} node - The node identifier.
* @throws {Error} If node is not a non-empty string or already exists.
*/
addNode(node) {
if (!node || typeof node !== 'string') {
throw new Error('Node must be a non-empty string');
}
if (this.nodes.has(node)) {
throw new Error('Node already exists');
}
this.nodes.add(node);
// Create virtual nodes for the real node.
for (let i = 0; i < this.virtualNodes; i++) {
const hash = this._hash(`${node}-vn-${i}`);
this.ring.push(hash);
this.virtualToReal.set(hash, node);
}
// Keep the ring sorted for efficient binary search (numeric comparator).
this.ring.sort((a, b) => a - b);
}
/**
* Add multiple nodes to the hash ring, sorting the ring only once.
*
* @param {string[]} nodes - The node identifiers.
* @throws {Error} If nodes is not a non-empty array, or any node is invalid/duplicated.
*/
addNodes(nodes) {
if (!Array.isArray(nodes) || nodes.length === 0) {
throw new Error('Nodes must be a non-empty array');
}
for (const node of nodes) {
if (!node || typeof node !== 'string') {
throw new Error('Node must be a non-empty string');
}
if (this.nodes.has(node) || nodes.indexOf(node) !== nodes.lastIndexOf(node)) {
throw new Error('Node already exists');
}
this.nodes.add(node);
}
for (const node of nodes) {
for (let i = 0; i < this.virtualNodes; i++) {
const hash = this._hash(`${node}-vn-${i}`);
this.ring.push(hash);
this.virtualToReal.set(hash, node);
}
}
this.ring.sort((a, b) => a - b);
}
/**
* Remove a node and its associated virtual nodes from the hash ring.
*
* @param {string} node - The node identifier.
* @throws {Error} If the node does not exist.
*/
removeNode(node) {
if (!this.nodes.has(node)) {
throw new Error('Node does not exist');
}
this.nodes.delete(node);
// Remove all virtual nodes associated with the given node.
const removed = new Set();
for (let i = 0; i < this.virtualNodes; i++) {
const hash = this._hash(`${node}-vn-${i}`);
this.virtualToReal.delete(hash);
removed.add(hash);
}
// Rebuild the ring without the removed virtual nodes.
this.ring = this.ring.filter((hash) => !removed.has(hash));
}
/**
* Get the node responsible for a given key.
* Returns null if no nodes are present.
*
* @param {string} key - The key to look up.
* @returns {string|null} The node responsible for the key, or null if none exists.
* @throws {Error} If the key is not a non-empty string.
*/
getNode(key) {
if (!key || typeof key !== 'string') {
throw new Error('Key must be a non-empty string');
}
if (this.ring.length === 0) {
return null;
}
const hash = this._hash(key);
const ring = this.ring;
// Binary search to find the first virtual node hash greater than or equal to the key hash.
let low = 0, high = ring.length;
while (low < high) {
const mid = (low + high) >>> 1;
if (ring[mid] < hash) {
low = mid + 1;
} else {
high = mid;
}
}
const index = low % ring.length; // Wrap around if needed.
return this.virtualToReal.get(ring[index]);
}
/**
* Get the number of real nodes in the hash ring.
*
* @returns {number} The count of real nodes.
*/
size() {
return this.nodes.size;
}
}
export default ConsistentHash