memcache 0.2.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/ISSUE_TEMPLATE/bug_report.md +14 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +14 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +6 -0
- package/.github/workflows/code-coverage.yaml +41 -0
- package/.github/workflows/codeql.yaml +75 -0
- package/.github/workflows/release.yaml +41 -0
- package/.github/workflows/tests.yaml +40 -0
- package/.nvmrc +1 -0
- package/CODE_OF_CONDUCT.md +128 -0
- package/CONTRIBUTING.md +27 -0
- package/LICENSE +21 -0
- package/README.md +369 -71
- package/SECURITY.md +3 -0
- package/biome.json +35 -0
- package/dist/index.cjs +1502 -0
- package/dist/index.d.cts +501 -0
- package/dist/index.d.ts +501 -0
- package/dist/index.js +1475 -0
- package/docker-compose.yml +24 -0
- package/package.json +38 -17
- package/pnpm-workspace.yaml +2 -0
- package/site/favicon.ico +0 -0
- package/site/logo.ai +7222 -37
- package/site/logo.png +0 -0
- package/site/logo.svg +7 -0
- package/site/logo.webp +0 -0
- package/site/logo_medium.png +0 -0
- package/site/logo_small.png +0 -0
- package/src/index.ts +1130 -0
- package/src/ketama.ts +449 -0
- package/src/node.ts +488 -0
- package/test/index.test.ts +2734 -0
- package/test/ketama.test.ts +526 -0
- package/test/memcache-node-instances.test.ts +102 -0
- package/test/node.test.ts +809 -0
- package/tsconfig.json +29 -0
- package/vitest.config.ts +16 -0
- package/.gitignore +0 -2
- package/Makefile +0 -13
- package/example.js +0 -68
- package/index.js +0 -1
- package/lib/memcache.js +0 -344
- package/test/test-memcache.js +0 -238
package/src/ketama.ts
ADDED
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Orginal Work is from https://github.com/connor4312/ketama
|
|
3
|
+
* Maintained in project for bug fixes and also configuration
|
|
4
|
+
* Thanks connor4312!
|
|
5
|
+
*/
|
|
6
|
+
import { createHash } from "node:crypto";
|
|
7
|
+
import type { HashProvider } from "./index.js";
|
|
8
|
+
import type { MemcacheNode } from "./node.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Function that returns an int32 hash of the input (a number between
|
|
12
|
+
* -2147483648 and 2147483647). If your hashing library gives you a Buffer
|
|
13
|
+
* back, a convenient way to get this is `buf.readInt32BE()`.
|
|
14
|
+
*/
|
|
15
|
+
export type HashFunction = (input: Buffer) => number;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Creates a hash function using a built-in Node.js crypto algorithm.
|
|
19
|
+
* @param algorithm - The name of the hashing algorithm (e.g., "sha1", "md5")
|
|
20
|
+
* @returns A HashFunction that uses the specified algorithm
|
|
21
|
+
*/
|
|
22
|
+
const hashFunctionForBuiltin =
|
|
23
|
+
(algorithm: string): HashFunction =>
|
|
24
|
+
(value) =>
|
|
25
|
+
createHash(algorithm).update(value).digest().readInt32BE();
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Extracts the key from a node, whether it's a string or an object with a key property.
|
|
29
|
+
* @param node - The node to extract the key from
|
|
30
|
+
* @returns The key as a string
|
|
31
|
+
*/
|
|
32
|
+
const keyFor = (node: string | { key: string }) =>
|
|
33
|
+
typeof node === "string" ? node : node.key;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Represents the hash clock, which is an array of [hash, node key] tuples sorted by hash value.
|
|
37
|
+
* This forms the consistent hashing ring where each tuple represents a virtual node position.
|
|
38
|
+
*/
|
|
39
|
+
type HashClock = [hash: number, node: string][];
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* A consistent hashing implementation using the Ketama algorithm.
|
|
43
|
+
* This provides a way to distribute keys across nodes in a way that minimizes
|
|
44
|
+
* redistribution when nodes are added or removed.
|
|
45
|
+
*
|
|
46
|
+
* @template TNode - The type of nodes in the ring (string or object with key property)
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* ```typescript
|
|
50
|
+
* // Create a ring with string nodes
|
|
51
|
+
* const ring = new HashRing(['server1', 'server2', 'server3']);
|
|
52
|
+
* const node = ring.getNode('my-key'); // Returns the node responsible for 'my-key'
|
|
53
|
+
*
|
|
54
|
+
* // Create a ring with weighted nodes
|
|
55
|
+
* const weightedRing = new HashRing([
|
|
56
|
+
* { node: 'server1', weight: 2 },
|
|
57
|
+
* { node: 'server2', weight: 1 }
|
|
58
|
+
* ]);
|
|
59
|
+
*
|
|
60
|
+
* // Create a ring with object nodes
|
|
61
|
+
* const objRing = new HashRing([
|
|
62
|
+
* { key: 'server1', host: 'localhost', port: 11211 },
|
|
63
|
+
* { key: 'server2', host: 'localhost', port: 11212 }
|
|
64
|
+
* ]);
|
|
65
|
+
* ```
|
|
66
|
+
*/
|
|
67
|
+
export class HashRing<TNode extends string | { key: string } = string> {
|
|
68
|
+
/**
|
|
69
|
+
* Base weight of each node in the hash ring. Having a base weight of 1 is
|
|
70
|
+
* not very desirable, since then, due to the ketama-style "clock", it's
|
|
71
|
+
* possible to end up with a clock that's very skewed when dealing with a
|
|
72
|
+
* small number of nodes. Setting to 50 nodes seems to give a better
|
|
73
|
+
* distribution, so that load is spread roughly evenly to +/- 5%.
|
|
74
|
+
*/
|
|
75
|
+
public static baseWeight = 50;
|
|
76
|
+
|
|
77
|
+
/** The hash function used to compute node positions on the ring */
|
|
78
|
+
private readonly hashFn: HashFunction;
|
|
79
|
+
|
|
80
|
+
/** The sorted array of [hash, node key] tuples representing virtual nodes on the ring */
|
|
81
|
+
private _clock: HashClock = [];
|
|
82
|
+
|
|
83
|
+
/** Map of node keys to actual node objects */
|
|
84
|
+
private _nodes = new Map<string, TNode>();
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Gets the sorted array of [hash, node key] tuples representing virtual nodes on the ring.
|
|
88
|
+
* @returns The hash clock array
|
|
89
|
+
*/
|
|
90
|
+
public get clock(): HashClock {
|
|
91
|
+
return this._clock;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Gets the map of node keys to actual node objects.
|
|
96
|
+
* @returns The nodes map
|
|
97
|
+
*/
|
|
98
|
+
public get nodes(): ReadonlyMap<string, TNode> {
|
|
99
|
+
return this._nodes;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Creates a new HashRing instance.
|
|
104
|
+
*
|
|
105
|
+
* @param initialNodes - Array of nodes to add to the ring, optionally with weights
|
|
106
|
+
* @param hashFn - Hash function to use (string algorithm name or custom function, defaults to "sha1")
|
|
107
|
+
*
|
|
108
|
+
* @example
|
|
109
|
+
* ```typescript
|
|
110
|
+
* // Simple ring with default SHA-1 hashing
|
|
111
|
+
* const ring = new HashRing(['node1', 'node2']);
|
|
112
|
+
*
|
|
113
|
+
* // Ring with custom hash function
|
|
114
|
+
* const customRing = new HashRing(['node1', 'node2'], 'md5');
|
|
115
|
+
*
|
|
116
|
+
* // Ring with weighted nodes
|
|
117
|
+
* const weightedRing = new HashRing([
|
|
118
|
+
* { node: 'heavy-server', weight: 3 },
|
|
119
|
+
* { node: 'light-server', weight: 1 }
|
|
120
|
+
* ]);
|
|
121
|
+
* ```
|
|
122
|
+
*/
|
|
123
|
+
constructor(
|
|
124
|
+
initialNodes: ReadonlyArray<TNode | { weight: number; node: TNode }> = [],
|
|
125
|
+
hashFn: string | HashFunction = "sha1",
|
|
126
|
+
) {
|
|
127
|
+
this.hashFn =
|
|
128
|
+
typeof hashFn === "string" ? hashFunctionForBuiltin(hashFn) : hashFn;
|
|
129
|
+
for (const node of initialNodes) {
|
|
130
|
+
if (typeof node === "object" && "weight" in node && "node" in node) {
|
|
131
|
+
this.addNode(node.node, node.weight);
|
|
132
|
+
} else {
|
|
133
|
+
this.addNode(node);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Add a new node to the ring. If the node already exists in the ring, it
|
|
140
|
+
* will be updated. For example, you can use this to update the node's weight.
|
|
141
|
+
*
|
|
142
|
+
* @param node - The node to add to the ring
|
|
143
|
+
* @param weight - The relative weight of this node (default: 1). Higher weights mean more keys will be assigned to this node. A weight of 0 removes the node.
|
|
144
|
+
* @throws {RangeError} If weight is negative
|
|
145
|
+
*
|
|
146
|
+
* @example
|
|
147
|
+
* ```typescript
|
|
148
|
+
* const ring = new HashRing();
|
|
149
|
+
* ring.addNode('server1'); // Add with default weight of 1
|
|
150
|
+
* ring.addNode('server2', 2); // Add with weight of 2 (will handle ~2x more keys)
|
|
151
|
+
* ring.addNode('server1', 3); // Update server1's weight to 3
|
|
152
|
+
* ring.addNode('server2', 0); // Remove server2
|
|
153
|
+
* ```
|
|
154
|
+
*/
|
|
155
|
+
public addNode(node: TNode, weight = 1) {
|
|
156
|
+
if (weight === 0) {
|
|
157
|
+
this.removeNode(node);
|
|
158
|
+
} else if (weight < 0) {
|
|
159
|
+
throw new RangeError("Cannot add a node to the hashring with weight < 0");
|
|
160
|
+
} else {
|
|
161
|
+
this.removeNode(node);
|
|
162
|
+
const key = keyFor(node);
|
|
163
|
+
this._nodes.set(key, node);
|
|
164
|
+
this.addNodeToClock(key, Math.round(weight * HashRing.baseWeight));
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Removes the node from the ring. No-op if the node does not exist.
|
|
170
|
+
*
|
|
171
|
+
* @param node - The node to remove from the ring
|
|
172
|
+
*
|
|
173
|
+
* @example
|
|
174
|
+
* ```typescript
|
|
175
|
+
* const ring = new HashRing(['server1', 'server2']);
|
|
176
|
+
* ring.removeNode('server1'); // Removes server1 from the ring
|
|
177
|
+
* ring.removeNode('nonexistent'); // Safe to call with non-existent node
|
|
178
|
+
* ```
|
|
179
|
+
*/
|
|
180
|
+
public removeNode(node: TNode) {
|
|
181
|
+
const key = keyFor(node);
|
|
182
|
+
if (this._nodes.delete(key)) {
|
|
183
|
+
this._clock = this._clock.filter(([, n]) => n !== key);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Gets the node which should handle the given input key. Returns undefined if
|
|
189
|
+
* the hashring has no nodes.
|
|
190
|
+
*
|
|
191
|
+
* Uses consistent hashing to ensure the same input always maps to the same node,
|
|
192
|
+
* and minimizes redistribution when nodes are added or removed.
|
|
193
|
+
*
|
|
194
|
+
* @param input - The key to find the responsible node for (string or Buffer)
|
|
195
|
+
* @returns The node responsible for this key, or undefined if ring is empty
|
|
196
|
+
*
|
|
197
|
+
* @example
|
|
198
|
+
* ```typescript
|
|
199
|
+
* const ring = new HashRing(['server1', 'server2', 'server3']);
|
|
200
|
+
* const node = ring.getNode('user:123'); // Returns e.g., 'server2'
|
|
201
|
+
* const sameNode = ring.getNode('user:123'); // Always returns 'server2'
|
|
202
|
+
*
|
|
203
|
+
* // Also accepts Buffer input
|
|
204
|
+
* const bufferNode = ring.getNode(Buffer.from('user:123'));
|
|
205
|
+
* ```
|
|
206
|
+
*/
|
|
207
|
+
public getNode(input: string | Buffer): TNode | undefined {
|
|
208
|
+
if (this._clock.length === 0) {
|
|
209
|
+
return undefined;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const index = this.getIndexForInput(input);
|
|
213
|
+
const key =
|
|
214
|
+
index === this._clock.length ? this._clock[0][1] : this._clock[index][1];
|
|
215
|
+
|
|
216
|
+
return this._nodes.get(key);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Finds the index in the clock for the given input by hashing it and performing binary search.
|
|
221
|
+
*
|
|
222
|
+
* @param input - The input to find the clock position for
|
|
223
|
+
* @returns The index in the clock array
|
|
224
|
+
*/
|
|
225
|
+
private getIndexForInput(input: string | Buffer) {
|
|
226
|
+
const hash = this.hashFn(
|
|
227
|
+
typeof input === "string" ? Buffer.from(input) : input,
|
|
228
|
+
);
|
|
229
|
+
return binarySearchRing(this._clock, hash);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Gets multiple replica nodes that should handle the given input. Useful for
|
|
234
|
+
* implementing replication strategies where you want to store data on multiple nodes.
|
|
235
|
+
*
|
|
236
|
+
* The returned array will contain unique nodes in the order they appear on the ring
|
|
237
|
+
* starting from the primary node. If there are fewer nodes than replicas requested,
|
|
238
|
+
* all nodes are returned.
|
|
239
|
+
*
|
|
240
|
+
* @param input - The key to find replica nodes for (string or Buffer)
|
|
241
|
+
* @param replicas - The number of replica nodes to return
|
|
242
|
+
* @returns Array of nodes that should handle this key (length ≤ replicas)
|
|
243
|
+
*
|
|
244
|
+
* @example
|
|
245
|
+
* ```typescript
|
|
246
|
+
* const ring = new HashRing(['server1', 'server2', 'server3', 'server4']);
|
|
247
|
+
*
|
|
248
|
+
* // Get 3 replicas for a key
|
|
249
|
+
* const replicas = ring.getNodes('user:123', 3);
|
|
250
|
+
* // Returns e.g., ['server2', 'server4', 'server1']
|
|
251
|
+
*
|
|
252
|
+
* // If requesting more replicas than nodes, returns all nodes
|
|
253
|
+
* const allNodes = ring.getNodes('user:123', 10);
|
|
254
|
+
* // Returns ['server1', 'server2', 'server3', 'server4']
|
|
255
|
+
* ```
|
|
256
|
+
*/
|
|
257
|
+
public getNodes(input: string | Buffer, replicas: number): TNode[] {
|
|
258
|
+
if (this._clock.length === 0) {
|
|
259
|
+
return [];
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (replicas >= this._nodes.size) {
|
|
263
|
+
return [...this._nodes.values()];
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const chosen = new Set<string>();
|
|
267
|
+
|
|
268
|
+
// We know this will terminate, since we know there are at least as many
|
|
269
|
+
// unique nodes to be chosen as there are replicas
|
|
270
|
+
for (let i = this.getIndexForInput(input); chosen.size < replicas; i++) {
|
|
271
|
+
chosen.add(this._clock[i % this._clock.length][1]);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return [...chosen].map((c) => this._nodes.get(c) as TNode);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Adds virtual nodes to the clock for the given node key.
|
|
279
|
+
* Creates multiple positions on the ring for better distribution.
|
|
280
|
+
*
|
|
281
|
+
* @param key - The node key to add to the clock
|
|
282
|
+
* @param weight - The number of virtual nodes to create (weight * baseWeight)
|
|
283
|
+
*/
|
|
284
|
+
private addNodeToClock(key: string, weight: number) {
|
|
285
|
+
for (let i = weight; i > 0; i--) {
|
|
286
|
+
const hash = this.hashFn(Buffer.from(`${key}\0${i}`));
|
|
287
|
+
this._clock.push([hash, key]);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
this._clock.sort((a, b) => a[0] - b[0]);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* A distribution hash implementation using the Ketama consistent hashing algorithm.
|
|
296
|
+
* This class wraps the HashRing to implement the DistributionHash interface for use with Memcache.
|
|
297
|
+
*
|
|
298
|
+
* @example
|
|
299
|
+
* ```typescript
|
|
300
|
+
* const distribution = new KetamaDistributionHash();
|
|
301
|
+
* distribution.addNode(node1);
|
|
302
|
+
* distribution.addNode(node2);
|
|
303
|
+
* const targetNode = distribution.getNodesByKey('my-key')[0];
|
|
304
|
+
* ```
|
|
305
|
+
*/
|
|
306
|
+
export class KetamaHash implements HashProvider {
|
|
307
|
+
/** The name of this distribution strategy */
|
|
308
|
+
public readonly name = "ketama";
|
|
309
|
+
|
|
310
|
+
/** Internal hash ring for consistent hashing */
|
|
311
|
+
private hashRing: HashRing<string>;
|
|
312
|
+
|
|
313
|
+
/** Map of node IDs to MemcacheNode instances */
|
|
314
|
+
private nodeMap: Map<string, MemcacheNode>;
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Creates a new KetamaDistributionHash instance.
|
|
318
|
+
*
|
|
319
|
+
* @param hashFn - Hash function to use (string algorithm name or custom function, defaults to "sha1")
|
|
320
|
+
*
|
|
321
|
+
* @example
|
|
322
|
+
* ```typescript
|
|
323
|
+
* // Use default SHA-1 hashing
|
|
324
|
+
* const distribution = new KetamaDistributionHash();
|
|
325
|
+
*
|
|
326
|
+
* // Use MD5 hashing
|
|
327
|
+
* const distribution = new KetamaDistributionHash('md5');
|
|
328
|
+
* ```
|
|
329
|
+
*/
|
|
330
|
+
constructor(hashFn?: string | HashFunction) {
|
|
331
|
+
this.hashRing = new HashRing<string>([], hashFn);
|
|
332
|
+
this.nodeMap = new Map();
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Gets all nodes in the distribution.
|
|
337
|
+
* @returns Array of all MemcacheNode instances
|
|
338
|
+
*/
|
|
339
|
+
public get nodes(): Array<MemcacheNode> {
|
|
340
|
+
return Array.from(this.nodeMap.values());
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Adds a node to the distribution with its weight for consistent hashing.
|
|
345
|
+
*
|
|
346
|
+
* @param node - The MemcacheNode to add
|
|
347
|
+
*
|
|
348
|
+
* @example
|
|
349
|
+
* ```typescript
|
|
350
|
+
* const node = new MemcacheNode('localhost', 11211, { weight: 2 });
|
|
351
|
+
* distribution.addNode(node);
|
|
352
|
+
* ```
|
|
353
|
+
*/
|
|
354
|
+
public addNode(node: MemcacheNode): void {
|
|
355
|
+
// Add to internal map for lookups
|
|
356
|
+
this.nodeMap.set(node.id, node);
|
|
357
|
+
// Add to hash ring with weight
|
|
358
|
+
this.hashRing.addNode(node.id, node.weight);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Removes a node from the distribution by its ID.
|
|
363
|
+
*
|
|
364
|
+
* @param id - The node ID (e.g., "localhost:11211")
|
|
365
|
+
*
|
|
366
|
+
* @example
|
|
367
|
+
* ```typescript
|
|
368
|
+
* distribution.removeNode('localhost:11211');
|
|
369
|
+
* ```
|
|
370
|
+
*/
|
|
371
|
+
public removeNode(id: string): void {
|
|
372
|
+
// Remove from internal map
|
|
373
|
+
this.nodeMap.delete(id);
|
|
374
|
+
// Remove from hash ring
|
|
375
|
+
this.hashRing.removeNode(id);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Gets a specific node by its ID.
|
|
380
|
+
*
|
|
381
|
+
* @param id - The node ID (e.g., "localhost:11211")
|
|
382
|
+
* @returns The MemcacheNode if found, undefined otherwise
|
|
383
|
+
*
|
|
384
|
+
* @example
|
|
385
|
+
* ```typescript
|
|
386
|
+
* const node = distribution.getNode('localhost:11211');
|
|
387
|
+
* if (node) {
|
|
388
|
+
* console.log(`Found node: ${node.uri}`);
|
|
389
|
+
* }
|
|
390
|
+
* ```
|
|
391
|
+
*/
|
|
392
|
+
public getNode(id: string): MemcacheNode | undefined {
|
|
393
|
+
return this.nodeMap.get(id);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Gets the nodes responsible for a given key using consistent hashing.
|
|
398
|
+
* Currently returns a single node (the primary node for the key).
|
|
399
|
+
*
|
|
400
|
+
* @param key - The cache key to find the responsible node for
|
|
401
|
+
* @returns Array containing the responsible node(s), empty if no nodes available
|
|
402
|
+
*
|
|
403
|
+
* @example
|
|
404
|
+
* ```typescript
|
|
405
|
+
* const nodes = distribution.getNodesByKey('user:123');
|
|
406
|
+
* if (nodes.length > 0) {
|
|
407
|
+
* console.log(`Key will be stored on: ${nodes[0].id}`);
|
|
408
|
+
* }
|
|
409
|
+
* ```
|
|
410
|
+
*/
|
|
411
|
+
public getNodesByKey(key: string): Array<MemcacheNode> {
|
|
412
|
+
// Get the node from hash ring
|
|
413
|
+
const nodeId = this.hashRing.getNode(key);
|
|
414
|
+
if (!nodeId) {
|
|
415
|
+
return [];
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Map back to MemcacheNode
|
|
419
|
+
const node = this.nodeMap.get(nodeId);
|
|
420
|
+
return node ? [node] : [];
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Performs binary search on the hash ring to find the appropriate position for a given hash.
|
|
426
|
+
* Returns the index of the first virtual node with a hash value >= the input hash.
|
|
427
|
+
* If no such node exists, returns the length of the ring (wraps to beginning).
|
|
428
|
+
*
|
|
429
|
+
* @param ring - The sorted array of [hash, node] tuples
|
|
430
|
+
* @param hash - The hash value to search for
|
|
431
|
+
* @returns The index where the hash should be inserted or the next node position
|
|
432
|
+
*/
|
|
433
|
+
function binarySearchRing(ring: HashClock, hash: number) {
|
|
434
|
+
let mid: number;
|
|
435
|
+
let lo = 0;
|
|
436
|
+
let hi = ring.length - 1;
|
|
437
|
+
|
|
438
|
+
while (lo <= hi) {
|
|
439
|
+
mid = Math.floor((lo + hi) / 2);
|
|
440
|
+
|
|
441
|
+
if (ring[mid][0] >= hash) {
|
|
442
|
+
hi = mid - 1;
|
|
443
|
+
} else {
|
|
444
|
+
lo = mid + 1;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
return lo;
|
|
449
|
+
}
|