1 |
|
2 |
|
3 | var
|
4 | demand = require('must'),
|
5 | Lightcycle = require('../index')
|
6 | ;
|
7 |
|
8 | function MockResource(name)
|
9 | {
|
10 | this._name = name;
|
11 | }
|
12 |
|
13 | MockResource.prototype.name = function name()
|
14 | {
|
15 | return this._name;
|
16 | };
|
17 |
|
18 | function makeFruitCycle()
|
19 | {
|
20 | var cycle = new Lightcycle({ size: 10, replicas: 3 });
|
21 | var r1 = new MockResource('kiwi');
|
22 | var r2 = new MockResource('papaya');
|
23 | var r3 = new MockResource('litchi');
|
24 |
|
25 | cycle.add(r1, r1.name());
|
26 | cycle.add(r2, r2.name());
|
27 | cycle.add(r3, r3.name());
|
28 |
|
29 | return cycle;
|
30 | }
|
31 |
|
32 | describe('light-cycle', () =>
|
33 | {
|
34 | describe('constructor', () =>
|
35 | {
|
36 | it('demands a positive integer size setting', () =>
|
37 | {
|
38 | function mustThrow()
|
39 | {
|
40 | return new Lightcycle({ size: -3 });
|
41 | }
|
42 | mustThrow.must.throw(Error);
|
43 | });
|
44 |
|
45 | it('provides a default hash seed', () =>
|
46 | {
|
47 | var cycle = new Lightcycle({ });
|
48 | cycle.seed.must.equal(0xcafed00d);
|
49 | });
|
50 |
|
51 | it('respects the hash seed setting', () =>
|
52 | {
|
53 | var cycle = new Lightcycle({ seed: 0xdeadbeef });
|
54 | cycle.seed.must.equal(0xdeadbeef);
|
55 | });
|
56 |
|
57 | it('defaults size to 128', () =>
|
58 | {
|
59 | var cycle = new Lightcycle({ });
|
60 | cycle.size.must.equal(128);
|
61 | });
|
62 |
|
63 | it('defaults replica count to 128', () =>
|
64 | {
|
65 | var cycle = new Lightcycle({ });
|
66 | cycle.replicas.must.equal(128);
|
67 | });
|
68 |
|
69 | it('defaults replica count to size if size is passed in', () =>
|
70 | {
|
71 | var cycle = new Lightcycle({ size: 1024 });
|
72 | cycle.replicas.must.equal(1024);
|
73 | });
|
74 |
|
75 | it('obeys both size & replica settings if provided', () =>
|
76 | {
|
77 | var cycle = new Lightcycle({ size: 10, replicas: 3 });
|
78 | cycle.size.must.equal(10);
|
79 | cycle.replicas.must.equal(3);
|
80 | });
|
81 | });
|
82 |
|
83 | describe('add()', () =>
|
84 | {
|
85 | it('demands both resource and id parameters', () =>
|
86 | {
|
87 | var cycle = new Lightcycle({ size: 10, replicas: 3 });
|
88 | var resource = { name: 'nameless' };
|
89 |
|
90 | function mustThrow()
|
91 | {
|
92 | cycle.add(resource);
|
93 | }
|
94 |
|
95 | mustThrow.must.throw(Error);
|
96 | });
|
97 |
|
98 | it('adds a resource to the cycle', () =>
|
99 | {
|
100 | var cycle = new Lightcycle({ size: 10, replicas: 3 });
|
101 | var resource = new MockResource('kiwi');
|
102 |
|
103 | cycle.add(resource, resource.name());
|
104 |
|
105 | var key1 = cycle.hashit(resource.name() + '0');
|
106 | var item = cycle.resources.match(key1);
|
107 |
|
108 | item.must.exist();
|
109 | item.must.equal(resource);
|
110 | });
|
111 |
|
112 | it('adds `replicas` count replicas to the cycle', () =>
|
113 | {
|
114 | var cycle = new Lightcycle({ size: 10, replicas: 3 });
|
115 | var resource = new MockResource('kiwi');
|
116 |
|
117 | cycle.add(resource, resource.name());
|
118 |
|
119 | var allEntries = cycle.resources.find();
|
120 | allEntries.length.must.equal(3);
|
121 | });
|
122 |
|
123 | it('adding twice has no ill effect', () =>
|
124 | {
|
125 | var cycle = new Lightcycle({ size: 10, replicas: 3 });
|
126 | var resource = new MockResource('kiwi');
|
127 |
|
128 | cycle.add(resource, resource.name());
|
129 | cycle.add(resource, resource.name());
|
130 | var allEntries = cycle.resources.find();
|
131 | allEntries.length.must.equal(3);
|
132 | });
|
133 | });
|
134 |
|
135 | describe('all()', () =>
|
136 | {
|
137 | it('returns a hash of the resources & ids', () =>
|
138 | {
|
139 | var cycle = makeFruitCycle();
|
140 | var entries = cycle.all();
|
141 |
|
142 | entries.must.be.an.object();
|
143 | Object.keys(entries).length.must.equal(3);
|
144 | entries.kiwi.must.exist();
|
145 | entries.papaya.must.exist();
|
146 | entries.litchi.must.exist();
|
147 | });
|
148 | });
|
149 |
|
150 | describe('remove()', () =>
|
151 | {
|
152 | it('removes all replicas from the cycle', () =>
|
153 | {
|
154 | var cycle = new Lightcycle({ size: 10, replicas: 3 });
|
155 | var r1 = new MockResource('kiwi');
|
156 | var r2 = new MockResource('papaya');
|
157 |
|
158 | cycle.add(r1, r1.name());
|
159 | cycle.add(r2, r2.name());
|
160 |
|
161 | var allItems = cycle.resources.find();
|
162 | allItems.length.must.equal(6);
|
163 |
|
164 | cycle.remove(r1.name());
|
165 |
|
166 | var afterItems = cycle.resources.find();
|
167 | afterItems.length.must.equal(3);
|
168 |
|
169 | var key1 = cycle.hashit(r1.name() + '0');
|
170 |
|
171 | var found = cycle.resources.match(key1);
|
172 | demand(found).be.null();
|
173 | });
|
174 |
|
175 | it('silently ignores items that are not in the cycle', () =>
|
176 | {
|
177 | var cycle = new Lightcycle({ size: 10, replicas: 3 });
|
178 | var r1 = new MockResource('kiwi');
|
179 | var r2 = new MockResource('papaya');
|
180 | var r3 = new MockResource('litchi');
|
181 |
|
182 | cycle.add(r1, r1.name());
|
183 | cycle.add(r2, r2.name());
|
184 |
|
185 | cycle.remove(r3.name());
|
186 | });
|
187 | });
|
188 |
|
189 | describe('locate()', () =>
|
190 | {
|
191 | it('returns a single resource for a given id', () =>
|
192 | {
|
193 | var cycle = makeFruitCycle();
|
194 | var loc = cycle.locate('pomegranate');
|
195 | loc.must.exist();
|
196 | });
|
197 |
|
198 | it('handles the case of resources at the end of the circle by returning the first resource', () =>
|
199 | {
|
200 | var cycle = new Lightcycle();
|
201 |
|
202 | var r1 = new MockResource('durian');
|
203 | var r2 = new MockResource('gooseberry');
|
204 |
|
205 | cycle.resources.insert('a', r1);
|
206 | cycle.resources.insert('b', r2);
|
207 |
|
208 | var key = cycle.hashit('pomegranate');
|
209 | key.must.be.a.string();
|
210 | var loc = cycle.locate('pomegranate');
|
211 | loc.must.equal(r1);
|
212 | });
|
213 |
|
214 | it('handles ids that are buffers', () =>
|
215 | {
|
216 | var cycle = makeFruitCycle();
|
217 | var loc = cycle.locate(new Buffer('mangosteen'));
|
218 | loc.must.exist();
|
219 | });
|
220 |
|
221 | it('returns null when asked to locate an id when no resources are in the cycle', () =>
|
222 | {
|
223 | var cycle = new Lightcycle();
|
224 | var location = cycle.locate('kumquat');
|
225 | demand(location).be.null();
|
226 | });
|
227 |
|
228 | it('gives the correct new location for items that used to live on the removed resource', () =>
|
229 | {
|
230 | var cycle = makeFruitCycle();
|
231 |
|
232 | var originalLoc = cycle.locate('pomegranate');
|
233 | cycle.remove(originalLoc.name());
|
234 |
|
235 | var newLoc = cycle.locate('pomegranate');
|
236 | newLoc.must.be.truthy();
|
237 | newLoc.name().must.not.equal(originalLoc.name());
|
238 | });
|
239 | });
|
240 |
|
241 | describe('rebalance', () =>
|
242 | {
|
243 | it('is triggered when adding makes the number of entries greater than the size', () =>
|
244 | {
|
245 | var cycle = new Lightcycle({ size: 2, replicas: 2 });
|
246 | var r1 = new MockResource('durian');
|
247 | var r2 = new MockResource('gooseberry');
|
248 | var r3 = new MockResource('kumquat');
|
249 |
|
250 | cycle.add(r1, r1.name());
|
251 | cycle.add(r2, r2.name());
|
252 | cycle.add(r3, r3.name());
|
253 |
|
254 | cycle.size.must.equal(3 + Lightcycle.SIZE_PAD,
|
255 | 'expected size to be ' + Lightcycle.SIZE_PAD + ' + the number of entries');
|
256 | cycle.replicas.must.equal(3 + Lightcycle.REPLICAS_PAD,
|
257 | 'expected replica count to be ' + Lightcycle.REPLICAS_PAD + ' + the number of entries');
|
258 | });
|
259 | });
|
260 | });
|