@qiniu/miku-delivery-mp-ks
Version:
Kuaishou Mini Program SDK for Miku Delivery
242 lines (241 loc) • 9.3 kB
JavaScript
import { randomText } from '../test';
import ConsistentHash from './consistent-hash';
describe('ConsistentHash', () => {
it('should new correctly', () => {
const ch = new ConsistentHash();
});
it('should add correctly', () => {
const ch = new ConsistentHash();
const added = ch.add('abcdefg', 20);
expect(added).toBe(true);
const added2 = ch.add('abcdefg', 20);
expect(added2).toBe(false);
const added3 = ch.add('abcdefg', 30);
expect(added3).toBe(false);
});
it('should remove correctly', () => {
const ch = new ConsistentHash();
const removed = ch.remove('abcdefg');
expect(removed).toBe(false);
ch.add('abcdefg', 20);
const removed2 = ch.remove('abcdefg');
expect(removed2).toBe(true);
});
describe('get', () => {
it('should work correctly with empty members', () => {
const ch = new ConsistentHash();
expect(ch.get('asdfsadfsadf')).toBe(null);
});
it('should work correctly with single member', () => {
const ch = new ConsistentHash();
ch.add('abcdefg', 20);
withRandomInput(input => {
expect(ch.get(input)).toBe('abcdefg');
});
});
it('should work correctly with multiple member', () => {
const ch = new ConsistentHash();
const cases = [
{ in: 'ggg', out: 'hijklmn' },
{ in: 'hhh', out: 'abcdefg' },
{ in: 'iii', out: 'opqrstu' }
];
for (const c of cases) {
ch.add(c.out, 20);
}
for (const c of cases) {
expect(ch.get(c.in)).toBe(c.out);
}
});
it('should work correctly with random input', () => {
const ch = new ConsistentHash();
const members = ['abcdefg', 'hijklmn', 'opqrstu'];
for (const m of members) {
ch.add(m, 20);
}
withRandomInput(input => {
expect(members).toContain(ch.get(input));
});
});
it('should work correctly with remove', () => {
const ch = new ConsistentHash();
const casesBefore = [
{ in: 'ggg', out: 'hijklmn' },
{ in: 'hhh', out: 'abcdefg' },
{ in: 'iii', out: 'opqrstu' }
];
const casesAfter = [
{ in: 'ggg', out: 'opqrstu' },
{ in: 'hhh', out: 'abcdefg' },
{ in: 'iii', out: 'opqrstu' }
];
for (const c of casesBefore) {
ch.add(c.out, 20);
}
ch.remove('hijklmn');
for (const c of casesAfter) {
expect(ch.get(c.in)).toBe(c.out);
}
});
it('should work correctly with remove & random input', () => {
const ch = new ConsistentHash();
const members = ['abcdefg', 'hijklmn', 'opqrstu'];
for (const m of members) {
ch.add(m, 20);
}
const [toRemove] = members.splice(1, 1);
ch.remove(toRemove);
withRandomInput(input => {
expect(members).toContain(ch.get(input));
});
});
});
describe('getN', () => {
it('should work correctly with empty members', () => {
const ch = new ConsistentHash();
expect(ch.getN('asdfsadfsadf', 1)).toEqual([]);
expect(ch.getN('asdfsadfsadf', 2)).toEqual([]);
});
it('should work correctly with single member', () => {
const ch = new ConsistentHash();
ch.add('abcdefg', 20);
withRandomInput(input => {
expect(ch.getN(input, 1)).toEqual(['abcdefg']);
expect(ch.getN(input, 2)).toEqual(['abcdefg']);
});
});
it('should work correctly with multiple member', () => {
const ch = new ConsistentHash();
const cases = [
{ in: 'ggg', out: 'hijklmn' },
{ in: 'hhh', out: 'abcdefg' },
{ in: 'iii', out: 'opqrstu' }
];
for (const c of cases) {
ch.add(c.out, 20);
}
for (const c of cases) {
expect(ch.getN(c.in, 1)).toEqual([c.out]);
const [el1, el2] = ch.getN(c.in, 2);
expect(el1).toBe(c.out);
expect(el2).not.toBe(el1);
const allEls = ch.getN(c.in, 3);
expect(allEls[0]).toBe(c.out);
expect(allEls.sort()).toEqual(['abcdefg', 'hijklmn', 'opqrstu']);
}
});
it('should work correctly with availableMembers', () => {
const ch = new ConsistentHash();
ch.add('hijklmn', 20);
ch.add('abcdefg', 20);
ch.add('opqrstu', 20);
const availableMembers = new Set(['hijklmn', 'opqrstu']);
withRandomInput(input => {
const elts = ch.getN(input, 2, availableMembers);
expect(elts.sort()).toEqual(['hijklmn', 'opqrstu']);
});
withRandomInput(input => {
expect(ch.getN(input, 1, availableMembers)).not.toBe(['abcdefg']);
});
});
});
describe('set', () => {
it('should work well', () => {
const ch = new ConsistentHash();
ch.add('abc', 20);
ch.add('def', 40);
ch.add('ghi', 80);
const members = [
{ key: 'abc', replicas: 20 },
{ key: 'jkl', replicas: 40 },
{ key: 'mno', replicas: 60 }
];
ch.set(members);
withRandomInput(input => {
expect(['abc', 'jkl', 'mno']).toContain(ch.get(input));
});
});
});
describe('replicas', () => {
// 注意这里 replicas 值尽量设置大一点的数字,这样统计误差会小一点
it('should work well with add', () => {
const ch = new ConsistentHash();
const members = [
{ key: 'abcdefg', replicas: 2000 },
{ key: 'hijklmn', replicas: 4000 },
{ key: 'opqrstu', replicas: 6000 }
];
for (const m of members) {
ch.add(m.key, m.replicas);
}
checkReplicas(ch, members);
});
it('should work well with set', () => {
const ch = new ConsistentHash();
const members = [
{ key: 'abcdefg', replicas: 2000 },
{ key: 'hijklmn', replicas: 4000 },
{ key: 'opqrstu', replicas: 6000 }
];
ch.set(members);
checkReplicas(ch, members);
});
it('should work well with add & set', () => {
const ch = new ConsistentHash();
ch.add('abc', 2000);
ch.add('def', 4000);
ch.add('ghi', 8000);
const members = [
{ key: 'abc', replicas: 2000 },
{ key: 'jkl', replicas: 4000 },
{ key: 'mno', replicas: 6000 }
];
ch.set(members);
checkReplicas(ch, members);
});
it('should work well with remove', () => {
const ch = new ConsistentHash();
const members = [
{ key: 'abcdefg', replicas: 2000 },
{ key: 'hijklmn', replicas: 4000 },
{ key: 'opqrstu', replicas: 6000 }
];
ch.set(members.slice(0, 2));
ch.add(members[2].key, members[2].replicas);
const [toRemove] = members.splice(1, 1);
ch.remove(toRemove.key);
expect(members).toHaveLength(2);
checkReplicas(ch, members);
});
});
});
function checkReplicas(ch, members) {
const memberKeys = members.map(m => m.key);
const nums = members.map(() => 0);
withRandomInput(input => {
const got = ch.get(input);
const idx = memberKeys.indexOf(got);
if (idx < 0)
throw new Error(`Unexpected result: ${got}`);
nums[idx]++;
}, 1000);
const replicasSum = members.reduce((sum, m) => sum + m.replicas, 0);
const numsSum = nums.reduce((sum, n) => sum + n, 0);
members.forEach((m, i) => {
const expectedRatio = m.replicas / replicasSum;
const receivedRatio = nums[i] / numsSum;
const similarity = receivedRatio / expectedRatio;
if (similarity < 0.85 || similarity > 1.15) {
// 允许一定的统计学误差:不超过 +/- 15%
// 如果还是运气不好挂了,重新跑就行;重新跑都不好,那么大概率有问题
throw new Error(`Unexpected similarity of ${m.key}: expect ${expectedRatio}, got ${receivedRatio}, similarity ${similarity}`);
}
});
}
function withRandomInput(fn, times = 1000, range = 1000) {
for (let i = 0; i < times; i++) {
const inputLength = Math.floor(Math.random() * range);
const input = randomText(inputLength);
fn(input);
}
}