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
|
@@ -0,0 +1,526 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { HashRing, KetamaHash } from "../src/ketama.js";
|
|
3
|
+
import { MemcacheNode } from "../src/node.js";
|
|
4
|
+
|
|
5
|
+
describe("HashRing", () => {
|
|
6
|
+
describe("constructor", () => {
|
|
7
|
+
it("should create empty ring with no initial nodes", () => {
|
|
8
|
+
const ring = new HashRing<string>();
|
|
9
|
+
expect(ring.nodes.size).toBe(0);
|
|
10
|
+
expect(ring.clock.length).toBe(0);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("should create ring with simple string nodes", () => {
|
|
14
|
+
const ring = new HashRing<string>(["node1", "node2", "node3"]);
|
|
15
|
+
expect(ring.nodes.size).toBe(3);
|
|
16
|
+
expect(ring.clock.length).toBeGreaterThan(0);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("should create ring with weighted nodes", () => {
|
|
20
|
+
const ring = new HashRing<string>([
|
|
21
|
+
{ node: "heavy", weight: 3 },
|
|
22
|
+
{ node: "light", weight: 1 },
|
|
23
|
+
]);
|
|
24
|
+
expect(ring.nodes.size).toBe(2);
|
|
25
|
+
// Heavy node should have more virtual nodes
|
|
26
|
+
const heavyVirtualNodes = ring.clock.filter(([, n]) => n === "heavy");
|
|
27
|
+
const lightVirtualNodes = ring.clock.filter(([, n]) => n === "light");
|
|
28
|
+
expect(heavyVirtualNodes.length).toBeGreaterThan(
|
|
29
|
+
lightVirtualNodes.length,
|
|
30
|
+
);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("should create ring with mixed weighted and unweighted nodes", () => {
|
|
34
|
+
const ring = new HashRing<string>([
|
|
35
|
+
"simple",
|
|
36
|
+
{ node: "weighted", weight: 2 },
|
|
37
|
+
]);
|
|
38
|
+
expect(ring.nodes.size).toBe(2);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("should create ring with custom hash function (md5)", () => {
|
|
42
|
+
const ring = new HashRing<string>(["node1"], "md5");
|
|
43
|
+
expect(ring.clock.length).toBeGreaterThan(0);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("should create ring with custom hash function", () => {
|
|
47
|
+
const customHash = (buf: Buffer) => {
|
|
48
|
+
let hash = 0;
|
|
49
|
+
for (let i = 0; i < buf.length; i++) {
|
|
50
|
+
hash = (hash << 5) - hash + buf[i];
|
|
51
|
+
hash |= 0; // Convert to 32bit integer
|
|
52
|
+
}
|
|
53
|
+
return hash;
|
|
54
|
+
};
|
|
55
|
+
const ring = new HashRing<string>(["node1"], customHash);
|
|
56
|
+
expect(ring.clock.length).toBeGreaterThan(0);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("should handle object nodes with key property", () => {
|
|
60
|
+
const ring = new HashRing<{ key: string; port: number }>([
|
|
61
|
+
{ key: "server1", port: 11211 },
|
|
62
|
+
{ key: "server2", port: 11212 },
|
|
63
|
+
]);
|
|
64
|
+
expect(ring.nodes.size).toBe(2);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe("getters", () => {
|
|
69
|
+
it("should return clock via getter", () => {
|
|
70
|
+
const ring = new HashRing<string>(["node1"]);
|
|
71
|
+
const clock = ring.clock;
|
|
72
|
+
expect(Array.isArray(clock)).toBe(true);
|
|
73
|
+
expect(clock.length).toBeGreaterThan(0);
|
|
74
|
+
expect(clock[0]).toHaveLength(2); // [hash, node]
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("should return nodes via getter", () => {
|
|
78
|
+
const ring = new HashRing<string>(["node1", "node2"]);
|
|
79
|
+
const nodes = ring.nodes;
|
|
80
|
+
expect(nodes instanceof Map).toBe(true);
|
|
81
|
+
expect(nodes.size).toBe(2);
|
|
82
|
+
expect(nodes.get("node1")).toBe("node1");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("should return readonly nodes map", () => {
|
|
86
|
+
const ring = new HashRing<string>(["node1"]);
|
|
87
|
+
const nodes = ring.nodes;
|
|
88
|
+
expect(nodes).toBeInstanceOf(Map);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe("addNode", () => {
|
|
93
|
+
it("should add a node with default weight", () => {
|
|
94
|
+
const ring = new HashRing<string>();
|
|
95
|
+
ring.addNode("node1");
|
|
96
|
+
expect(ring.nodes.size).toBe(1);
|
|
97
|
+
expect(ring.clock.length).toBe(HashRing.baseWeight);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("should add a node with custom weight", () => {
|
|
101
|
+
const ring = new HashRing<string>();
|
|
102
|
+
ring.addNode("node1", 2);
|
|
103
|
+
expect(ring.nodes.size).toBe(1);
|
|
104
|
+
expect(ring.clock.length).toBe(HashRing.baseWeight * 2);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("should update existing node weight", () => {
|
|
108
|
+
const ring = new HashRing<string>();
|
|
109
|
+
ring.addNode("node1", 1);
|
|
110
|
+
const initialClockLength = ring.clock.length;
|
|
111
|
+
ring.addNode("node1", 2);
|
|
112
|
+
expect(ring.nodes.size).toBe(1);
|
|
113
|
+
expect(ring.clock.length).toBe(initialClockLength * 2);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("should remove node when weight is 0", () => {
|
|
117
|
+
const ring = new HashRing<string>(["node1"]);
|
|
118
|
+
expect(ring.nodes.size).toBe(1);
|
|
119
|
+
ring.addNode("node1", 0);
|
|
120
|
+
expect(ring.nodes.size).toBe(0);
|
|
121
|
+
expect(ring.clock.length).toBe(0);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("should throw error for negative weight", () => {
|
|
125
|
+
const ring = new HashRing<string>();
|
|
126
|
+
expect(() => ring.addNode("node1", -1)).toThrow(RangeError);
|
|
127
|
+
expect(() => ring.addNode("node1", -1)).toThrow(
|
|
128
|
+
"Cannot add a node to the hashring with weight < 0",
|
|
129
|
+
);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
describe("removeNode", () => {
|
|
134
|
+
it("should remove existing node", () => {
|
|
135
|
+
const ring = new HashRing<string>(["node1", "node2"]);
|
|
136
|
+
expect(ring.nodes.size).toBe(2);
|
|
137
|
+
ring.removeNode("node1");
|
|
138
|
+
expect(ring.nodes.size).toBe(1);
|
|
139
|
+
expect(ring.nodes.has("node1")).toBe(false);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("should be no-op when removing non-existent node", () => {
|
|
143
|
+
const ring = new HashRing<string>(["node1"]);
|
|
144
|
+
ring.removeNode("nonexistent");
|
|
145
|
+
expect(ring.nodes.size).toBe(1);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("should remove all virtual nodes from clock", () => {
|
|
149
|
+
const ring = new HashRing<string>(["node1"]);
|
|
150
|
+
const initialClockLength = ring.clock.length;
|
|
151
|
+
expect(initialClockLength).toBeGreaterThan(0);
|
|
152
|
+
ring.removeNode("node1");
|
|
153
|
+
expect(ring.clock.length).toBe(0);
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
describe("getNode", () => {
|
|
158
|
+
it("should return node for a given key", () => {
|
|
159
|
+
const ring = new HashRing<string>(["node1", "node2", "node3"]);
|
|
160
|
+
const node = ring.getNode("test-key");
|
|
161
|
+
expect(node).toBeDefined();
|
|
162
|
+
expect(["node1", "node2", "node3"]).toContain(node);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("should return consistent node for same key", () => {
|
|
166
|
+
const ring = new HashRing<string>(["node1", "node2", "node3"]);
|
|
167
|
+
const node1 = ring.getNode("test-key");
|
|
168
|
+
const node2 = ring.getNode("test-key");
|
|
169
|
+
expect(node1).toBe(node2);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("should return undefined for empty ring", () => {
|
|
173
|
+
const ring = new HashRing<string>();
|
|
174
|
+
const node = ring.getNode("test-key");
|
|
175
|
+
expect(node).toBeUndefined();
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("should accept Buffer input", () => {
|
|
179
|
+
const ring = new HashRing<string>(["node1", "node2"]);
|
|
180
|
+
const node = ring.getNode(Buffer.from("test-key"));
|
|
181
|
+
expect(node).toBeDefined();
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it("should return same node for string and Buffer with same content", () => {
|
|
185
|
+
const ring = new HashRing<string>(["node1", "node2"]);
|
|
186
|
+
const nodeFromString = ring.getNode("test-key");
|
|
187
|
+
const nodeFromBuffer = ring.getNode(Buffer.from("test-key"));
|
|
188
|
+
expect(nodeFromString).toBe(nodeFromBuffer);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it("should distribute keys across nodes", () => {
|
|
192
|
+
const ring = new HashRing<string>(["node1", "node2", "node3"]);
|
|
193
|
+
const distribution = new Map<string, number>();
|
|
194
|
+
|
|
195
|
+
// Test with many keys to ensure distribution
|
|
196
|
+
for (let i = 0; i < 300; i++) {
|
|
197
|
+
const node = ring.getNode(`key-${i}`);
|
|
198
|
+
if (node) {
|
|
199
|
+
distribution.set(node, (distribution.get(node) || 0) + 1);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// All nodes should get some keys
|
|
204
|
+
expect(distribution.size).toBe(3);
|
|
205
|
+
// Each node should get roughly 1/3 of keys (with some variance)
|
|
206
|
+
for (const count of distribution.values()) {
|
|
207
|
+
expect(count).toBeGreaterThan(50); // At least some keys
|
|
208
|
+
expect(count).toBeLessThan(200); // Not too many keys
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
describe("getNodes (replicas)", () => {
|
|
214
|
+
it("should return empty array for empty ring", () => {
|
|
215
|
+
const ring = new HashRing<string>();
|
|
216
|
+
const nodes = ring.getNodes("test-key", 3);
|
|
217
|
+
expect(nodes).toEqual([]);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it("should return all nodes when replicas >= node count", () => {
|
|
221
|
+
const ring = new HashRing<string>(["node1", "node2", "node3"]);
|
|
222
|
+
const nodes = ring.getNodes("test-key", 5);
|
|
223
|
+
expect(nodes.length).toBe(3);
|
|
224
|
+
expect(nodes).toContain("node1");
|
|
225
|
+
expect(nodes).toContain("node2");
|
|
226
|
+
expect(nodes).toContain("node3");
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it("should return all nodes when replicas equals node count", () => {
|
|
230
|
+
const ring = new HashRing<string>(["node1", "node2", "node3"]);
|
|
231
|
+
const nodes = ring.getNodes("test-key", 3);
|
|
232
|
+
expect(nodes.length).toBe(3);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it("should return requested number of unique nodes", () => {
|
|
236
|
+
const ring = new HashRing<string>(["node1", "node2", "node3", "node4"]);
|
|
237
|
+
const nodes = ring.getNodes("test-key", 2);
|
|
238
|
+
expect(nodes.length).toBe(2);
|
|
239
|
+
expect(new Set(nodes).size).toBe(2); // All unique
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it("should return consistent replicas for same key", () => {
|
|
243
|
+
const ring = new HashRing<string>(["node1", "node2", "node3", "node4"]);
|
|
244
|
+
const nodes1 = ring.getNodes("test-key", 3);
|
|
245
|
+
const nodes2 = ring.getNodes("test-key", 3);
|
|
246
|
+
expect(nodes1).toEqual(nodes2);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it("should return nodes in ring order", () => {
|
|
250
|
+
const ring = new HashRing<string>(["node1", "node2", "node3", "node4"]);
|
|
251
|
+
const nodes = ring.getNodes("test-key", 3);
|
|
252
|
+
expect(nodes.length).toBe(3);
|
|
253
|
+
// All nodes should be unique
|
|
254
|
+
const uniqueNodes = new Set(nodes);
|
|
255
|
+
expect(uniqueNodes.size).toBe(3);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it("should handle single node ring", () => {
|
|
259
|
+
const ring = new HashRing<string>(["node1"]);
|
|
260
|
+
const nodes = ring.getNodes("test-key", 3);
|
|
261
|
+
expect(nodes).toEqual(["node1"]);
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
describe("KetamaHash", () => {
|
|
267
|
+
describe("constructor", () => {
|
|
268
|
+
it("should create instance with default hash function", () => {
|
|
269
|
+
const distribution = new KetamaHash();
|
|
270
|
+
expect(distribution.name).toBe("ketama");
|
|
271
|
+
expect(distribution.nodes).toEqual([]);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it("should create instance with custom hash algorithm", () => {
|
|
275
|
+
const distribution = new KetamaHash("md5");
|
|
276
|
+
expect(distribution.name).toBe("ketama");
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it("should create instance with custom hash function", () => {
|
|
280
|
+
const customHash = (buf: Buffer) => buf.readInt32BE();
|
|
281
|
+
const distribution = new KetamaHash(customHash);
|
|
282
|
+
expect(distribution.name).toBe("ketama");
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
describe("nodes getter", () => {
|
|
287
|
+
it("should return empty array when no nodes added", () => {
|
|
288
|
+
const distribution = new KetamaHash();
|
|
289
|
+
expect(distribution.nodes).toEqual([]);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it("should return all added nodes", () => {
|
|
293
|
+
const distribution = new KetamaHash();
|
|
294
|
+
const node1 = new MemcacheNode("localhost", 11211);
|
|
295
|
+
const node2 = new MemcacheNode("localhost", 11212);
|
|
296
|
+
|
|
297
|
+
distribution.addNode(node1);
|
|
298
|
+
distribution.addNode(node2);
|
|
299
|
+
|
|
300
|
+
const nodes = distribution.nodes;
|
|
301
|
+
expect(nodes.length).toBe(2);
|
|
302
|
+
expect(nodes).toContain(node1);
|
|
303
|
+
expect(nodes).toContain(node2);
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
describe("addNode", () => {
|
|
308
|
+
it("should add node to distribution", () => {
|
|
309
|
+
const distribution = new KetamaHash();
|
|
310
|
+
const node = new MemcacheNode("localhost", 11211);
|
|
311
|
+
|
|
312
|
+
distribution.addNode(node);
|
|
313
|
+
expect(distribution.nodes.length).toBe(1);
|
|
314
|
+
expect(distribution.nodes[0]).toBe(node);
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it("should add node with custom weight", () => {
|
|
318
|
+
const distribution = new KetamaHash();
|
|
319
|
+
const node = new MemcacheNode("localhost", 11211, { weight: 3 });
|
|
320
|
+
|
|
321
|
+
distribution.addNode(node);
|
|
322
|
+
expect(distribution.nodes.length).toBe(1);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it("should handle multiple nodes", () => {
|
|
326
|
+
const distribution = new KetamaHash();
|
|
327
|
+
const node1 = new MemcacheNode("server1", 11211);
|
|
328
|
+
const node2 = new MemcacheNode("server2", 11211);
|
|
329
|
+
|
|
330
|
+
distribution.addNode(node1);
|
|
331
|
+
distribution.addNode(node2);
|
|
332
|
+
|
|
333
|
+
expect(distribution.nodes.length).toBe(2);
|
|
334
|
+
});
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
describe("removeNode", () => {
|
|
338
|
+
it("should remove node by ID", () => {
|
|
339
|
+
const distribution = new KetamaHash();
|
|
340
|
+
const node = new MemcacheNode("localhost", 11211);
|
|
341
|
+
|
|
342
|
+
distribution.addNode(node);
|
|
343
|
+
expect(distribution.nodes.length).toBe(1);
|
|
344
|
+
|
|
345
|
+
distribution.removeNode(node.id);
|
|
346
|
+
expect(distribution.nodes.length).toBe(0);
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
it("should be no-op when removing non-existent node", () => {
|
|
350
|
+
const distribution = new KetamaHash();
|
|
351
|
+
const node = new MemcacheNode("localhost", 11211);
|
|
352
|
+
|
|
353
|
+
distribution.addNode(node);
|
|
354
|
+
distribution.removeNode("nonexistent:11211");
|
|
355
|
+
expect(distribution.nodes.length).toBe(1);
|
|
356
|
+
});
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
describe("getNode", () => {
|
|
360
|
+
it("should get node by ID", () => {
|
|
361
|
+
const distribution = new KetamaHash();
|
|
362
|
+
const node = new MemcacheNode("localhost", 11211);
|
|
363
|
+
|
|
364
|
+
distribution.addNode(node);
|
|
365
|
+
const retrieved = distribution.getNode("localhost:11211");
|
|
366
|
+
|
|
367
|
+
expect(retrieved).toBe(node);
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
it("should return undefined for non-existent node", () => {
|
|
371
|
+
const distribution = new KetamaHash();
|
|
372
|
+
const retrieved = distribution.getNode("nonexistent:11211");
|
|
373
|
+
|
|
374
|
+
expect(retrieved).toBeUndefined();
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
it("should distinguish between different node IDs", () => {
|
|
378
|
+
const distribution = new KetamaHash();
|
|
379
|
+
const node1 = new MemcacheNode("server1", 11211);
|
|
380
|
+
const node2 = new MemcacheNode("server2", 11211);
|
|
381
|
+
|
|
382
|
+
distribution.addNode(node1);
|
|
383
|
+
distribution.addNode(node2);
|
|
384
|
+
|
|
385
|
+
expect(distribution.getNode("server1:11211")).toBe(node1);
|
|
386
|
+
expect(distribution.getNode("server2:11211")).toBe(node2);
|
|
387
|
+
});
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
describe("getNodesByKey", () => {
|
|
391
|
+
it("should return node for given key", () => {
|
|
392
|
+
const distribution = new KetamaHash();
|
|
393
|
+
const node1 = new MemcacheNode("localhost", 11211);
|
|
394
|
+
const node2 = new MemcacheNode("localhost", 11212);
|
|
395
|
+
|
|
396
|
+
distribution.addNode(node1);
|
|
397
|
+
distribution.addNode(node2);
|
|
398
|
+
|
|
399
|
+
const nodes = distribution.getNodesByKey("test-key");
|
|
400
|
+
expect(nodes.length).toBe(1);
|
|
401
|
+
expect([node1, node2]).toContain(nodes[0]);
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
it("should return consistent node for same key", () => {
|
|
405
|
+
const distribution = new KetamaHash();
|
|
406
|
+
const node1 = new MemcacheNode("localhost", 11211);
|
|
407
|
+
const node2 = new MemcacheNode("localhost", 11212);
|
|
408
|
+
|
|
409
|
+
distribution.addNode(node1);
|
|
410
|
+
distribution.addNode(node2);
|
|
411
|
+
|
|
412
|
+
const nodes1 = distribution.getNodesByKey("test-key");
|
|
413
|
+
const nodes2 = distribution.getNodesByKey("test-key");
|
|
414
|
+
expect(nodes1[0]).toBe(nodes2[0]);
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
it("should return empty array when no nodes available", () => {
|
|
418
|
+
const distribution = new KetamaHash();
|
|
419
|
+
const nodes = distribution.getNodesByKey("test-key");
|
|
420
|
+
expect(nodes).toEqual([]);
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
it("should distribute keys across nodes", () => {
|
|
424
|
+
const distribution = new KetamaHash();
|
|
425
|
+
const node1 = new MemcacheNode("server1", 11211);
|
|
426
|
+
const node2 = new MemcacheNode("server2", 11211);
|
|
427
|
+
const node3 = new MemcacheNode("server3", 11211);
|
|
428
|
+
|
|
429
|
+
distribution.addNode(node1);
|
|
430
|
+
distribution.addNode(node2);
|
|
431
|
+
distribution.addNode(node3);
|
|
432
|
+
|
|
433
|
+
const distributionMap = new Map<string, number>();
|
|
434
|
+
|
|
435
|
+
// Test with many keys
|
|
436
|
+
for (let i = 0; i < 300; i++) {
|
|
437
|
+
const nodes = distribution.getNodesByKey(`key-${i}`);
|
|
438
|
+
if (nodes.length > 0) {
|
|
439
|
+
const nodeId = nodes[0].id;
|
|
440
|
+
distributionMap.set(nodeId, (distributionMap.get(nodeId) || 0) + 1);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// All nodes should receive some keys
|
|
445
|
+
expect(distributionMap.size).toBe(3);
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
it("should handle weighted nodes", () => {
|
|
449
|
+
const distribution = new KetamaHash();
|
|
450
|
+
const heavyNode = new MemcacheNode("heavy", 11211, { weight: 3 });
|
|
451
|
+
const lightNode = new MemcacheNode("light", 11211, { weight: 1 });
|
|
452
|
+
|
|
453
|
+
distribution.addNode(heavyNode);
|
|
454
|
+
distribution.addNode(lightNode);
|
|
455
|
+
|
|
456
|
+
const distributionMap = new Map<string, number>();
|
|
457
|
+
|
|
458
|
+
// Test with many keys
|
|
459
|
+
for (let i = 0; i < 400; i++) {
|
|
460
|
+
const nodes = distribution.getNodesByKey(`key-${i}`);
|
|
461
|
+
if (nodes.length > 0) {
|
|
462
|
+
const nodeId = nodes[0].id;
|
|
463
|
+
distributionMap.set(nodeId, (distributionMap.get(nodeId) || 0) + 1);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const heavyCount = distributionMap.get("heavy:11211") || 0;
|
|
468
|
+
const lightCount = distributionMap.get("light:11211") || 0;
|
|
469
|
+
|
|
470
|
+
// Heavy node should handle more keys than light node
|
|
471
|
+
expect(heavyCount).toBeGreaterThan(lightCount);
|
|
472
|
+
});
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
describe("integration", () => {
|
|
476
|
+
it("should handle add, get, and remove operations", () => {
|
|
477
|
+
const distribution = new KetamaHash();
|
|
478
|
+
const node1 = new MemcacheNode("server1", 11211);
|
|
479
|
+
const node2 = new MemcacheNode("server2", 11211);
|
|
480
|
+
|
|
481
|
+
// Add nodes
|
|
482
|
+
distribution.addNode(node1);
|
|
483
|
+
distribution.addNode(node2);
|
|
484
|
+
expect(distribution.nodes.length).toBe(2);
|
|
485
|
+
|
|
486
|
+
// Get by key
|
|
487
|
+
const nodeForKey = distribution.getNodesByKey("my-key");
|
|
488
|
+
expect(nodeForKey.length).toBe(1);
|
|
489
|
+
|
|
490
|
+
// Get by ID
|
|
491
|
+
const retrievedNode = distribution.getNode("server1:11211");
|
|
492
|
+
expect(retrievedNode).toBe(node1);
|
|
493
|
+
|
|
494
|
+
// Remove node
|
|
495
|
+
distribution.removeNode("server1:11211");
|
|
496
|
+
expect(distribution.nodes.length).toBe(1);
|
|
497
|
+
expect(distribution.getNode("server1:11211")).toBeUndefined();
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
it("should redistribute keys when nodes are removed", () => {
|
|
501
|
+
const distribution = new KetamaHash();
|
|
502
|
+
const node1 = new MemcacheNode("server1", 11211);
|
|
503
|
+
const node2 = new MemcacheNode("server2", 11211);
|
|
504
|
+
|
|
505
|
+
distribution.addNode(node1);
|
|
506
|
+
distribution.addNode(node2);
|
|
507
|
+
|
|
508
|
+
const keysOnNode1BeforeRemoval = [];
|
|
509
|
+
for (let i = 0; i < 100; i++) {
|
|
510
|
+
const nodes = distribution.getNodesByKey(`key-${i}`);
|
|
511
|
+
if (nodes[0]?.id === "server1:11211") {
|
|
512
|
+
keysOnNode1BeforeRemoval.push(`key-${i}`);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// Remove node1
|
|
517
|
+
distribution.removeNode("server1:11211");
|
|
518
|
+
|
|
519
|
+
// All keys previously on node1 should now be on node2
|
|
520
|
+
for (const key of keysOnNode1BeforeRemoval) {
|
|
521
|
+
const nodes = distribution.getNodesByKey(key);
|
|
522
|
+
expect(nodes[0]?.id).toBe("server2:11211");
|
|
523
|
+
}
|
|
524
|
+
});
|
|
525
|
+
});
|
|
526
|
+
});
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import Memcache, { createNode } from "../src/index";
|
|
3
|
+
|
|
4
|
+
describe("MemcacheNode Instances Support", () => {
|
|
5
|
+
describe("addNode with MemcacheNode instances", () => {
|
|
6
|
+
it("should accept MemcacheNode instances via addNode", async () => {
|
|
7
|
+
const client = new Memcache({ timeout: 5000 });
|
|
8
|
+
|
|
9
|
+
// Create a MemcacheNode instance using createNode
|
|
10
|
+
const node1 = createNode("localhost", 11212, { weight: 2 });
|
|
11
|
+
const node2 = createNode("localhost", 11213, { weight: 3 });
|
|
12
|
+
|
|
13
|
+
// Add the node instances directly
|
|
14
|
+
await client.addNode(node1);
|
|
15
|
+
await client.addNode(node2);
|
|
16
|
+
|
|
17
|
+
// Verify nodes were added
|
|
18
|
+
expect(client.nodeIds).toHaveLength(3); // 2 + default
|
|
19
|
+
expect(client.nodeIds).toContain("localhost:11212");
|
|
20
|
+
expect(client.nodeIds).toContain("localhost:11213");
|
|
21
|
+
|
|
22
|
+
// Verify the nodes retain their properties
|
|
23
|
+
const addedNode1 = client.getNode("localhost:11212");
|
|
24
|
+
expect(addedNode1).toBeDefined();
|
|
25
|
+
expect(addedNode1?.weight).toBe(2);
|
|
26
|
+
|
|
27
|
+
const addedNode2 = client.getNode("localhost:11213");
|
|
28
|
+
expect(addedNode2).toBeDefined();
|
|
29
|
+
expect(addedNode2?.weight).toBe(3);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("should throw error when adding duplicate MemcacheNode instance", async () => {
|
|
33
|
+
const client = new Memcache({ timeout: 5000 });
|
|
34
|
+
|
|
35
|
+
// Create a node instance
|
|
36
|
+
const node = createNode("localhost", 11212);
|
|
37
|
+
|
|
38
|
+
// Add it once
|
|
39
|
+
await client.addNode(node);
|
|
40
|
+
expect(client.nodeIds).toContain("localhost:11212");
|
|
41
|
+
|
|
42
|
+
// Try to add it again - should throw error
|
|
43
|
+
await expect(client.addNode(node)).rejects.toThrow(
|
|
44
|
+
"Node localhost:11212 already exists",
|
|
45
|
+
);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe("Constructor with MemcacheNode instances", () => {
|
|
50
|
+
it("should initialize with MemcacheNode instances in options", () => {
|
|
51
|
+
const node1 = createNode("server1", 11211, { weight: 2 });
|
|
52
|
+
const node2 = createNode("server2", 11211, { weight: 3 });
|
|
53
|
+
|
|
54
|
+
const testClient = new Memcache({
|
|
55
|
+
nodes: [node1, node2],
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
expect(testClient.nodeIds).toHaveLength(2);
|
|
59
|
+
expect(testClient.nodeIds).toContain("server1:11211");
|
|
60
|
+
expect(testClient.nodeIds).toContain("server2:11211");
|
|
61
|
+
|
|
62
|
+
// Verify nodes retain their properties
|
|
63
|
+
const addedNode1 = testClient.getNode("server1:11211");
|
|
64
|
+
expect(addedNode1?.weight).toBe(2);
|
|
65
|
+
|
|
66
|
+
const addedNode2 = testClient.getNode("server2:11211");
|
|
67
|
+
expect(addedNode2?.weight).toBe(3);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("should initialize with mixed string URIs and MemcacheNode instances", () => {
|
|
71
|
+
const node1 = createNode("server1", 11211, { weight: 5 });
|
|
72
|
+
|
|
73
|
+
const testClient = new Memcache({
|
|
74
|
+
nodes: ["localhost:11211", node1, "server2:11212"],
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
expect(testClient.nodeIds).toHaveLength(3);
|
|
78
|
+
expect(testClient.nodeIds).toContain("localhost:11211");
|
|
79
|
+
expect(testClient.nodeIds).toContain("server1:11211");
|
|
80
|
+
expect(testClient.nodeIds).toContain("server2:11212");
|
|
81
|
+
|
|
82
|
+
// Verify the MemcacheNode instance retained its weight
|
|
83
|
+
const addedNode = testClient.getNode("server1:11211");
|
|
84
|
+
expect(addedNode?.weight).toBe(5);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("should work with only MemcacheNode instances (no string URIs)", () => {
|
|
88
|
+
const node1 = createNode("host1", 11211);
|
|
89
|
+
const node2 = createNode("host2", 11212);
|
|
90
|
+
const node3 = createNode("host3", 11213);
|
|
91
|
+
|
|
92
|
+
const testClient = new Memcache({
|
|
93
|
+
nodes: [node1, node2, node3],
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
expect(testClient.nodeIds).toHaveLength(3);
|
|
97
|
+
expect(testClient.nodeIds).toContain("host1:11211");
|
|
98
|
+
expect(testClient.nodeIds).toContain("host2:11212");
|
|
99
|
+
expect(testClient.nodeIds).toContain("host3:11213");
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
});
|