├── .coveralls.yml ├── .editorconfig ├── .eslintrc ├── .gitignore ├── .jscsrc ├── .jshintrc ├── .travis.yml ├── LICENSE ├── README.md ├── index.js ├── package-lock.json ├── package.json └── test └── test-01.js /.coveralls.yml: -------------------------------------------------------------------------------- 1 | service_name: travis-ci 2 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | 7 | [*.js] 8 | indent_style = tab 9 | indent_size = 4 10 | trim_trailing_whitespace = true 11 | curly_bracket_next_line = true 12 | indent_brace_style = Allman 13 | quote_type = single 14 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": false, 4 | "node": true 5 | }, 6 | 7 | "globals": { 8 | "crypto": true, 9 | "escape": false, 10 | "unescape": false 11 | }, 12 | 13 | "ecmaFeatures": { 14 | "arrowFunctions": true, 15 | "binaryLiterals": true, 16 | "blockBindings": true, 17 | "defaultParams": true, 18 | "forOf": true, 19 | "generators": true, 20 | "objectLiteralComputedProperties": true, 21 | "objectLiteralDuplicateProperties": false, 22 | "objectLiteralShorthandMethods": true, 23 | "objectLiteralShorthandProperties": true, 24 | "octalLiterals": false, 25 | "regexUFlag": true, 26 | "regexYFlag": true, 27 | "superInFunctions": true, 28 | "templateStrings": true, 29 | "unicodeCodePointEscapes": true, 30 | "globalReturn": true 31 | }, 32 | 33 | "rules": { 34 | "block-scoped-var": 0, 35 | "brace-style": [2, "allman", { "allowSingleLine": true }], 36 | "camelcase": 0, 37 | "comma-spacing": [2, {"before": false, "after": true}], 38 | "comma-style": [2, "last"], 39 | "complexity": 0, 40 | "consistent-return": 0, 41 | "consistent-this": 0, 42 | "curly": 0, 43 | "default-case": 0, 44 | "dot-notation": 0, 45 | "eol-last": 2, 46 | "eqeqeq": [2, "allow-null"], 47 | "func-names": 0, 48 | "func-style": [0, "declaration"], 49 | "generator-star": 0, 50 | "global-strict": 0, 51 | "guard-for-in": 0, 52 | "handle-callback-err": [2, "^(err|error|anySpecificError)$" ], 53 | "indent": [2, "tab"], 54 | "max-depth": 0, 55 | "max-len": 0, 56 | "max-nested-callbacks": 0, 57 | "max-params": 0, 58 | "max-statements": 0, 59 | "new-cap": [2, { "newIsCap": true, "capIsNew": false }], 60 | "new-parens": 2, 61 | "no-alert": 2, 62 | "no-array-constructor": 2, 63 | "no-bitwise": 0, 64 | "no-caller": 2, 65 | "no-catch-shadow": 0, 66 | "no-cond-assign": 2, 67 | "no-console": 0, 68 | "no-constant-condition": 0, 69 | "no-control-regex": 2, 70 | "no-debugger": 2, 71 | "no-delete-var": 2, 72 | "no-div-regex": 0, 73 | "no-dupe-keys": 2, 74 | "no-else-return": 0, 75 | "no-empty": 0, 76 | "no-empty-character-class": 2, 77 | "no-eq-null": 0, 78 | "no-eval": 2, 79 | "no-ex-assign": 2, 80 | "no-extend-native": 2, 81 | "no-extra-bind": 2, 82 | "no-extra-boolean-cast": 2, 83 | "no-extra-parens": 0, 84 | "no-extra-semi": 2, 85 | "no-fallthrough": 2, 86 | "no-floating-decimal": 2, 87 | "no-func-assign": 2, 88 | "no-implied-eval": 2, 89 | "no-inline-comments": 0, 90 | "no-inner-declarations": [2, "functions"], 91 | "no-invalid-regexp": 2, 92 | "no-irregular-whitespace": 2, 93 | "no-iterator": 2, 94 | "no-label-var": 2, 95 | "no-labels": 0, 96 | "no-lone-blocks": 2, 97 | "no-lonely-if": 0, 98 | "no-loop-func": 0, 99 | "no-mixed-requires": [0, false], 100 | "no-mixed-spaces-and-tabs": [2, false], 101 | "no-multi-str": 2, 102 | "no-multiple-empty-lines": [2, {"max": 1}], 103 | "no-native-reassign": 2, 104 | "no-negated-in-lhs": 2, 105 | "no-nested-ternary": 0, 106 | "no-new": 2, 107 | "no-new-func": 2, 108 | "no-new-object": 2, 109 | "no-new-require": 2, 110 | "no-new-wrappers": 2, 111 | "no-obj-calls": 2, 112 | "no-octal": 0, 113 | "no-octal-escape": 2, 114 | "no-path-concat": 0, 115 | "no-plusplus": 0, 116 | "no-process-env": 0, 117 | "no-process-exit": 0, 118 | "no-proto": 2, 119 | "no-redeclare": 2, 120 | "no-regex-spaces": 2, 121 | "no-reserved-keys": 0, 122 | "no-restricted-modules": 0, 123 | "no-return-assign": 2, 124 | "no-script-url": 2, 125 | "no-self-compare": 2, 126 | "no-sequences": 2, 127 | "no-shadow": 0, 128 | "no-shadow-restricted-names": 2, 129 | "no-space-before-semi": 0, 130 | "no-spaced-func": 2, 131 | "no-sparse-arrays": 2, 132 | "no-sync": 0, 133 | "no-ternary": 0, 134 | "no-trailing-spaces": 2, 135 | "no-undef": 2, 136 | "no-undef-init": 2, 137 | "no-undefined": 0, 138 | "no-underscore-dangle": 0, 139 | "no-unreachable": 2, 140 | "no-unused-expressions": 0, 141 | "no-unused-vars": [2, {"vars": "local", "args": "none", "varsIgnorePattern": "demand"}], 142 | "no-use-before-define": 0, 143 | "no-var": 0, 144 | "no-void": 0, 145 | "no-warning-comments": [0, { "terms": ["todo", "fixme", "xxx"], "location": "start" }], 146 | "no-with": 2, 147 | "one-var": 0, 148 | "operator-assignment": [0, "always"], 149 | "padded-blocks": 0, 150 | "quote-props": 0, 151 | "quotes": [2, "single", "avoid-escape"], 152 | "radix": 2, 153 | "semi": [2, "always"], 154 | "sort-vars": 0, 155 | "space-before-function-paren": [2, "never"], 156 | "keyword-spacing": 2, 157 | "space-before-blocks": [2, "always"], 158 | "space-in-brackets": 0, 159 | "space-in-parens": [2, "never"], 160 | "space-infix-ops": 2, 161 | "space-unary-ops": [2, { "words": true, "nonwords": false }], 162 | "strict": 0, 163 | "use-isnan": 2, 164 | "valid-jsdoc": 0, 165 | "valid-typeof": 2, 166 | "vars-on-top": 0, 167 | "wrap-iife": [2, "any"], 168 | "wrap-regex": 0, 169 | "yoda": [2, "never"] 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | test/coverage.html 4 | .nyc_output 5 | npm-debug.log 6 | -------------------------------------------------------------------------------- /.jscsrc: -------------------------------------------------------------------------------- 1 | { 2 | "disallowSpacesInNamedFunctionExpression": { 3 | "beforeOpeningRoundBrace": true 4 | }, 5 | "disallowSpacesInFunctionExpression": { 6 | "beforeOpeningRoundBrace": true 7 | }, 8 | "disallowSpacesInAnonymousFunctionExpression": { 9 | "beforeOpeningRoundBrace": true 10 | }, 11 | "disallowSpacesInFunctionDeclaration": { 12 | "beforeOpeningRoundBrace": true 13 | }, 14 | "disallowEmptyBlocks": true, 15 | "disallowQuotedKeysInObjects": true, 16 | "disallowSpaceAfterObjectKeys": true, 17 | "disallowSpaceAfterPrefixUnaryOperators": true, 18 | "disallowSpaceBeforePostfixUnaryOperators": true, 19 | "disallowSpaceBeforeBinaryOperators": [ 20 | "," 21 | ], 22 | "disallowMixedSpacesAndTabs": true, 23 | "disallowTrailingWhitespace": true, 24 | "disallowKeywords": [ "with" ], 25 | "disallowMultipleLineBreaks": true, 26 | "requireParenthesesAroundIIFE": true, 27 | "requireSpacesInConditionalExpression": true, 28 | "requireBlocksOnNewline": 1, 29 | "requireCommaBeforeLineBreak": true, 30 | "requireSpaceBeforeBinaryOperators": true, 31 | "requireSpaceAfterBinaryOperators": true, 32 | "requireLineFeedAtFileEnd": true, 33 | "requireCapitalizedConstructors": true, 34 | "requireDotNotation": true, 35 | "requireSpacesInForStatement": true, 36 | "requireSpaceBetweenArguments": true, 37 | "requireCurlyBraces": [ 38 | "do" 39 | ], 40 | "requireSpaceAfterKeywords": [ 41 | "if", 42 | "else", 43 | "for", 44 | "while", 45 | "do", 46 | "switch", 47 | "case", 48 | "return", 49 | "try", 50 | "catch", 51 | "typeof" 52 | ], 53 | "safeContextKeyword": "self", 54 | "validateLineBreaks": "LF", 55 | "validateQuoteMarks": "'", 56 | "validateIndentation": "\t", 57 | "requireSemicolons": true 58 | } 59 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "bitwise" : false, // Prohibit bitwise operators (&, |, ^, etc.). 3 | "curly" : false, // Require {} for every new block or scope. 4 | "eqeqeq" : false, // Require triple equals i.e. `===`. 5 | "forin" : true, // Tolerate `for in` loops without `hasOwnPrototype`. 6 | "immed" : true, // Require immediate invocations to be wrapped in parens e.g. `( function(){}() );` 7 | "latedef" : false, // Prohibit variable use before definition. 8 | "newcap" : false, // Require capitalization of all constructor functions e.g. `new F()`. 9 | "noarg" : true, // Prohibit use of `arguments.caller` and `arguments.callee`. 10 | "noempty" : true, // Prohibit use of empty blocks. 11 | "nonew" : false, // Prohibit use of constructors for side-effects. 12 | "plusplus" : false, // Prohibit use of `++` & `--`. 13 | "regexp" : false, // Prohibit `.` and `[^...]` in regular expressions. 14 | "undef" : true, // Require all non-global variables be declared before they are used. 15 | "strict" : false, // Require `use strict` pragma in every file. 16 | "trailing" : false, // Prohibit trailing whitespaces. 17 | 18 | 19 | "asi" : false, // Tolerate Automatic Semicolon Insertion (no semicolons). 20 | "boss" : true, // Tolerate assignments inside if, for & while. Usually conditions & loops are for comparison, not assignments. 21 | "debug" : false, // Allow debugger statements e.g. browser breakpoints. 22 | "eqnull" : false, // Tolerate use of `== null`. 23 | "esnext" : true, // Allow ES.next specific features such as `const` and `let`. 24 | "evil" : false, // Tolerate use of `eval`. 25 | "expr" : true, // Tolerate `ExpressionStatement` as Programs. 26 | "funcscope" : false, // Tolerate declarations of variables inside of control structures while accessing them later from the outside. 27 | "globalstrict" : false, // Allow global "use strict" (also enables 'strict'). 28 | "iterator" : false, // Allow usage of __iterator__ property. 29 | "lastsemic" : false, // Tolerat missing semicolons when the it is omitted for the last statement in a one-line block. 30 | "laxbreak" : false, // Tolerate unsafe line breaks e.g. `return [\n] x` without semicolons. 31 | "laxcomma" : true, // Suppress warnings about comma-first coding style. 32 | "loopfunc" : false, // Allow functions to be defined within loops. 33 | "multistr" : false, // Tolerate multi-line strings. 34 | "onecase" : false, // Tolerate switches with just one case. 35 | "proto" : false, // Tolerate __proto__ property. This property is deprecated. 36 | "regexdash" : false, // Tolerate unescaped last dash i.e. `[-...]`. 37 | "scripturl" : false, // Tolerate script-targeted URLs. 38 | "smarttabs" : true, // Tolerate mixed tabs and spaces when the latter are used for alignmnent only. 39 | "shadow" : false, // Allows re-define variables later in code e.g. `var x=1; x=2;`. 40 | "sub" : true, // Tolerate all forms of subscript notation besides dot notation e.g. `dict['key']` instead of `dict.key`. 41 | "supernew" : false, // Tolerate `new function () { ... };` and `new Object;`. 42 | "validthis" : false, // Tolerate strict violations when the code is running in strict mode and you use this in a non-constructor function. 43 | 44 | "browser" : false, // Standard browser globals e.g. `window`, `document`. 45 | "node" : true, // Enable globals available when code is running inside of the NodeJS runtime environment. 46 | 47 | "maxerr" : 100, // Maximum errors before stopping. 48 | "predef" : [ // Extra globals. 49 | //"exampleVar", 50 | //"anotherCoolGlobal", 51 | //"iLoveDouglas" 52 | ], 53 | "indent" : 4 // Specify indentation spacing 54 | } 55 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - "4" 5 | - "6" 6 | - "7" 7 | after_success: npm run coverage 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2015, C J Silverio 2 | 3 | 4 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. 5 | 6 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Light-cycle 2 | ============ 3 | 4 | A consistent hashringcycle for sharding your dataz, with 100% more blue glow and 50% less Wagner. 5 | 6 | [![npm](http://img.shields.io/npm/v/light-cycle.svg?style=flat)](https://www.npmjs.org/package/light-cycle) [![Tests](http://img.shields.io/travis/ceejbot/light-cycle.svg?style=flat)](http://travis-ci.org/ceejbot/light-cycle) [![Coverage Status](https://img.shields.io/coveralls/ceejbot/light-cycle.svg?style=flat)](https://coveralls.io/github/ceejbot/light-cycle?branch=master) [![Dependencies](https://david-dm.org/ceejbot/light-cycle.svg)](https://david-dm.org/ceejbot/light-cycle) 7 | 8 | ## Usage 9 | 10 | To install: 11 | 12 | `npm install light-cycle` 13 | 14 | Sample usage: 15 | 16 | ```javascript 17 | var Lightcycle = require('light-cycle'); 18 | 19 | var cycle = new Lightcycle( 20 | { 21 | seed: 0xdeadbeef, 22 | size: 50 23 | }); 24 | 25 | // Create entries in the hash ring for each of our redis caches, using redis:host:port as 26 | // their unique ids in the ring. 27 | for (var i = 0; i < myRedisInstances.length; i++) 28 | { 29 | var redisShard = myRedisInstances[i]; 30 | cycle.add(redisShard, ['redis', redisShard.host, redisShard.port].join(':')); 31 | } 32 | 33 | // Now we have something to cache in one of our shards. 34 | var dataToStore = 35 | { 36 | id: '3421', 37 | data: 'This is very important data that must be cached in our redises.', 38 | timestamp: Date.now() 39 | }; 40 | 41 | // Where shall we store this? 42 | var whichRedis = cycle.locate(dataToStore.id); 43 | whichRedis.hmset(dataToStore.id, dataToStore, callback); 44 | ``` 45 | 46 | ## API 47 | 48 | Resources are any object or identifier you wish to store. You can store an open database connection, a resource identifier, or something else. This module does not attempt to inspect or use the resource. It just implements a sharding scheme based on the resource's id. 49 | 50 | ### new Lightcycle(settings) 51 | 52 | Construct a cycle. 53 | 54 | Settings may include the following fields: 55 | 56 | `seed`: seed for the hash function; must be a positive integer; defaults to `0xcafed00d` 57 | `size`: expected number of resources you'll be storing; defaults to 128 58 | `replicas`: number of replicas to store in the cycle for each resource; defaults to `size` 59 | 60 | If you want your light-cycle to behave identically to other invocations, pass the same hash seed. 61 | 62 | ### cycle.add(resource, id) 63 | 64 | Add a resource to the cycle. This will create *replicas* entries in the underlying data structure. The `id` parameter must be a string. 65 | 66 | ### cycle.remove(id) 67 | 68 | Remove the resource with the given id from the cycle. This removes all replica entries. 69 | 70 | ### cycle.locate(id) 71 | 72 | Given the id of some data you wish to locate, return the resource where it should reside. `id` may be a string or a buffer. 73 | 74 | ### cycle.rebalance() 75 | 76 | Resize the cycle to accomodate the current number of entries plus some padding. 77 | 78 | This is called automatically if the number of entries added exceeds the size option passed in at configuration. Rebalance is not automatically called when resources are removed. 79 | 80 | Hash keys are cached, so rebalancing shouldn't be too slow, but to avoid thrash rebalancing pads out the size and the replica count by `Lightcycle.SIZE_PAD` and `Lightcycle.REPLICAS_PAD` respectively. For best results, choose a size setting at start that can accomodate the number of resources you intend to use. 81 | 82 | ## See Also 83 | 84 | [Wikipedia](http://en.wikipedia.org/wiki/Consistent_hashing) is informative. 85 | 86 | [This page is another good introduction](http://www.martinbroadhurst.com/Consistent-Hash-Ring.html) to consistent hash rings. 87 | 88 | [So is this one.](http://www.tom-e-white.com/2007/11/consistent-hashing.html) 89 | 90 | And there's always [the original paper](http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.147.1879). 91 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // This code base means freedom. 2 | 3 | var 4 | assert = require('assert'), 5 | Skiplist = require('skiplist'), 6 | Xxhash = require('xxhashjs').h64 7 | ; 8 | 9 | var Lightcycle = module.exports = function Lightcycle(settings) 10 | { 11 | settings = settings || {}; 12 | 13 | this.seed = settings.seed || 0xcafed00d; 14 | this.size = Math.round(settings.size) || 128; 15 | assert(this.size > 0, 'you must pass in a positive integer for size'); 16 | 17 | this.replicas = settings.replicas || this.size; 18 | this.resources = new Skiplist(this.size * this.replicas + 16); // a little padding 19 | this.cache = {}; 20 | this.entries = {}; 21 | }; 22 | 23 | Lightcycle.prototype.seed = 0xcafed00d; 24 | Lightcycle.prototype.size = 128; 25 | Lightcycle.prototype.replicas = 128; 26 | Lightcycle.prototype.resources = null; 27 | Lightcycle.prototype.cache = null; 28 | Lightcycle.prototype.entries = null; 29 | 30 | // Constants used when rebalancing to leave space. 31 | Lightcycle.SIZE_PAD = 16; 32 | Lightcycle.REPLICAS_PAD = 8; 33 | 34 | Lightcycle.prototype.add = function add(resource, id) 35 | { 36 | assert(resource); 37 | assert(id && typeof id === 'string'); 38 | if (!this.cache[id]) 39 | this.cache[id] = []; 40 | var key; 41 | 42 | for (var i = 0; i < this.replicas; i++) 43 | { 44 | if (this.cache[id][i]) 45 | key = this.cache[id][i]; 46 | else 47 | { 48 | key = this.hashit(id + String(i)); 49 | this.cache[id][i] = key; 50 | } 51 | this.resources.insert(key, resource); 52 | } 53 | 54 | this.entries[id] = resource; 55 | if (Object.keys(this.entries).length > this.size) 56 | this.rebalance(); 57 | }; 58 | 59 | Lightcycle.prototype.remove = function remove(id) 60 | { 61 | assert(id && typeof id === 'string'); 62 | if (!Array.isArray(this.cache[id])) 63 | return; 64 | var key; 65 | 66 | for (var i = 0; i < this.replicas; i++) 67 | { 68 | key = this.cache[id][i]; 69 | this.resources.remove(key); 70 | } 71 | 72 | delete this.entries[id]; 73 | }; 74 | 75 | Lightcycle.prototype.locate = function locate(id) 76 | { 77 | var key = this.hashit(id); 78 | var results = this.resources.findWithCount(key, 1); 79 | 80 | if (results.length === 0) 81 | results = this.resources.findWithCount(null, 1); 82 | 83 | if (results.length > 0) 84 | return results[0][1]; 85 | 86 | return null; 87 | }; 88 | 89 | Lightcycle.prototype.hashit = function hashit(input) 90 | { 91 | if (!Buffer.isBuffer(input)) 92 | input = new Buffer(input); 93 | 94 | var hash = Xxhash(this.seed); 95 | hash.update(input); 96 | var result = hash.digest().toString(16); 97 | while (result.length < 8) result = '0' + result; 98 | 99 | return result; 100 | }; 101 | 102 | Lightcycle.prototype.all = function all() 103 | { 104 | return this.entries; 105 | }; 106 | 107 | Lightcycle.prototype.rebalance = function rebalance() 108 | { 109 | var ids = Object.keys(this.entries); 110 | 111 | this.size = ids.length + Lightcycle.SIZE_PAD; 112 | this.replicas = ids.length + Lightcycle.REPLICAS_PAD; 113 | this.resources = new Skiplist(this.size * this.replicas); 114 | 115 | for (var i = 0; i < ids.length; i++) 116 | this.add(this.entries[ids[i]], ids[i]); 117 | }; 118 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ceejbot/light-cycle", 3 | "description": "a consistent hash ring for your blue-glowing shards of PURE ENERGY", 4 | "version": "1.4.2", 5 | "author": "C J Silverio ", 6 | "bugs": { 7 | "url": "https://github.com/ceejbot/light-cycle/issues" 8 | }, 9 | "contributors": [ 10 | "C J Silverio ", 11 | "Kit Cambridge " 12 | ], 13 | "dependencies": { 14 | "skiplist": "~1.0.0", 15 | "xxhashjs": "~0.2.2" 16 | }, 17 | "devDependencies": { 18 | "coveralls": "~3.0.0", 19 | "eslint-config-ceejbot": "~1.1.1", 20 | "mocha": "~5.1.0", 21 | "must": "~0.13.4", 22 | "nyc": "~10.1.2", 23 | "xo": "~0.20.3" 24 | }, 25 | "homepage": "https://github.com/ceejbot/light-cycle", 26 | "keywords": [ 27 | "hashing", 28 | "hash ring", 29 | "consistent hash", 30 | "consistent hash ring", 31 | "sharding" 32 | ], 33 | "license": "MIT", 34 | "main": "index.js", 35 | "repository": "git://github.com/ceejbot/light-cycle.git", 36 | "scripts": { 37 | "lint": "xo", 38 | "test": "nyc mocha --check-leaks -R spec test/test*.js", 39 | "coverage": "nyc report --reporter=text-lcov | coveralls" 40 | }, 41 | "xo": { 42 | "extends": "eslint-config-ceejbot" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /test/test-01.js: -------------------------------------------------------------------------------- 1 | /*global describe:true, it:true, before:true, after:true */ 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 | }); 261 | --------------------------------------------------------------------------------