UNPKG

@qiniu/miku-delivery-mp-ks

Version:

Kuaishou Mini Program SDK for Miku Delivery

242 lines (241 loc) 9.3 kB
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); } }