├── contributing.md ├── example ├── rslice.js ├── progression.js ├── hash-table.js ├── evolve.js └── hash.js ├── test ├── lib │ ├── show.js │ ├── round-slices.js │ ├── slices-overlap.js │ ├── move.js │ └── valid.js ├── parse.js ├── multi.js ├── grow.js ├── shrink.js └── move.js ├── package.json ├── LICENSE-PARITY ├── readme.md ├── index.js └── LICENSE-APACHE /contributing.md: -------------------------------------------------------------------------------- 1 | By contributing, you agree to release your modifications under the Apache 2.0 2 | license (see the file LICENSE-APACHE). 3 | -------------------------------------------------------------------------------- /example/rslice.js: -------------------------------------------------------------------------------- 1 | var RS = require('../') 2 | var rs = new RS 3 | rs.set({ A: 40, B: 120 }) 4 | rs.set({ A: 32, C: 80 }) 5 | 6 | Object.entries(rs.getBins()).forEach(function ([key,bin]) { 7 | console.log(key, bin.size, bin.slices.map(showSlice).join(', ')) 8 | }) 9 | 10 | function showSlice ([start,end]) { 11 | return `${start[0]}/${start[1]}..${end[0]}/${end[1]}` 12 | } 13 | -------------------------------------------------------------------------------- /test/lib/show.js: -------------------------------------------------------------------------------- 1 | module.exports = function (rs) { 2 | Object.keys(rs._bins).forEach(function (key) { 3 | var b = rs._bins[key] 4 | var n = 10000 5 | console.log(key,b.size,JSON.stringify(b.slices.map(function (slice) { 6 | var start = slice[0].toNumber() 7 | var end = slice[1].toNumber() 8 | return [Math.round(start*n)/n,Math.round(end*n)/n] 9 | }))) 10 | }) 11 | } 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "random-slicing", 3 | "version": "2.0.2", 4 | "description": "resizable hashing strategy for large-scale storage", 5 | "license": "Parity-6.0.0 AND Apache-2.0", 6 | "keywords": [ 7 | "slicing", 8 | "hash", 9 | "hash ring", 10 | "partitioning" 11 | ], 12 | "dependencies": { 13 | "bigint-rational": "^1.0.0" 14 | }, 15 | "devDependencies": { 16 | "tape": "^4.11.0" 17 | }, 18 | "scripts": { 19 | "test": "tape test/*.js" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /test/lib/round-slices.js: -------------------------------------------------------------------------------- 1 | module.exports = function roundSlices (rs, n) { 2 | var nbins = {} 3 | var p = 10**n 4 | var bins = rs.getBins() 5 | Object.keys(bins).forEach(function (key) { 6 | nbins[key] = { 7 | size: bins[key].size, 8 | slices: bins[key].slices.map(([[xn,xd],[yn,yd]]) => { 9 | return [ 10 | Math.round(Number(xn)*p/Number(xd))/p, 11 | Math.round(Number(yn)*p/Number(yd))/p 12 | ] 13 | }) 14 | } 15 | }) 16 | return nbins 17 | } 18 | -------------------------------------------------------------------------------- /test/parse.js: -------------------------------------------------------------------------------- 1 | var test = require('tape') 2 | var RS = require('../') 3 | var calcMoves = require('./lib/move.js') 4 | var valid = require('./lib/valid.js') 5 | 6 | test('serialize / parse', function (t) { 7 | var rs = new RS 8 | rs.set({ A: 50, B: 20, C: 44 }) 9 | rs.set({ A: 65, C: 25, D: 15 }) 10 | 11 | var str = rs.serialize() 12 | t.equal(typeof str, 'string') 13 | 14 | var copy = RS.parse(str) 15 | t.ifError(valid(copy), 'valid after parsing') 16 | 17 | var moves = calcMoves(rs, copy) 18 | 19 | t.deepEqual(moves, [0n,1n], 'no moves') 20 | t.deepEqual(rs.getBins(), copy.getBins(), 'bins equal') 21 | t.end() 22 | }) 23 | -------------------------------------------------------------------------------- /example/progression.js: -------------------------------------------------------------------------------- 1 | var RS = require('../') 2 | var rs = new RS 3 | rs.set({ A: 20, B: 20, C: 20, D: 20 }) 4 | show(rs) 5 | rs.set({ A: 10 }) 6 | show(rs) 7 | rs.set({ B: 15 }) 8 | show(rs) 9 | rs.set({ C: 40 }) 10 | show(rs) 11 | rs.set({ B: 12 }) 12 | show(rs) 13 | rs.set({ D: 17 }) 14 | show(rs) 15 | rs.set({ A: 5 }) 16 | show(rs) 17 | rs.set({ B: 25 }) 18 | show(rs) 19 | 20 | function show (rs) { 21 | Object.entries(rs.getBins()).forEach(function ([key,b]) { 22 | var n = 10000 23 | console.log(key, b.size, b.slices.map(([start,end]) => { 24 | return `[${start[0]}/${start[1]}..${end[0]}/${end[1]}]` 25 | }).join(', ')) 26 | }) 27 | console.log('---') 28 | } 29 | -------------------------------------------------------------------------------- /test/lib/slices-overlap.js: -------------------------------------------------------------------------------- 1 | var rat = require('bigint-rational') 2 | var ZERO = rat.create(0,1) 3 | var tmp0 = rat.create(0,1) 4 | var tmp1 = rat.create(0,1) 5 | 6 | module.exports = function (a, b) { 7 | var overlap = rat.create(0,1) 8 | for (var i = 0; i < a.length; i++) { 9 | for (var j = 0; j < b.length; j++) { 10 | rat.multiply(tmp0, tmp0, ZERO) 11 | rat.add(tmp0,tmp0, a[i][1]) 12 | rat.subtract(tmp0, tmp0, b[j][0]) 13 | rat.multiply(tmp1, tmp1, ZERO) 14 | rat.add(tmp1, tmp1, b[j][1]) 15 | rat.subtract(tmp1, tmp1, a[i][0]) 16 | rat.add(overlap, overlap, max(ZERO, min(tmp0, tmp1))) 17 | } 18 | } 19 | rat.reduce(overlap, overlap) 20 | return overlap 21 | } 22 | 23 | function min (a, b) { return rat.lt(a,b) ? a : b } 24 | function max (a, b) { return rat.gt(a,b) ? a : b } 25 | -------------------------------------------------------------------------------- /test/lib/move.js: -------------------------------------------------------------------------------- 1 | var rat = require('bigint-rational') 2 | var calcOverlap = require('./slices-overlap.js') 3 | var tmp = [0n,1n] 4 | var ZERO = [0n,1n] 5 | 6 | module.exports = function (a, b) { 7 | var okeys = {} 8 | Object.keys(a._bins).forEach(key => { okeys[key] = true }) 9 | Object.keys(b._bins).forEach(key => { okeys[key] = true }) 10 | var keys = Object.keys(okeys) 11 | var overlap = rat.create(0,1) 12 | keys.forEach(function (key) { 13 | var delta = calcOverlap(a._bins[key].slices, b._bins[key].slices) 14 | rat.subtract(tmp, length(a._bins[key].slices), delta) 15 | if (rat.gt(tmp, ZERO)) rat.add(overlap, overlap, tmp) 16 | }) 17 | rat.reduce(overlap, overlap) 18 | return overlap 19 | } 20 | 21 | function length (slices) { 22 | var len = rat.create(0,1) 23 | for (var i = 0; i < slices.length; i++) { 24 | rat.add(len, len, slices[i][1]) 25 | rat.subtract(len, len, slices[i][0]) 26 | } 27 | return len 28 | } 29 | -------------------------------------------------------------------------------- /example/hash-table.js: -------------------------------------------------------------------------------- 1 | var { createHash } = require('crypto') 2 | 3 | var RS = require('../') 4 | var rs = new RS 5 | rs.set({ A: 40, B: 100 }) 6 | rs.set({ A: 32, C: 80 }) 7 | rs.set({ D: 50, B: 110 }) 8 | rs.set({ A: 20, B: 20, C: 20, D: 20 }) 9 | rs.set({ A: 10 }) 10 | rs.set({ B: 15 }) 11 | rs.set({ C: 40 }) 12 | rs.set({ B: 12 }) 13 | rs.set({ D: 17 }) 14 | rs.set({ A: 5 }) 15 | rs.set({ B: 25 }) 16 | 17 | // scale the slices up into 2**256 to get sha256 hash ranges 18 | var buckets = [] 19 | var max = 2n**256n-1n 20 | Object.entries(rs.getBins()).forEach(([key,bin]) => { 21 | bin.slices.forEach(([start,end]) => { 22 | buckets.push([ 23 | (start[0]*max/start[1]).toString(16).padStart(64,'0'), 24 | (end[0]*max/end[1]).toString(16).padStart(64,'0'), 25 | key 26 | ]) 27 | }) 28 | }) 29 | buckets.sort((a,b) => a[0] < b[0] ? -1 : +1) 30 | 31 | buckets.forEach(([start,end,key]) => { 32 | console.log(key, start) 33 | console.log(end.padStart(key.length+1+end.length)) 34 | console.log() 35 | }) 36 | -------------------------------------------------------------------------------- /test/multi.js: -------------------------------------------------------------------------------- 1 | var test = require('tape') 2 | var RSlice = require('../') 3 | var calcMoves = require('./lib/move.js') 4 | var valid = require('./lib/valid.js') 5 | var rat = require('bigint-rational') 6 | var ZERO = [0n,1n] 7 | 8 | test('multi', function (t) { 9 | var rs = new RSlice 10 | rs.set({ A: 20, B: 20, C: 20, D: 20 }) 11 | t.ifError(valid(rs)) 12 | var prev = new RSlice(rs.getBins()) 13 | rs.set({ A: 40, B: 40, C: 40, D: 40 }) 14 | t.ifError(valid(rs)) 15 | var moved = calcMoves(prev, rs) 16 | t.ok(rat.eq(moved,ZERO), 'no moves for scaled update') 17 | prev = new RSlice(rs.getBins()) 18 | rs.set({ A: 40, B: 40, C: 50, D: 55 }) 19 | t.ifError(valid(rs)) 20 | var moved = calcMoves(prev, rs) 21 | var expected = [0n,1n] 22 | var expected = rat.subtract([], 23 | [50n+55n,40n+40n+50n+55n], 24 | [40n+40n,40n+40n+40n+40n] 25 | ) 26 | rat.reduce(expected, expected) 27 | t.deepEqual(moved, expected, 'moves after multi-grow') 28 | t.ifError(valid(rs)) 29 | t.end() 30 | }) 31 | 32 | function round (x, n) { return Math.round(x*n)/n } 33 | -------------------------------------------------------------------------------- /example/evolve.js: -------------------------------------------------------------------------------- 1 | var RS = require('../') 2 | var valid = require('../test/lib/valid.js') 3 | 4 | var rs = new RS 5 | var sizes = { A: 20, B: 20, C: 20, D: 20, E: 20, F: 20, G: 20 } 6 | rs.set(sizes) 7 | var keys = Object.keys(sizes) 8 | var totalSize = 20*keys.length 9 | show(rs) 10 | 11 | for (var i = 0; i < 10000; i++) { 12 | var key = keys[Math.floor(Math.random()*keys.length)] 13 | var prevSize = sizes[key] 14 | var size = Math.max(0, prevSize + Math.floor((Math.random()*2-1)*8)) 15 | if (size === 0 && totalSize === prevSize) { 16 | size = 1 17 | } 18 | var op = {} 19 | op[key] = Math.max(0, size) 20 | rs.set(op) 21 | totalSize += size - prevSize 22 | sizes[key] = size 23 | var err = valid(rs) 24 | if (err) throw err 25 | show(rs) 26 | } 27 | 28 | function show (rs) { 29 | Object.entries(rs.getBins()).forEach(function ([key,b]) { 30 | var n = 10000 31 | console.log(key, b.size, b.slices.map(([start,end]) => { 32 | return `[${start[0]}/${start[1]}..${end[0]}/${end[1]}]` 33 | }).join(', ')) 34 | }) 35 | console.log('---', i) 36 | } 37 | -------------------------------------------------------------------------------- /example/hash.js: -------------------------------------------------------------------------------- 1 | var { createHash } = require('crypto') 2 | 3 | var RS = require('../') 4 | var rs = new RS 5 | rs.set({ A: 40, B: 100 }) 6 | rs.set({ A: 32, C: 80 }) 7 | rs.set({ D: 50, B: 110 }) 8 | 9 | // scale the slices up into 2**256 to get sha256 hash ranges 10 | var buckets = [] 11 | var max = 2n**256n-1n 12 | Object.entries(rs.getBins()).forEach(([key,bin]) => { 13 | bin.slices.forEach(([start,end]) => { 14 | buckets.push([ 15 | (start[0]*max/start[1]).toString(16).padStart(64,'0'), 16 | (end[0]*max/end[1]).toString(16).padStart(64,'0'), 17 | key 18 | ]) 19 | }) 20 | }) 21 | buckets.sort((a,b) => a[0] < b[0] ? -1 : +1) 22 | 23 | console.log(`meow => ${lookup('meow')}`) // D 24 | console.log(`kitty => ${lookup('kitty')}`) // B 25 | console.log(`oof => ${lookup('oof')}`) // B 26 | console.log(`gecko => ${lookup('gecko')}`) // D 27 | console.log(`toad => ${lookup('toad')}`) // A 28 | 29 | function lookup (key) { 30 | var h = createHash('sha256') 31 | h.write(key) 32 | var hash = h.digest('hex') 33 | for (var i = 0; i < buckets.length; i++) { 34 | if (hash < buckets[i][1]) return buckets[i][2] 35 | } 36 | return null 37 | } 38 | -------------------------------------------------------------------------------- /test/grow.js: -------------------------------------------------------------------------------- 1 | var test = require('tape') 2 | var RSlice = require('../') 3 | var roundSlices = require('./lib/round-slices.js') 4 | var valid = require('./lib/valid.js') 5 | 6 | test('uniformly grow then adjust', function (t) { 7 | var rs = new RSlice 8 | rs.set({ A: 20 }) 9 | t.deepEqual(roundSlices(rs,2), { 10 | A: { size: 20, slices: [[0,1]] } 11 | }) 12 | t.ifError(valid(rs)) 13 | rs.set({ B: 20 }) 14 | t.deepEqual(roundSlices(rs,2), { 15 | A: { size: 20, slices: [[0,0.5]] }, 16 | B: { size: 20, slices: [[0.5,1]] } 17 | }) 18 | t.ifError(valid(rs)) 19 | rs.set({ C: 20 }) 20 | t.deepEqual(roundSlices(rs,2), { 21 | A: { size: 20, slices: [[0,0.33]] }, 22 | B: { size: 20, slices: [[0.5,0.83]] }, 23 | C: { size: 20, slices: [[0.33,0.5],[0.83,1]] } 24 | }) 25 | t.ifError(valid(rs)) 26 | rs.set({ D: 20 }) 27 | t.deepEqual(roundSlices(rs,2), { 28 | A: { size: 20, slices: [[0,0.25]] }, 29 | B: { size: 20, slices: [[0.5,0.75]] }, 30 | C: { size: 20, slices: [[0.33,0.42],[0.83,1]] }, 31 | D: { size: 20, slices: [[0.25,0.33],[0.42,0.5],[0.75,0.83]] } 32 | }) 33 | t.ifError(valid(rs)) 34 | rs.set({ D: 30 }) 35 | t.deepEqual(roundSlices(rs,2), { 36 | A: { size: 20, slices: [[0,0.22]] }, 37 | B: { size: 20, slices: [[0.5,0.72]] }, 38 | C: { size: 20, slices: [[0.33,0.39],[0.83,1]] }, 39 | D: { size: 30, slices: [[0.22,0.33],[0.39,0.5],[0.72,0.83]] } 40 | }) 41 | t.ifError(valid(rs)) 42 | t.end() 43 | }) 44 | -------------------------------------------------------------------------------- /test/lib/valid.js: -------------------------------------------------------------------------------- 1 | var rat = require('bigint-rational') 2 | 3 | module.exports = function (rs) { 4 | var total = 0 5 | for (var i = 0; i < rs._binKeys.length; i++) { 6 | total += rs._bins[rs._binKeys[i]].size 7 | } 8 | var totalFloat = 0 9 | for (var i = 0; i < rs._binKeys.length; i++) { 10 | var b = rs._bins[rs._binKeys[i]] 11 | var ratio = 0 12 | for (var j = 0; j < b.slices.length; j++) { 13 | var iv = b.slices[j] 14 | if (rat.gte(iv[0],iv[1])) { 15 | return new Error(`invalid interval in 16 | ${rs._binKeys[i]}:${j} [${iv}]`) 17 | } 18 | ratio += Number(iv[1].toString()) - Number(iv[0].toString()) 19 | } 20 | totalFloat += ratio 21 | if (Math.abs(ratio - b.size / total) > 0.0001) { 22 | return new Error(`invalid ratio in ${rs._binKeys[i]}: 23 | ${b.size} / ${total}. expected: ${b.size/total}. actual: ${ratio}`) 24 | } 25 | } 26 | if (Math.abs(totalFloat-1.0) > 0.0001) { 27 | return new Error(`invalid total: ${totalFloat}`) 28 | } 29 | var slices = [] 30 | for (var i = 0; i < rs._binKeys.length; i++) { 31 | var b = rs._bins[rs._binKeys[i]] 32 | slices = slices.concat(b.slices) 33 | } 34 | for (var i = 0; i < slices.length; i++) { 35 | var a = slices[i] 36 | for (var j = i+1; j < slices.length; j++) { 37 | var b = slices[j] 38 | if (rat.lt(a[0],b[1]) && rat.gt(a[1],b[0])) { 39 | return new Error('overlap') 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /test/shrink.js: -------------------------------------------------------------------------------- 1 | var test = require('tape') 2 | var RSlice = require('../') 3 | var roundSlices = require('./lib/round-slices.js') 4 | var valid = require('./lib/valid.js') 5 | 6 | test('shrink', function (t) { 7 | var rs = new RSlice 8 | rs.set({ A: 20 }) 9 | rs.set({ B: 20 }) 10 | rs.set({ C: 20 }) 11 | rs.set({ D: 30 }) 12 | t.deepEqual(roundSlices(rs,2), { 13 | A: { size: 20, slices: [[0,0.22]] }, 14 | B: { size: 20, slices: [[0.5,0.72]] }, 15 | C: { size: 20, slices: [[0.33,0.39],[0.83,1]] }, 16 | D: { size: 30, slices: [[0.22,0.33],[0.39,0.5],[0.72,0.83]] } 17 | }) 18 | t.ifError(valid(rs), 'valid initial') 19 | rs.set({ D: 20 }) 20 | t.ifError(valid(rs), 'valid after shrinking D') 21 | t.equal(rs.getBins().D.size, 20) 22 | t.end() 23 | }) 24 | 25 | test('multi-grow/shrink', function (t) { 26 | var rs = new RSlice 27 | rs.set({ A: 20, B: 20, C: 20, D: 20 }) 28 | t.ifError(valid(rs), 'valid initial') 29 | rs.set({ A: 10 }) 30 | t.ifError(valid(rs), 'valid after shrinking A') 31 | rs.set({ B: 15 }) 32 | t.ifError(valid(rs), 'valid after shrinking B') 33 | rs.set({ C: 40 }) 34 | t.ifError(valid(rs), 'valid after growing C') 35 | rs.set({ B: 12 }) 36 | t.ifError(valid(rs), 'valid after shrinking B') 37 | rs.set({ D: 17 }) 38 | t.ifError(valid(rs), 'valid after shrinking D') 39 | rs.set({ A: 5 }) 40 | t.ifError(valid(rs), 'valid after shrinking A') 41 | rs.set({ B: 25 }) 42 | t.ifError(valid(rs), 'valid after growing B') 43 | t.equal(rs.getBins().A.size, 5) 44 | t.equal(rs.getBins().B.size, 25) 45 | t.equal(rs.getBins().C.size, 40) 46 | t.equal(rs.getBins().D.size, 17) 47 | t.end() 48 | }) 49 | -------------------------------------------------------------------------------- /LICENSE-PARITY: -------------------------------------------------------------------------------- 1 | The Parity Public License 6.0.0 2 | 3 | Contributor: James Halliday 4 | 5 | Source Code: https://github.com/peermaps/bitfield-db 6 | 7 | This license lets you use and share this software for free, as 8 | long as you contribute software you make with it. Specifically: 9 | 10 | If you follow the rules below, you may do everything with this 11 | software that would otherwise infringe either the contributor's 12 | copyright in it, any patent claim the contributor can license, 13 | or both. 14 | 15 | 1. Contribute changes and additions you make to this software. 16 | 17 | 2. If you combine this software with other software, contribute 18 | that other software. 19 | 20 | 3. Contribute software you develop, deploy, monitor, or run with 21 | this software. 22 | 23 | 4. Ensure everyone who gets a copy of this software from you, in 24 | source code or any other form, gets the text of this license 25 | and the contributor and source code lines above. 26 | 27 | 5. Do not make any legal claim against anyone accusing this 28 | software, with or without changes, alone or with other 29 | software, of infringing any patent claim. 30 | 31 | To contribute software, publish all its source code, in the 32 | preferred form for making changes, through a freely accessible 33 | distribution system widely used for similar source code, and 34 | license contributions not already licensed to the public on terms 35 | as permissive as this license accordingly. 36 | 37 | You are excused for unknowingly breaking 1, 2, or 3 if you 38 | contribute as required, or stop doing anything requiring this 39 | license, within 30 days of learning you broke the rule. 40 | 41 | **As far as the law allows, this software comes as is, without 42 | any warranty, and the contributor will not be liable to anyone 43 | for any damages related to this software or this license, for any 44 | kind of legal claim.** 45 | -------------------------------------------------------------------------------- /test/move.js: -------------------------------------------------------------------------------- 1 | var test = require('tape') 2 | var rat = require('bigint-rational') 3 | var RSlice = require('../') 4 | var calcMoves = require('./lib/move.js') 5 | var valid = require('./lib/valid.js') 6 | 7 | var tmpr0 = [0n,1n] 8 | var tmpr1 = [0n,1n] 9 | 10 | test('check move calculation', function (t) { 11 | var prev = new RSlice({ 12 | A: { 13 | size: 10, 14 | slices: [[[0n,1n],[1n,7n]],[[87n,364n],[1n,4n]]] 15 | }, 16 | B: { 17 | size: 15, 18 | slices: [[[1n,2n],[19n,26n]]] 19 | }, 20 | C: { 21 | size: 20, 22 | slices: [ 23 | [[5n,28n],[3n,14n]], 24 | [[5n,23n],[87n,364n]], 25 | [[1n,3n],[5n,12n]], 26 | [[5n,6n],[1n,1n]] 27 | ] 28 | }, 29 | D: { 30 | size: 20, 31 | slices: [ 32 | [[1n,7n],[5n,28n]], 33 | [[3n,14n],[5n,23n]], 34 | [[1n,4n],[1n,3n]], 35 | [[5n,12n],[1n,2n]], 36 | [[19n,26n],[5n,6n]] 37 | ] 38 | } 39 | }) 40 | var cur = new RSlice({ 41 | A: { 42 | size: 10, 43 | slices: [[[0n,1n],[2n,17n]]] 44 | }, 45 | B: { 46 | size: 15, 47 | slices: [[[1n,2n],[23n,34n]]] 48 | }, 49 | C: { 50 | size: 40, 51 | slices: [ 52 | [[2n,17n],[1n,4n]], 53 | [[397n,1326n],[5n,12n]], 54 | [[23n,34n],[19n,26n]], 55 | [[5n,6n],[1n,1n]] 56 | ] 57 | }, 58 | D: { 59 | size: 20, 60 | slices: [ 61 | [[1n,4n],[397n,1326n]], 62 | [[5n,12n],[1n,2n]], 63 | [[19n,26n],[5n,6n]] 64 | ] 65 | } 66 | }) 67 | var moves = calcMoves(prev,cur) 68 | t.deepEqual(moves, [23235n,142324n], 'expected move ratio') 69 | t.end() 70 | }) 71 | 72 | test('moves', function (t) { 73 | var rs = new RSlice 74 | rs.set({ A: 20, B: 20, C: 20, D: 20 }) 75 | 76 | var ops = [ 77 | { A: 10 }, // shrink 78 | { B: 15 }, // shrink 79 | { C: 40 }, // grow 80 | { B: 12 }, // shrink 81 | { D: 17 }, // shrink 82 | { A: 5 }, // shrink 83 | { B: 25 } // grow 84 | ] 85 | ops.forEach(function (op) { 86 | var prev = new RSlice(rs.getBins()) 87 | var prevTotal = 0 88 | var bins = rs.getBins() 89 | Object.keys(bins).forEach(function (key) { 90 | prevTotal += bins[key].size 91 | }) 92 | var newTotal = prevTotal 93 | Object.keys(op).forEach(function (key) { 94 | newTotal += op[key] - bins[key].size 95 | }) 96 | var expected = [0n,1n] 97 | Object.keys(op).forEach(function (key) { 98 | rat.set(tmpr0, bins[key].size, prevTotal) 99 | rat.set(tmpr1, op[key], newTotal) 100 | rat.subtract(tmpr0, tmpr0, tmpr1) 101 | rat.abs(tmpr0, tmpr0) 102 | rat.add(expected, expected, tmpr0) 103 | }) 104 | rat.reduce(expected, expected) 105 | rs.set(op) 106 | var moved = calcMoves(prev, rs) 107 | t.deepEqual(moved, expected, `moved for ${JSON.stringify(op)}`) 108 | t.ifError(valid(rs), `valid after ${JSON.stringify(op)}`) 109 | }) 110 | t.end() 111 | }) 112 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # random-slicing 2 | 3 | resizable hashing strategy for large-scale storage 4 | 5 | implements the algorithm from the [random slicing paper][paper], 6 | a better alternative to the [hash ring technique used in riak][riak-critique] 7 | 8 | The random slicing algorithm is designed to maintain a resizable address space 9 | across storage nodes while minimizing the amount of data that needs to be moved 10 | during a resize operation on the address space. 11 | 12 | The address space exists on a real number line from 0 to 1. Each bin is granted 13 | slices on this number line based on its size. These allotments may change over 14 | the course of the program, but the algorithm will minimize the change in slices 15 | for each bin while preserving the size ratios. 16 | 17 | This implementation internally uses arbitrary-precision rationals for slicing 18 | calculations to eliminate rounding errors as the system evolves over time. 19 | Consult the hash example for how to convert these rationals into the hash space 20 | of your chosen hashing algorithm. 21 | 22 | [paper]: ftp://ftp.cse.ucsc.edu/pub/darrell/miranda-tos14.pdf 23 | [riak-critique]: https://mobilemonitoringsolutions.com/article-a-critique-of-resizable-hash-tables-riak-core-random-slicing/ 24 | 25 | # example 26 | 27 | In this example, we initialize a previous allocation for nodes A (size 40) and B 28 | (size 120). In practice you might get this previous allocation from persistent 29 | storage or the network. 30 | 31 | Then, we shrink A from 40 to 32 and add a new node C with size 80. 32 | 33 | Finally we display the integer ratios for each slicing interval. 34 | 35 | ``` js 36 | var RS = require('random-slicing') 37 | var rs = new RS 38 | rs.set({ A: 40, B: 120 }) 39 | rs.set({ A: 32, C: 80 }) 40 | 41 | Object.entries(rs.getBins()).forEach(function ([key,bin]) { 42 | console.log(key, bin.size, bin.slices.map(showSlice).join(', ')) 43 | }) 44 | 45 | function showSlice ([start,end]) { 46 | return `${start[0]}/${start[1]}..${end[0]}/${end[1]}` 47 | } 48 | ``` 49 | 50 | which prints: 51 | 52 | ``` 53 | A 32 0/1..640/4640 54 | B 120 40/160..3560/4640 55 | C 80 640/4640..40/160, 3560/4640..160/160 56 | ``` 57 | 58 | # api 59 | 60 | ``` 61 | var RS = require('random-slicing') 62 | ``` 63 | 64 | ## var rs = new RS(bins) 65 | 66 | Initialize a new random slicing with an optional allocation of `bins`. 67 | 68 | `bins` should be of the format returned by `rs.getBins()` documented below. 69 | 70 | ## rs.set(updates) 71 | 72 | Set the new sizes with an object `updates` mapping keys to their new sizes. Keys 73 | not present in `updates` will keep the same size. 74 | 75 | To delete a bin, set its size to `0`. 76 | 77 | ## var bins = rs.getBins() 78 | 79 | Return the collection of allocated `bins`, an object that maps bin names to 80 | bins, where each `bin` has: 81 | 82 | * `bin.size` - presently allocated size 83 | * `bin.slices` - array of intervals 84 | 85 | Each interval is an array 2-tuple `[start,end]` and `start` and `end` are each 86 | array 2-tuples of the form `[numerator,denominator]` where `numerator` and 87 | denominator are both built-in bigints. 88 | 89 | For example, a `bin` might look like: 90 | 91 | ``` js 92 | { 93 | size: 10, 94 | slices: [[[0n,1n],[1n,7n]],[[87n,364n],[1n,4n]]] 95 | } 96 | ``` 97 | 98 | which contains slices from 0 to 1/7 and from 87/364 to 1/4. 99 | 100 | ## var str = rs.serialize() 101 | 102 | Serialize an `rs` instance to a string. 103 | 104 | ## var rs = RS.parse(str) 105 | 106 | Create a new `rs` instance from a previously serialized string `str`. 107 | 108 | # license 109 | 110 | [license zero parity](https://licensezero.com/licenses/parity) 111 | and [apache 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) 112 | (contributions) 113 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var rat = require('bigint-rational') 2 | var ZERO = [0n,1n] 3 | var r0 = [0n,1n] 4 | var r1 = [0n,1n] 5 | var r2 = [0n,1n] 6 | 7 | module.exports = RSlice 8 | 9 | function RSlice (bins) { 10 | var self = this 11 | if (!(self instanceof RSlice)) return new RSlice(bins) 12 | self._bins = {} 13 | if (bins) { 14 | Object.keys(bins).forEach(function (key) { 15 | self._bins[key] = { 16 | size: bins[key].size, 17 | slices: bins[key].slices.map(function (iv) { 18 | var start = [toBig(iv[0][0]),toBig(iv[0][1])] 19 | var end = [toBig(iv[1][0]),toBig(iv[1][1])] 20 | return [start,end] 21 | }) 22 | } 23 | }) 24 | } 25 | self._totalSize = 0 26 | self._binKeys = Object.keys(self._bins).sort() 27 | self._binKeys.forEach(function (key) { 28 | self._totalSize += self._bins[key].size 29 | }) 30 | } 31 | 32 | RSlice.prototype.getBins = function () { 33 | return this._bins 34 | } 35 | 36 | RSlice.parse = function (str) { 37 | return new RSlice(JSON.parse(str)) 38 | } 39 | 40 | RSlice.prototype.serialize = function () { 41 | var self = this 42 | var bins = {} 43 | for (var i = 0; i < self._binKeys.length; i++) { 44 | var key = self._binKeys[i] 45 | var bin = self._bins[key] 46 | bins[key] = { 47 | size: bin.size, 48 | slices: bin.slices.map(function (slice) { 49 | return [ 50 | [slice[0][0].toString(),slice[0][1].toString()], 51 | [slice[1][0].toString(),slice[1][1].toString()] 52 | ] 53 | }) 54 | } 55 | } 56 | return JSON.stringify(bins) 57 | } 58 | 59 | RSlice.prototype.set = function (updates) { 60 | var self = this 61 | var newSize = self._totalSize 62 | Object.keys(updates).forEach(function (key) { 63 | var bin = self._bins[key] 64 | newSize += updates[key] - (bin ? bin.size : 0) 65 | }) 66 | if (self._binKeys.length === 0) { 67 | var offset = [0n,1n] 68 | Object.keys(updates).forEach(function (key) { 69 | var iv = [ 70 | rat.copy([],offset), 71 | rat.create(updates[key],newSize) 72 | ] 73 | rat.add(iv[1], offset, iv[1]) 74 | rat.copy(offset, iv[1]) 75 | rat.reduce(iv[0], iv[0]) 76 | rat.reduce(iv[1], iv[1]) 77 | self._bins[key] = { size: updates[key], slices: [iv] } 78 | self._binKeys.push(key) 79 | self._totalSize += updates[key] 80 | }) 81 | self._binKeys.sort() 82 | return 83 | } 84 | var addedKey = false 85 | Object.keys(updates).forEach(function (key) { 86 | if (!self._bins[key]) { 87 | self._bins[key] = { size: 0, slices: [] } 88 | self._binKeys.push(key) 89 | addedKey = true 90 | } 91 | }) 92 | if (addedKey) self._binKeys.sort() 93 | 94 | // first phase: shrink intervals by removing gaps 95 | var gaps = [] 96 | for (var i = 0; i < self._binKeys.length; i++) { 97 | var key = self._binKeys[i] 98 | var bin = self._bins[key] 99 | var newBinSize = updates.hasOwnProperty(key) ? updates[key] : bin.size 100 | var newRatio = rat.set(r0, newBinSize, newSize) 101 | var ratio = sliceSum(r1, bin.slices) 102 | var delta = rat.subtract(r2, ratio, newRatio) // amount to shrink 103 | if (rat.lte(delta,ZERO)) continue 104 | var matched = false 105 | // first search for exact matches 106 | for (var j = 0; j < bin.slices.length; j++) { 107 | var iv = bin.slices[j] 108 | var len = length(r0, iv) 109 | if (!rat.eq(len, delta)) continue 110 | gaps.push(iv) 111 | bin.slices.splice(j,1) 112 | matched = true 113 | break 114 | } 115 | if (matched) continue 116 | // assign smaller intervals to gaps 117 | for (var j = 0; j < bin.slices.length; j++) { 118 | var iv = bin.slices[j] 119 | var len = length(r0, iv) 120 | if (rat.gt(len,delta)) continue 121 | gaps.push(iv) 122 | bin.slices.splice(j,1) 123 | j-- 124 | rat.subtract(delta, delta, len) 125 | if (rat.eq(delta,ZERO)) { 126 | matched = true 127 | break 128 | } 129 | } 130 | if (matched) continue 131 | // find a suitable node to split 132 | for (var j = 0; j < bin.slices.length; j++) { 133 | var iv = bin.slices[j] 134 | var len = length(r0, iv) 135 | if (rat.lt(len,delta)) continue 136 | if (rat.eq(len,delta)) { 137 | gaps.push(iv) 138 | bin.slices.splice(j,1) 139 | matched = true 140 | break 141 | } 142 | gaps.push([ 143 | rat.subtract([0n,1n],iv[1],delta), 144 | rat.copy([0n,1n],iv[1]) 145 | ]) 146 | rat.subtract(iv[1], iv[1], delta) 147 | matched = true 148 | break 149 | } 150 | if (matched) continue 151 | throw new Error('not matched: ' + key + ': ' + newBinSize) 152 | } 153 | // second phase: assign gaps to intervals that need to grow 154 | for (var i = 0; i < self._binKeys.length; i++) { 155 | var key = self._binKeys[i] 156 | var bin = self._bins[key] 157 | var newBinSize = updates.hasOwnProperty(key) ? updates[key] : bin.size 158 | var newRatio = rat.set(r0, newBinSize, newSize) 159 | var ratio = sliceSum(r1, bin.slices) 160 | var delta = rat.subtract(r2, newRatio, ratio) // amount to grow 161 | if (rat.lte(delta,ZERO)) continue 162 | var matched = false 163 | // first search for exact matches 164 | for (var j = 0; j < gaps.length; j++) { 165 | var iv = gaps[j] 166 | var len = length(r0, iv) 167 | if (!rat.eq(delta,len)) continue 168 | bin.slices.push(iv) 169 | gaps.splice(j,1) 170 | matched = true 171 | break 172 | } 173 | if (matched) continue 174 | // assign smaller gaps 175 | for (var j = 0; j < gaps.length; j++) { 176 | var iv = gaps[j] 177 | var len = length(r0, iv) 178 | if (rat.gt(len,delta)) continue 179 | bin.slices.push(iv) 180 | gaps.splice(j,1) 181 | j-- 182 | rat.subtract(delta,delta,len) 183 | if (rat.eq(delta,ZERO)) { 184 | matched = true 185 | break 186 | } 187 | } 188 | if (matched) continue 189 | // find a suitable gap to split 190 | for (var j = 0; j < gaps.length; j++) { 191 | var iv = gaps[j] 192 | var len = length(r0, iv) 193 | if (rat.eq(len,delta)) { 194 | bin.slices.push(iv) 195 | gaps.splice(j,1) 196 | matched = true 197 | break 198 | } else if (rat.lt(len,delta)) continue 199 | bin.slices.push([ 200 | rat.subtract([0n,1n],iv[1],delta), 201 | rat.copy([0n,1n],iv[1]) 202 | ]) 203 | rat.subtract(iv[1],iv[1],delta) 204 | matched = true 205 | break 206 | } 207 | if (matched) continue 208 | if (rat.eq(delta,ZERO)) continue 209 | throw new Error('not matched: ' + key + ': ' + newBinSize) 210 | } 211 | if (gaps.length > 0) throw new Error('gaps remain: ' + displayGaps(gaps)) 212 | // --- 213 | for (var i = 0; i < self._binKeys.length; i++) { 214 | var key = self._binKeys[i] 215 | var b = self._bins[key] 216 | if (updates.hasOwnProperty(key)) { 217 | b.size = updates[key] 218 | } 219 | if (b.size === 0) { 220 | delete self._bins[self._binKeys[i]] 221 | self._binKeys.splice(i,1) 222 | i-- 223 | } else cleanup(b) 224 | } 225 | self._totalSize = newSize 226 | } 227 | 228 | function sliceSum (out, slices) { 229 | rat.set(out, 0n, 1n) 230 | for (var i = 0; i < slices.length; i++) { 231 | rat.add(out, out, slices[i][1]) 232 | rat.subtract(out, out, slices[i][0]) 233 | } 234 | return out 235 | } 236 | 237 | function cleanup (dst) { 238 | // reduce, sort, remove 0-width, and combine adjacent slices 239 | for (var i = 0; i < dst.slices.length; i++) { 240 | var iv = dst.slices[i] 241 | rat.reduce(iv[0], iv[0]) 242 | rat.reduce(iv[1], iv[1]) 243 | } 244 | dst.slices.sort(cmpIv) 245 | for (var i = 0; i < dst.slices.length; i++) { 246 | if (rat.eq(length(r0, dst.slices[i]),ZERO)) { 247 | dst.slices.splice(i,1) 248 | i-- 249 | } 250 | } 251 | for (var i = 1; i < dst.slices.length; i++) { 252 | if (adjacent(dst.slices[i-1],dst.slices[i])) { 253 | dst.slices[i-1][1] = dst.slices[i][1] 254 | dst.slices.splice(i,1) 255 | i-- 256 | } 257 | } 258 | } 259 | 260 | function adjacent (g, iv) { 261 | if (!g) return false 262 | return rat.eq(g[1],iv[0]) || rat.eq(g[0],iv[1]) 263 | } 264 | function length (out, iv) { 265 | return rat.subtract(out, iv[1], iv[0]) 266 | } 267 | function cmpIv (a, b) { 268 | return rat.eq(a[0],b[0]) ? rat.compare(a[1],b[1]) : rat.compare(a[0],b[0]) 269 | } 270 | 271 | function displayGaps (gaps) { 272 | return '[' + gaps.map(function (g) { 273 | return '(' + g[0] + '..' + g[1] + ')' 274 | }).join(', ') + ']' 275 | } 276 | 277 | function toBig (x) { 278 | if (typeof x === 'string') { 279 | return BigInt(x) 280 | } else if (typeof x === 'bigint') { 281 | return x 282 | } else { 283 | throw new Error('unexpected interval start type ' + typeof x) 284 | } 285 | } 286 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | --------------------------------------------------------------------------------