├── .gitignore ├── LICENSE ├── README.md ├── docs └── Discover.md ├── index.js ├── package.json ├── scripts ├── localtest.js └── test.js └── test ├── add.js ├── cache.js ├── find.js ├── new.js ├── onFindNode.js ├── onKBucketPing.js ├── onNode.js ├── onReached.js ├── onTransportPing.js ├── onUnreachable.js ├── register.js ├── unreachable.js └── unregister.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Tristan Slominski 4 | 5 | Permission is hereby granted, free of charge, to any person 6 | obtaining a copy of this software and associated documentation 7 | files (the "Software"), to deal in the Software without 8 | restriction, including without limitation the rights to use, 9 | copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the 11 | Software is furnished to do so, subject to the following 12 | conditions: 13 | 14 | The above copyright notice and this permission notice shall be 15 | included in all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 19 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 21 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 22 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 23 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 24 | OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # discover 2 | 3 | _Stability: 2 - [Stable](https://github.com/tristanls/stability-index#stability-2---stable)_ 4 | 5 | [![NPM version](https://badge.fury.io/js/discover.png)](http://npmjs.org/package/discover) 6 | 7 | Discover is a distributed master-less node discovery mechanism that enables locating any entity (server, worker, drone, actor) based on node id. It enables point-to-point communications without pre-defined architecture. 8 | 9 | ## Contributors 10 | 11 | [@tristanls](https://github.com/tristanls), [@mikedeboer](https://github.com/mikedeboer), [@skeggse](https://github.com/skeggse) 12 | 13 | ## Contents 14 | 15 | * [Installation](#installation) 16 | * [Tests](#tests) 17 | * [Overview](#overview) 18 | * [Documentation](#documentation) 19 | * [Discover](#discover-1) 20 | * [Transport Interface](#transport-interface) 21 | * [Road Map](#road-map) 22 | * [Sources](#sources) 23 | 24 | ## Installation 25 | 26 | npm install discover 27 | 28 | ## Tests 29 | 30 | ### Unit Tests 31 | 32 | npm test 33 | 34 | ### Localhost Visual Trace Test 35 | 36 | npm run-script localtest 37 | 38 | ## Overview 39 | 40 | Discover is a distributed master-less node discovery mechanism that enables locating any entity (server, worker, drone, actor) based on node id. It enables point-to-point communications without pre-defined architecture and without a centralized router or centralized messaging. 41 | 42 | It is worth highlighting that Discover is _only_ a discovery mechanism. You can find out where a node is located (it's hostname and port, for example), but to communicate with it, you should have a way of doing that yourself. 43 | 44 | Each Discover instance stores information on numerous nodes. Each instance also functions as an external "gateway" of sorts to beyond the local environment. For example, if a local process wants to send a message to a remote process somewhere, Discover enables distributed master-less correlation of that remote process' node id with it's physical location so that a point-to-point link can be made (or failure reported if the contact cannot be located). 45 | 46 | ### Contacts 47 | 48 | Discover manages information about _nodes_ via maintaining node information in a structure called a _contact_. A contact stores the details of a particular node on the network. 49 | 50 | A contact is a JavaScript object that consists of `contact.id`, `contact.data`, and `contact.transport`. The `id` and `data` are the only properties that are guaranteed not to be changed by Discover. 51 | 52 | * `id`: _String (base64)_ A globally unique Base64 encoded node id. 53 | * `data`: _Any_ Any data that should be included with this contact when it is retrieved by others on the network. This should be a "serializable" structure (no circular references) so that it can be `JSON.stringify()`ed. 54 | * `transport`: _Any_ Any data that the transport mechanism requires for operation. Similarly to `data`, it should be a "serializable" structure so that it can be `JSON.stringify()`ed. 55 | 56 | Example contact with TCP Transport information: 57 | 58 | ```javascript 59 | var contact = { 60 | id: "Zm9v", // Base64 encoded String representing node id 61 | data: "foo", // any data (could be {foo: "bar"}, or ["foo", "bar"], etc.) 62 | transport: { 63 | host: "foo.bar.com", // or "localhost", "127.0.0.1", etc... 64 | port: 6742 65 | } 66 | }; 67 | ``` 68 | 69 | ### Use of `contact.transport` 70 | 71 | The transport data is only required for contact's that are _seeds_. That is, their transport information is known ahead of time so that a Discover node can connect to them. For all non-seed contacts, the `contact.transport` will be provided by the particular transport implementation. 72 | 73 | ### Use of `contact.data` 74 | 75 | As explained below in [Technical Origin Details](#technical-origin-details), Discover is intended to implement only PING and FIND-NODE RPCs. This reflects the intent of Discover to be a discovery mechanism and not a data storage/distribution mechanism. It is important to keep that in mind when using `contact.data`. 76 | 77 | The existence of `contact.data` is to support the discovery mechanism. Given that `contact.transport` contains information for how a Discover transport can connect to another Discover transport, this is not very useful if one is trying to figure out the endpoint address of another node for application level purposes. It may not correspond at all to what's in `contact.transport`. The intended use of `contact.data` is to store a **minimal** amount of information required for connecting to the node endpoint for the application's purpose. 78 | 79 | For example, if we want a DNS-like functionality, we could look for a contact with id of `my.secret.dns.com`. This could correspond to the following contact: 80 | 81 | ```javascript 82 | var contact = { 83 | id: "bXkuc2VjcmV0LmRucy5jb20=", // Base64 encoding of "my.secret.dns.com" 84 | data: { 85 | host: "10.22.1.37", 86 | port: 8080 87 | }, 88 | transport: { 89 | host: "10.22.1.37", 90 | port: 6742 91 | } 92 | }; 93 | ``` 94 | 95 | This would tell us that we can connect to `my.secret.dns.com` at IP address `10.22.1.37` and port `8080`. 96 | 97 | As another example and to illustrate perhaps less familiar intents, if we want to find an actor "receptionist" in the global actor system, we could look for a contact that looks like this: 98 | 99 | ```javascript 100 | var contact = { 101 | id: "tmqjRAfBILbEC6aaHoz3AurtluM=", // Base64 encoded receptionist address 102 | data: { 103 | webkey: "c9bf857b35ed4750ca35c0a4f41e56644df59547", 104 | host: "10.13.211.201", 105 | port: 9999, 106 | publicKey: "mQINBFJhVUwBEADRwsK6hvXoZU/niqZU2k9NXVNA9kAiVBfhUZjJZhT4BUrh1R6PynIBLWmGbQhcId5CVLlLSL/3WszBE5g1QrcA72vdffgHhF845Y5ErqAKwIhu0dEO6iNw/LYVVo0RKMXEIrDJkklv5gijdJfbyIxswxh/iKav4HI9nhFpxZBt8gykONf4wCAZevHA8KEsUFyY6pCjbVTJzIwYcgGNJWbQaowxH1yMo2rxZMG9AeerCr/TsdTyOZXjSPYf4yDarxk6br690OiQnUtFGvFNl0VZstWVB2B7v62icrWXHKAXyLvSUZMGW7GGbfiwjHoj5JVZXe6MgKw6TWiLgW/49docdTfjtlRzPHpvk6VdxFPtwSHuQW7GO9xIXkI6ZopTbkQ8PW1eaqlA/FWz6UwvDxT2bn6YCIxe024U9LJTvBg0n5tyP9Pbqv5UHyiGOQOXzPwGfSFqfdfK8Z9W8WtHpfw4/imh2w8ecB4hmBIjhUujREKDTALHq12t+A/8wnQMyCDA4llWQSmNEnHJtiXwKh98a0H9IjGXFfM+YiFzHCWIScxV/12V1EXlJe8Qu0YwOBmJUAfoKeRHSvQ+lB+h8wlw/yWszUhgDCKuswtr1OF3+ZsEBeM2i4EtFfgobvKUOPoNUZ/T0Nye0Z5Re8uYJXY+domLIjgIRSExmTl8n69ILwARAQABtB1FeGFtcGxlIDxleGFtcGxlQGV4YW1wbGUuY29tPokCPgQTAQIAKAUCUmFVTAIbAwUJAAaXgAYLCQgHAwIGFQgCCQoLBBYCAwECHgECF4AACgkQpJhrsYLKyttAlg/+MXZCyeF6B6qmU/2PXXmIYt6axcEozkcUZ7Mq2CoTs0zQgkzlboUny6auKpZuExPm38NM/KH+Q0nUvYw+UEV3xEPP1iwtBKtP10JY0OyMijqcR6I95KmPIgv5FXQqBKiJuwub168jUHVeHa6IUo4aIBBSvXlXsW46gi9vDKk5a/7AnLoYhmoT4DprofNjrkX6ldjI4W2CGR1xIPkbFMXb6Emu0SWPXb7JjQoNpBxbL5+8jVKOw/p2YGxCnP1P7DSCuOJbNWORUPf6A38nOOUQekU/2uaJFAvTnQO+9JdxJuPTFAq7wlutzYbt4aUK0qsbIWww0IAmUMkzCSob/EZOlM9OZYMeTitua6KYLDBy82ceRqZEn9Ss/nYVFZD2PzyrV4X7eXcM/9vodORmD+BzRr3gq0R1ErUbEDw8nuv02exaRav0xH/ly4D8We4qAGoN0xaJ8PY8+Du/aUPX6hWze/U+lJRnQPwYY1p3AB6fEJVDRWIUNFS3PUvHcq/YicWhNHf7S/aIg36d1laEhhW/EWVpBYpFsLJ2H9RppYft/8b1pOfoM0DXUUVRA8InZTKmpoCTDWXx0XSiSJzO4bPIIc0X2XxSc9zTEWPJulGo84UG99ESvh+TY+K7N506HllSQ15sfod1Hvx015C6Vy6i4HdjJXKU263ysJU5UWA7VyK5Ag0EUmFVTAEQANjuB28jtCCM7qLiaA1JnB118F9IQE7oNJKQZV7Rq/ZKE5ZHk0RnJ4c4uzTNlmrD/KEKLyEbmU9WO7lnpUKYAEJtRczn4j+MCDahM60cuB3IW9k3F7B9EUPDpCanb+D1GH7HVAiEP6ad6bGcqLjui/X1Lu7Xr9qwH5C9AHo1+k4h1CjBLoJ8X3fdRqEvYc/fNsp6qAYhpWSLZVyinh7xoQ7kplXMlOLILftAZ3FyNcxCLb1L2eKPUCTbf8xXKnVqcGnGfHzeYTslMENNA71rrjaKtBW9souBVl9GpZtwBCRwuDm03XzlZm7odzZTokggzqodP6/JQbQBiaJfM3EvG2vhDqiVYiQki+ybwxT/Zq9Rk5Geb31gh8hQQJk6nljxJ5qmxhwEJ81QbdX5RBoJPwl9KtC9IBN8V89HwtDxPX8BM9Z2226PFUKmTZk4K6F614EHdaBL6i9faf9T2tJygP/unQd67JGYv2X/nDUvot3NwkJRKwE9yy1NxcJWCHR/9pO9biUqCpKbHLLqqaO/UtDdng2kl64n3FTbPar/KmAcspixX8z6uLPn1z8u0SV42zy7YLfBUcnxF4jy49VUKm86Awn10gGKOByPvcD6xFqp/GlLNLVv+GtbMfGy4yYEWwfMoc0yjaEXNj5OPnWcjHcVFgejkq47FrFhtn1eYECLABEBAAGJAiUEGAECAA8FAlJhVUwCGwwFCQAGl4AACgkQpJhrsYLKyttSbQ/+IN8TVh0bcd0wZremWrOcRI19knv2Z8bVp1e6uzbG91/TOqlr6QexxJf7HbM5CCizf3OSYRYzTGc/7QJOPzDyGh8+YTtdOdPOICTLEjnGlqyKKiggNGHr6tsJKdgYh9qL7TaT13ZkX9NnBWzQCim8aqcouUC/2zjrOSsGNA9sk9OVleJ6aQCikQETmPhjqs2sD4vFmyv2dSneMbtd/31L1JHvmrwDZt85gsXrt7I00Gty4fjGw9DG3jGNoA6f4AiAbkf1jlRmAfDlwsNEn44HXNQ712Tmo0Un+q2yq9I6yDPVVBD73qtq8IVy+bDZ8XanI7E//SLpPNdc03v1Laki1s4cn0UQHGc7ZdM8NsofiBZDJphh0/nItdE0QZaJtiO5QTzJyKFZjt2mm47SE4u9HWGcTr98Nqdn8/ZqNfW51p/2VxoriIRQoejBxQB7npM6nBcpnFFQLJhRNrbeAJdgGibsB99I2Z1mRT/NAIC8xFT5ojyPvU2sEy7IFva57gSAaM2IgFEDEBVsfS0otcpByW+oJtonYkmAnGmqY1aMNe9HN58OGns76jb9zL1RcmekIqrBqkBjdxdJEcC/T1MILIRBubjETvW5VgGbbf+CpSBHyMCvB53r0ciW07+dbnv9KohonKAwRYKwEulkbtJSogNhlUfZNgaWYco9YzK2K1Q=" 107 | }, 108 | transport: { 109 | host: "10.13.211.201", 110 | port: 6742 111 | } 112 | }; 113 | ``` 114 | 115 | This would tell us that we can access the actor using the published webkey at IP address `10.13.211.201` and port `9999` and to encrypt our communication using provided public key. 116 | 117 | Uses of `contact.data` that are not "minimal" in this way can result in poor system behavior. 118 | 119 | ### Arbiter function and arbiter defaults 120 | 121 | Discover implements a conflict resolution mechanism using an `arbiter` function. The purpose of the `arbiter` is to choose between two `contact` objects with the same `id` but perhaps different properties and determine which one should be stored. As the `arbiter` function returns the actual object to be stored, it does not need to make an either/or choice, but instead could perform some sort of operation and return the result as a new object that would then be stored. `arbiterDefaults` function makes sure that `contact` has the appropriate defualt properties for the `arbiter` function to work correctly. 122 | 123 | `arbiter` function is used in three places. First, it is used as the k-bucket `arbiter` function. Second, it is used to determine whether a new remote contact should be inserted into the LRU cache (if `arbiter` returns something `!==` to the cached contact the remote contact will be inserted). Third, it is used to determine if unregistering a contact will succeed (if `arbiter` returns contact `===` to the stored contact and stored contact `!==` contact we want to unregister, then unregister will fail). 124 | 125 | For example, an `arbiter` function implementing a `vectorClock` mechanism (the default mechanism) would look something like: 126 | 127 | ```javascript 128 | // contact example 129 | var contact = { 130 | id: new Buffer('contactId'), 131 | vectorClock: 0 132 | }; 133 | 134 | function arbiterDefaults(contact) { 135 | if (!contact.vectorClock) { 136 | contact.vectorClock = 0; 137 | } 138 | return contact; 139 | }; 140 | 141 | function arbiter(incumbent, candidate) { 142 | if (!incumbent 143 | || (incumbent && !incumbent.vectorClock) 144 | || (incumbent && incumbent.vectorClock && candidate.vectorClock 145 | && (candidate.vectorClock >= incumbent.vectorClock))) { 146 | 147 | return candidate; 148 | } 149 | return incumbent; 150 | }; 151 | ``` 152 | 153 | _NOTE: `contact.vectorClock` is not guaranteed to be passed by the transport. This is a known bug. See [#9](https://github.com/tristanls/discover/issues/9) for updates._ 154 | 155 | Alternatively, consider an arbiter that implements a Grow-Only-Set CRDT mechanism: 156 | 157 | ```javascript 158 | // contact example 159 | var contact = { 160 | id: new Buffer('workerService'), 161 | data: { 162 | workerNodes: { 163 | '17asdaf7effa2': { host: '127.0.0.1', port: 1337 }, 164 | '17djsyqeryasu': { host: '127.0.0.1', port: 1338 } 165 | } 166 | } 167 | }; 168 | 169 | function arbiterDefaults(contact) { 170 | if (!contact.data) { 171 | contact.data = {}; 172 | } 173 | if (!contact.data.workerNodes) { 174 | contact.data.workerNodes = {}; 175 | } 176 | return contact; 177 | }; 178 | 179 | function arbiter(incumbent, candidate) { 180 | if (!incumbent || !incumbent.data || !incumbent.data.workerNodes) { 181 | return candidate; 182 | } 183 | 184 | if (!candidate || !candidate.data || !candidate.data.workerNodes) { 185 | return incumbent; 186 | } 187 | 188 | // we create a new object so that our selection is guaranteed to replace 189 | // the incumbent 190 | var merged = { 191 | id: incumbent.id, // incumbent.id === candidate.id within an arbiter 192 | data: { 193 | workerNodes: incumbent.data.workerNodes 194 | } 195 | }; 196 | 197 | Object.keys(candidate.data.workerNodes).forEach(function (workerNodeId) { 198 | merged.data.workerNodes[workerNodeId] = 199 | candidate.data.workerNodes[workerNodeId]; 200 | }); 201 | 202 | return merged; 203 | } 204 | ``` 205 | 206 | Notice that in the above case, the Grow-Only-Set assumes that each worker node has a globally unique id and that each value for a worker node id will be written only once. 207 | 208 | ### Technical Origin Details 209 | 210 | Discover is implemented using a stripped down version of the Kademlia Distributed Hash Table (DHT). It uses only the PING and FIND-NODE Kademlia protocol RPCs. (It leaves out STORE and FIND-VALUE). 211 | 212 | An enhancement on top of the Kademlia protocol implementation is the inclusion of an arbiter function in the discovery mechanism. See [Arbiter function and arbiter details](#arbiter-function-and-arbiter-defaults) for more detailed explanation. 213 | 214 | ### Why Discover? 215 | 216 | There are three reasons. 217 | 218 | Discover grew out of my experience with building messaging for a Node.js Platform as a Service based on an Actor Model of Computation. I did not like having a centralized messaging service that could bring down the entire platform. Messaging should be decentralized, which led to a Kademlia DHT-based implementation. _see: [Technical Origin Details](#technical-origin-details)_ 219 | 220 | Every Kademlia DHT implementation I came across in Node.js community tightly coupled the procotocol implementation with the transport implementation. 221 | 222 | Lastly, I wanted to learn and commit to intuition the implementation of Kademlia DHT so that I can apply that knowledge in other projects. 223 | 224 | ## Documentation 225 | 226 | ### Discover 227 | 228 | Node ids in Discover are represented as base64 encoded Strings. This is because the default generated node ids (20 random bytes) could be unsafe to print. `base64` encoding was picked over `hex` encoding because it takes up less space when printed or serialized in ASCII over the wire. 229 | 230 | _For more detailed documentation including private methods see [Discover doc](docs/Discover.md)_ 231 | 232 | **Public API** 233 | 234 | * [new Discover(options)](#new-discoveroptions) 235 | * [discover.add(remoteContact)](#discoveraddremotecontact) 236 | * [discover.find(nodeId, callback, \[announce\])](#discoverfindnodeid-callback-announce) 237 | * [discover.register(contact)](#discoverregistercontact) 238 | * [discover.unreachable(contact)](#discoverunreachablecontact) 239 | * [discover.unregister(contact)](#discoverunregistercontact) 240 | * [Event 'stats.timers.find.ms'](#event-statstimersfindms) 241 | * [Event 'stats.timers.find.request.ms'](#event-statstimersfindrequestms) 242 | * [Event 'stats.timers.find.round.ms'](#event-statstimersfindroundms) 243 | 244 | ### new Discover(options) 245 | 246 | * `options`: 247 | * `CONCURRENCY_CONSTANT`: _Integer_ _(Default: 3)_ Number of concurrent FIND-NODE requests to the network per `find` request. 248 | * `arbiter`: _Function_ _(Default: vector clock arbiter)_ `function (incumbent, candidate) {}` An optional arbiter function. `arbiter` function is used in three places. First, it is used as the k-bucket `arbiter` function. Second, it is used to determine whether a new remote contact should be inserted into the LRU cache (if `arbiter` returns something `!==` to the cached contact the remote contact will be inserted). Third, it is used to determine if unregistering a contact will succeed (if `arbiter` returns contact `===` to the stored contact, unregister will fail). 249 | * `arbiterDefaults`: _Function_ _(Default: vector clock arbiter defaults)_ `function (contact) {}` An optional arbiter defaults function that sets `contact` arbiter defaults when a `contact` is first registered. Remote contacts that are added via `add` are assumed to have appropriate arbiter properties already set. 250 | * `eventTrace`: _Boolean_ _(Default: false)_ If set to `true`, Discover will emit `~trace` events for debugging purposes. 251 | * `inlineTrace`: _Boolean_ _(Default: false)_ If set to `true`, Discover will log to console `~trace` messages for debugging purposes. 252 | * `maxCacheSize`: _Number_ _(Default: 1000)_ Maximum number of `contacts` to keep in non-kBucket cache (see #6) 253 | * `noCache`: _Boolean_ _(Default: false)_ If `true`, non-kBucket cache is not used. 254 | * `seeds`: _Array_ _(Default: [])_ An array of seed `contact` Objects that the `transport` understands. 255 | * `transport`: _Object_ _(Default: `discover-tcp-transport`)_ An optional initialized and ready to use transport module for sending communications that conforms to the Transport Protocol. If `transport` is not provided, a new instance of `discover-tcp-transport` will be created and used with default settings. 256 | 257 | Creates a new Discover instance. 258 | 259 | The `seeds` are necessary if joining an existing Discover cluster. Discover will use these `seeds` to announce itself to other nodes in the cluster. If `seeds` are not provided, then it is assumed that this is a seed node, and other nodes will include this node's address in their `seeds` option. It all has to start somewhere. 260 | 261 | ### discover.add(remoteContact) 262 | 263 | * `remoteContact`: _Object_ Contact object to add that is not managed by this Discover node. 264 | * `id`: _String (base64)_ The contact id, base64 encoded. 265 | * `data`: _Any_ Data to be included with the contact, it is guaranteed to be returned for anyone querying for this `contact` by `id`. 266 | * Return: _Object_ Contact that was added. 267 | 268 | Adds the `remoteContact`. This is different from `discover.register(contact)` in that adding a `remoteContact` means that the `remoteContact` _is not_ managed by this Discover node. 269 | 270 | The use-case motivating existence of this method is being able to hint where to send a response in a request-response type of asynchronous messaging between nodes that are part of the same Discover DHT. More precisely: 271 | 272 | 1. Server A creates a contact Alpha and registers it with Discover. 273 | 2. Server A queries Discover to find contact Beta (already existing). 274 | 3. Discover responds that contact Beta is on Server B. 275 | 4. Server A sends a message to contact Beta (on Server B) expecting a response to contact Alpha. 276 | 5. Server B wants to respond to Alpha "quickly". At this point, the contact Alpha information has not propagated through the DHT, so Server B will have to wait for it's Discover instance to query the DHT and make multiple trips looking for contact Alpha. 277 | 278 | In order to "speed up" step 5 above, we'd like to be able to hint information that is known, but maybe has not propagated yet. This means, that as part of step 4 above, we could also send a "hint" containing information on contact Alpha. This way, when Server B receives the message with a "hint", it can use `discover.add(remoteContact)` to populate it's local Discover cache without additional network traffic. 279 | 280 | ### discover.find(nodeId, callback, [announce]) 281 | 282 | * `nodeId`: _String (base64)_ The node id to find, base64 encoded. 283 | * `callback`: _Function_ The callback to call with the result of searching for `nodeId`. 284 | * `announce`: _Object_ _(Default: undefined)_ _**CAUTION: reserved for internal use**_ Contact object, if specified, it indicates an announcement to the network so we ask the network instead of satisfying request locally and the sender is the `announce` contact object. 285 | 286 | The `callback` is called with the result of searching for `nodeId`. The result will be a `contact` containing `contact.id`, `contact.data`, and `contact.transport` of the node. If an error occurs, only `error` will be provided. 287 | 288 | ```javascript 289 | discover.find('bm9kZS5pZC50aGF0LmltLmxvb2tpbmcuZm9y', function (error, contact) { 290 | if (error) return console.error(error); 291 | console.dir(contact); 292 | }); 293 | ``` 294 | 295 | ### discover.register(contact) 296 | 297 | * `contact`: _Object_ Contact object to register. 298 | * `id`: _String (base64)_ _(Default: `crypto.randomBytes(20).toString('base64'`)_ The contact id, base 64 encoded; will be created if not present. 299 | * `data`: _Any_ Data to be included with the contact, it is guaranteed to be returned for anyone querying for this `contact` by `id`. 300 | * Return: _Object_ Contact that was registered with `id` and generated arbiter defaults if necessary. 301 | 302 | Registers a new node on the network with `contact.id`. Returns a `contact`: 303 | 304 | ```javascript 305 | discover.register({ 306 | id: 'Zm9v', // base64 encoded String representing nodeId 307 | data: 'foo' 308 | }); 309 | ``` 310 | 311 | _NOTE: Current implementation creates a new k-bucket for every registered node id. It is important to remember that a k-bucket could store up to k*lg(n) contacts, where lg is log base 2, n is the number of registered node ids on the network, and k is the size of each k-bucket (by default 20). For 1 billion registered nodes on the network, each k-bucket could store around 20 * lg (1,000,000,000) = ~ 598 contacts. This isn't bad, until you have 1 million local entities for a total of 598,000,000 contacts plus k-bucket overhead, which starts to put real pressure on Node.js/V8 memory limit._ 312 | 313 | ### discover.unreachable(contact) 314 | 315 | * `contact`: _Object_ Contact object to report unreachable 316 | * `id`: _String (base64)_ The previously registered contact id, base 64 encoded. 317 | 318 | Reports the `contact` as unreachable in case Discover is storing outdated information. This can happen because Discover is a local cache of the global state of the network. If a change occurs, it may not immediately propagate to the local Discover instance. 319 | 320 | If it is desired to get the latest `contact` that is unreachable, the following code shows an example: 321 | 322 | ```javascript 323 | discover.find("Zm9v", function (error, contact) { 324 | // got contact 325 | // attempt to connect ... and fail :( 326 | discover.unreachable(contact); 327 | discover.find(contact.id, function (error, contact) { 328 | // new contact will be found in the network 329 | // or an error if it cannot be found 330 | }); 331 | }); 332 | ``` 333 | 334 | ### discover.unregister(contact) 335 | 336 | * `contact`: _Object_ Contact object to register 337 | * `id`: _String (base64)_ The previously registered contact id, base 64 encoded. 338 | 339 | Unregisters previously registered `contact` (if `arbiter` returns `contact` and not other stored value) from the network. 340 | 341 | #### Event: `stats.timers.find.ms` 342 | 343 | * `function (latency) {}` 344 | * `latency`: _Number_ Latency of `discover.find()` in milliseconds. 345 | 346 | #### Event: `stats.timers.find.request.ms` 347 | 348 | * `function (latency) {}` 349 | * `latency`: _Number_ Latency of a single request to another Discover noder as part of a round of `discover.find()` DHT lookups. 350 | 351 | #### Event: `stats.timers.find.round.ms` 352 | 353 | * `function (latency) {}` 354 | * `latency`: _Number_ Latency of a single round of `discover.find()` DHT lookups in milliseconds. 355 | 356 | ### Transport Interface 357 | 358 | Modules implementing the transport mechanism for Discover shall conform to the following interface. A `transport` is a JavaScript object. 359 | 360 | Transport implementations shall ensure that `contact.id` and `contact.data` will be immutable and will pass through the transportation system without modification (`contact` objects are passed through the transportation system during FIND-NODE and PING requests). 361 | 362 | Transport has full dominion over `contact.transport` property. 363 | 364 | Transport implementations shall allow registering and interacting with event listeners as provided by `events.EventEmitter` interface. 365 | 366 | For reference implementation, see [discover-tcp-transport](https://github.com/tristanls/node-discover-tcp-transport). 367 | 368 | _NOTE: Unreachability of nodes depends on the transport. For example, transports ,like TLS transport, could use invalid certificate criteria for reporting unreachable nodes._ 369 | 370 | _**WARNING**: Using TCP transport is meant primarily for development in a development environment. TCP transport exists because it is a low hanging fruit. It is most likely that it should be replaced with DTLS transport in production (maybe TLS if DTLS is not viable). There may also be a use-case for using UDP transport if communicating nodes are on a VPN/VPC. Only if UDP on a VPN/VPC seems not viable, should TCP transport be considered._ 371 | 372 | **Transport Interface Specification** 373 | 374 | * [transport.findNode(contact, nodeId, sender)](#transportfindnodecontact-nodeid-sender) 375 | * [transport.ping(contact, sender)](#transportpingcontact-sender) 376 | * [transport.setTransportInfo(contact)](#transportsettransportinfocontact) 377 | * [Event 'findNode'](#event-findnode) 378 | * [Event 'node'](#event-node) 379 | * [Event 'ping'](#event-ping) 380 | * [Event 'reached'](#event-reached) 381 | * [Event 'unreachable'](#event-unreachable) 382 | 383 | ### transport.findNode(contact, nodeId, sender) 384 | 385 | * `contact`: _Object_ The node to contact with request to find `nodeId`. 386 | * `id`: _String (base64)_ Base64 encoded contact node id. 387 | * `transport`: _Any_ Any data that the transport mechanism requires for operation. 388 | * `nodeId`: _String (base64)_ Base64 encoded string representation of the node id to find. 389 | * `sender`: _Object_ The sender of this request. 390 | * `id`: _String (base64)_ Base64 encoded sender id. 391 | * `data`: _Any_ Sender data. 392 | * `transport`: _Any_ Any data that the transport mechanism requires for operation. 393 | 394 | Issues a FIND-NODE request to the `contact`. Response, timeout, errors, or otherwise shall be communicated by emitting a `node` event. 395 | 396 | ### transport.ping(contact, sender) 397 | 398 | * `contact`: _Object_ Contact to ping. 399 | * `id`: _String (base64)_ Base64 encoded contact node id. 400 | * `transport`: _Any_ Any data that the transport mechanism requires for operation. 401 | * `sender`: _Object_ The sender of this request. 402 | * `id`: _String (base64)_ Base64 encoded sender id. 403 | * `data`: _Any_ Sender data. 404 | * `transport`: _Any_ Any data that the transport mechanism requires for operation. 405 | 406 | Issues a PING request to the `contact`. The transport will emit `unreachable` event if the `contact` is unreachable, or `reached` event otherwise. 407 | 408 | #### transport.setTransportInfo(contact) 409 | 410 | * `contact`: _Object_ A contact. 411 | * Return: _Object_ `contact` with `contact.transport` populated. 412 | 413 | Sets `contact.transport` to transport configured values. 414 | 415 | #### Event: `findNode` 416 | 417 | * `nodeId`: _String (base64)_ Base64 encoded string representation of the node id to find. 418 | * `sender`: _Object_ The contact making the request. 419 | * `id`: _String (base64)_ Base64 encoded sender id. 420 | * `data`: _Any_ Sender data. 421 | * `transport`: _Any_ Any data that the transport mechanism requires for operation. 422 | * `callback`: _Function_ The callback to call with the result of processing the FIND-NODE request. 423 | * `error`: _Error_ An error, if any. 424 | * `response`: _Object_ or _Array_ The response to FIND-NODE request. 425 | 426 | Emitted when another node issues a FIND-NODE request to this node. 427 | 428 | ```javascript 429 | transport.on('findNode', function (nodeId, sender, callback) { 430 | // this node knows the node with nodeId or is itself node with nodeId 431 | var error = null; 432 | return callback(error, contactWithNodeId); 433 | }); 434 | ``` 435 | 436 | A single `contactWithNodeId` shall be returned with the information identifying the contact corresponding to requested `nodeId`. 437 | 438 | ```javascript 439 | transport.on('findNode', function (nodeId, sender, callback) { 440 | // nodeId is unknown to this node, so it returns an array of nodes closer to it 441 | var error = null; 442 | return callback(error, closestContacts); 443 | }); 444 | ``` 445 | 446 | An Array of `closestContacts` shall be returned if the `nodeId` is unknown to this node. 447 | 448 | If an error occurs and a request cannot be fulfilled, an error should be passed to the callback. 449 | 450 | ```javascript 451 | transport.on('findNode', function (nodeId, sender, callback) { 452 | // some error happened 453 | return callback(new Error("oh no!")); 454 | }); 455 | ``` 456 | 457 | ### Event: `node` 458 | 459 | * `error`: _Error_ An error, if one occurred. 460 | * `contact`: _Object_ The node that FIND-NODE request was sent to. 461 | * `nodeId`: _String_ The original node id requested to be found. 462 | * `response`: _Object_ or _Array_ The response from the queried `contact`. 463 | 464 | If `error` occurs, the transport encountered an error when issuing the `findNode` request to the `contact`. `contact` and `nodeId` will also be provided in case of an error. `response` is to be undefined if an `error` occurs. 465 | 466 | `response` will be an Array if the `contact` does not contain the `nodeId` requested. In this case `response` will be a `contact` list of nodes closer to the `nodeId` that the queried node is aware of. The usual step is to next query the returned contacts with the FIND-NODE request. 467 | 468 | `response` will be an Object if the `contact` knows of the `nodeId`. In other words, the node has been found, and `response` is a `contact` object. 469 | 470 | ### Event: `ping` 471 | 472 | * `nodeId`: _String (base64)_ Base64 encoded string representation of the node id being pinged. 473 | * `sender`: _Object_ The contact making the request. 474 | * `id`: _String (base64)_ Base64 encoded sender node id. 475 | * `data`: _Any_ Sender node data. 476 | * `transport`: _Any_ Any data that the transport mechanism requires for operation. 477 | * `callback`: _Function_ The callback to call with the result of processing the PING request. 478 | * `error`: _Error_ An error, if any. 479 | * `response`: _Object_ or _Array_ The response to PING request, if any. 480 | 481 | Emitted when another node issues a PING request to this node. 482 | 483 | ```javascript 484 | transport.on('ping', function (nodeId, sender, callback) { 485 | // ... verify that we have the exact node specified by nodeId 486 | return callback(null, contact); 487 | }); 488 | ``` 489 | 490 | In the above example `contact` is an Object representing the answer to `ping` query. 491 | 492 | If the exact node specified by nodeId does not exist, an error shall be returned as shown below: 493 | 494 | ```javascript 495 | transport.on('ping', function (nodeId, sender, callback) { 496 | // ...we don't have the nodeId specified 497 | return callback(true); 498 | }); 499 | ``` 500 | 501 | ### Event: `reached` 502 | 503 | * `contact`: _Object_ The contact that was reached when pinged. 504 | * `id`: _String (base64)_ Base64 encoded contact node id. 505 | * `data`: _Any_ Data included with the contact. 506 | * `transport`: _Any_ Any data that the transport mechanism requires for operation. 507 | 508 | Emitted when a previously pinged `contact` is deemed reachable by the transport. 509 | 510 | ### Event: `unreachable` 511 | 512 | * `contact`: _Object_ The contact that was unreachable when pinged. 513 | * `id`: _String (base64)_ Base64 encoded contact node id. 514 | * `transport`: _Any_ Any data that the transport mechanism requires for operation. 515 | 516 | Emitted when a previously pinged `contact` is deemed unreachable by the transport. 517 | 518 | ## Road Map 519 | 520 | ### Immediate concerns 521 | 522 | This is roughly in order of current priority: 523 | 524 | * **Update Transport Interface**: The transport interface should probably guarantee immutability and pass through of `contact.arbiter` property (much like it does right now for `contact.id` and `contact.data`). See [#9](https://github.com/tristanls/discover/issues/9) for more details. 525 | * **Implementation Correctness**: Gain confidence that the protocol functions as expected. This should involve running a lot of nodes and measuring information distribution latency and accuracy. 526 | * **TLS Transport** _(separate module)_ or it might make sense to change the TCP Transport into Net Transport and include within both TCP and TLS. 527 | * **UDP Transport** _(separate module)_ 528 | * **DTLS Transport** _(separate module)_ 529 | * **Less destructive unregister**: Currently, `discover.unregister(contact)` deletes all "closest" contact information that was gathered within the k-bucket corresponding to the `contact`. This throws away DHT information stored there. An elaboration would be to distribute known contacts to other k-buckets when a `contact` is unregistered. 530 | * **Performance**: Make it fast and small. 531 | * **discover.kBuckets**: It should be a datastructure with _O(log n)_ operations. 532 | * **Storage Refactoring**: There emerged (obvious in retrospect) a "storage" abstraction during the implementation of `discover` that is higher level than a `k-bucket` but that still seems to be worth extracting. 533 | * _24 Sep 2013:_ Despite a storage abstraction, it is not straightforward to separate out due to the 'ping' interaction between `k-bucket` and transport. KBucket storage implementation would have to pass some sort of token to Discover in order to remove an old contact form the correct KBucket (a closer KBucket could be registered while pinging is happening), but this exposes internal implementation, the hiding of which, was the point of abstracting a storage mechanism. It is also a very KBucket specific mechanism that I have difficulty generalizing to a common "storage" interface. Additionally, I am hard pressed to see Discover working well with non-k-bucket storage. Thusly, storage refactoring is no longer a priority. 534 | 535 | ### Other considerations 536 | 537 | This is a non-exclusive list of some of the highlights to keep in mind and maybe implement if opportunity presents itself. 538 | 539 | #### Settle the vocabulary 540 | 541 | Throughout Discover, the transport, and the k-bucket implementations, the vocabulary is inconsistent (in particular the usage of "contact", "node", "network", and "DHT"). Once the implementation settles and it becomes obvious what belongs where, it will be helpful to have a common, unifying way to refer to everything. 542 | 543 | ## Sources 544 | 545 | The implementation has been sourced from: 546 | 547 | - [A formal specification of the Kademlia distributed hash table](http://maude.sip.ucm.es/kademlia/files/pita_kademlia.pdf) 548 | 549 | ### Background Reading 550 | 551 | - [Eventually Consistent: Not What You Were Expecting?](http://queue.acm.org/detail.cfm?id=2582994) 552 | -------------------------------------------------------------------------------- /docs/Discover.md: -------------------------------------------------------------------------------- 1 | ## Discover 2 | 3 | This is a more detailed documentation that includes private methods for reference. 4 | 5 | Node ids in Discover are represented as base64 encoded Strings. This is because the default generated node ids (SHA-1 hashes) could be unsafe to print. `base64` encoding was picked over `hex` encoding because it takes up less space when printed or serialized in ASCII over the wire. 6 | 7 | * [new Discover(options)](#new-discoveroptions) 8 | * [discover.add(remoteContact)](#discoveraddremotecontact) 9 | * [discover.executeQuery(query, callback)](#discoverexecutequeryquery-callback) 10 | * [discover.find(nodeId, callback, \[announce\])](#discoverfindnodeid-callback-announce) 11 | * [discover.findViaSeeds(nodeId, callback, \[announce\])](#discoverfindviaseedsnodeid-callback-announce) 12 | * [discover.getClosestContacts(nodeId, closestKBuckets)](#discovergetclosestcontactsnodeid-closestkbuckets) 13 | * [discover.getClosestKBuckets(nodeId)](#discovergetclosestkbucketsnodeid) 14 | * [discover.queryCompletionCheck(query, callback)](#discoverquerycompletioncheckquery-callback) 15 | * [discover.register(contact)](#discoverregistercontact) 16 | * [discover.timerEndInMilliseconds(type, key)](#discovertimerendinmillisecondstype-key) 17 | * [discover.timerStart(type, key)](#discovertimerstarttype-key) 18 | * [discover.trace(message)](#discovertracemessage) 19 | * [discover.unreachable(contact)](#discoverunreachablecontact) 20 | * [discover.unregister(contact)](#discoverunregistercontact) 21 | * [Event 'stats.timers.find.ms'](#event-statstimersfindms) 22 | * [Event 'stats.timers.find.request.ms'](#event-statstimersfindrequestms) 23 | * [Event 'stats.timers.find.round.ms'](#event-statstimersfindroundms) 24 | 25 | #### new Discover(options) 26 | 27 | * `options`: 28 | * `CONCURRENCY_CONSTANT`: _Integer_ _(Default: 3)_ Number of concurrent FIND-NODE requests to the network per `find` request. 29 | * `arbiter`: _Function_ _(Default: vector clock arbiter)_ `function (incumbent, candidate) {}` An optional arbiter function. `arbiter` function is used in three places. First, it is used as the k-bucket `arbiter` function. Second, it is used to determine whether a new remote contact should be inserted into the LRU cache (if `arbiter` returns something `!==` to the cached contact the remote contact will be inserted). Third, it is used to determine if unregistering a contact will succeed (if `arbiter` returns contact `===` to the stored contact, unregister will fail). 30 | * `arbiterDefaults`: _Function_ _(Default: vector clock arbiter defaults)_ `function (contact) {}` An optional arbiter defaults function that sets `contact` arbiter defaults when a `contact` is first registered. Remote contacts that are added via `add` are assumed to have appropriate arbiter properties already set. 31 | * `eventTrace`: _Boolean_ _(Default: false)_ If set to `true`, Discover will emit `~trace` events for debugging purposes. 32 | * `inlineTrace`: _Boolean_ _(Default: false)_ If set to `true`, Discover will log to console `~trace` messages for debugging purposes. 33 | * `maxCacheSize`: _Number_ _(Default: 1000)_ Maximum number of `contacts` to keep in non-kBucket cache (see #6) 34 | * `noCache`: _Boolean_ _(Default: false)_ If `true`, non-kBucket cache is not used. 35 | * `seeds`: _Array_ _(Default: [])_ An array of seed `contact` Objects that the `transport` understands. 36 | * `transport`: _Object_ _(Default: `discover-tcp-transport`)_ An optional initialized and ready to use transport module for sending communications that conforms to the Transport Protocol. If `transport` is not provided, a new instance of `discover-tcp-transport` will be created and used with default settings. 37 | 38 | Creates a new Discover instance. 39 | 40 | The `seeds` are necessary if joining an existing Discover cluster. Discover will use these `seeds` to announce itself to other nodes in the cluster. If `seeds` are not provided, then it is assumed that this is a seed node, and other nodes will include this node's address in their `seeds` option. It all has to start somewhere. 41 | 42 | #### discover.add(remoteContact) 43 | 44 | * `remoteContact`: _Object_ Contact object to add that is not managed by this Discover node. 45 | * `id`: _String (base64)_ The contact id, base64 encoded. 46 | * `data`: _Any_ Data to be included with the contact, it is guaranteed to be returned for anyone querying for this `contact` by `id`. 47 | * Return: _Object_ Contact that was added. 48 | 49 | Adds the `remoteContact`. This is different from `discover.register(contact)` in that adding a `remoteContact` means that the `remoteContact` _is not_ managed by this Discover node. 50 | 51 | The use-case motivating existence of this method is being able to hint where to send a response in a request-response type of asynchronous messaging between nodes that are part of the same Discover DHT. More precisely: 52 | 53 | 1. Server A creates a contact Alpha and registers it with Discover. 54 | 2. Server A queries Discover to find contact Beta (already existing). 55 | 3. Discover responds that contact Beta is on Server B. 56 | 4. Server A sends a message to contact Beta (on Server B) expecting a response to contact Alpha. 57 | 5. Server B wants to respond to Alpha "quickly". At this point, the contact Alpha information has not propagated through the DHT, so Server B will have to wait for it's Discover instance to query the DHT and make multiple trips looking for contact Alpha. 58 | 59 | In order to "speed up" step 5 above, we'd like to be able to hint information that is known, but maybe has not propagated yet. This means, that as part of step 4 above, we could also send a "hint" containing information on contact Alpha. This way, when Server B receives the message with a "hint", it can use `discover.add(remoteContact)` to populate it's local Discover cache without additional network traffic. 60 | 61 | #### discover.executeQuery(query, callback) 62 | 63 | _**CAUTION: reserved for internal use**_ 64 | 65 | * `query`: _Object_ Object containing query state for this request. 66 | * `nodeId`: _String (base64)_ Base64 encoded node id to find. 67 | * `nodes`: _Array_ `contact`s to query for `nodeId` arranged from closest to furthest 68 | * `node`: 69 | * `id`: _String (base64)_ Base64 encoded contact id. 70 | * `nodesMap`: _Object_ A map to the same `contact`s already present in `nodes` for O(1) access. 71 | * `callback`: _Function_ The callback to call with result. 72 | 73 | Used internally by `discover.find()` to maintain query state when looking for a specific node on the network. It will launch up to `CONCURRENCY_CONSTANT` `findNode` requests via the `transport` and keep going until the node is found or there are no longer any nodes closer to it (not found). Additionally, this function reports unreachable and reached nodes to the appropriate `KBucket` which maintains the local node's awareness of the state of the network. 74 | 75 | #### discover.find(nodeId, callback, [announce]) 76 | 77 | * `nodeId`: _String (base64)_ The node id to find, base64 encoded. 78 | * `callback`: _Function_ The callback to call with the result of searching for `nodeId`. 79 | * `announce`: _Object_ _(Default: undefined)_ _**CAUTION: reserved for internal use**_ Contact object, if specified, it indicates an announcement to the network so we ask the network instead of satisfying request locally and the sender is the `announce` contact object. 80 | 81 | The `callback` is called with the result of searching for `nodeId`. The result will be a `contact` containing `contact.id`, `contact.data`, `contact.transport` of the node. If an error occurs, only `error` will be provided. 82 | 83 | ```javascript 84 | discover.find('bm9kZS5pZC50aGF0LmltLmxvb2tpbmcuZm9y', function (error, contact) { 85 | if (error) return console.error(error); 86 | console.dir(contact); 87 | }); 88 | ``` 89 | 90 | #### discover.findViaSeeds(nodeId, callback, [announce]) 91 | 92 | _**CAUTION: reserved for internal use**_ 93 | 94 | * `nodeId`: _String (base64)_ Base64 encoded node id to find. 95 | * `callback`: _Function_ The callback to call with the result of searching for `nodeId`. 96 | * `announce`: _Object_ _(Default: undefined)_ Contact object, if specified, it indicates an announcement and the sender is the `announce` contact object. 97 | 98 | Uses `seeds` instead of closest contacts (because those don't exist) to find the node with `nodeId`. The `callback` is called with the result of searching for `nodeId`. The result will be a `contact` containing `contact.id` and `contact.data` of the node. If an error occurs, only `error` will be provided. 99 | 100 | #### discover.getClosestContacts(nodeId, closestKBuckets) 101 | 102 | _**CAUTION: reserved for internal use**_ 103 | 104 | * `nodeId`: _String (base64)_ Base64 encoded node id to find closest contacts to. 105 | * `closestKBuckets`: _Array_ Sorted array of `KBucket`s from closest to furthest from `nodeId`. 106 | * Return: _Array_ List of closest contacts. 107 | 108 | Retrieves maximum of three closest contacts from the closest `KBucket`. 109 | 110 | #### discover.getClosestKBuckets(nodeId) 111 | 112 | _**CAUTION: reserved for internal use**_ 113 | 114 | * `nodeId`: _String (base64)_ Base64 encoded node id to find closest contacts to. 115 | * Return: _Array_ List of closest `KBucket`s. 116 | 117 | Retrieves a sorted list of all `KBucket`s from closest to furthest. 118 | 119 | #### discover.queryCompletionCheck(query, callback) 120 | 121 | _**CAUTION: reserved for internal use**_ 122 | 123 | * `query`: _Object_ Object containing query state for this request. 124 | * `nodeId`: _String (base64)_ Base64 encoded node id to find. 125 | * `nodes`: _Array_ `contact`s to query for `nodeId` arranged from closest to furthest. 126 | * `node`: 127 | * `id`: _String (base64)_ Base64 encoded contact id. 128 | * `nodesMap`: _Object_ A map to the same `contact`s already present in `nodes` for O(1) access. 129 | * `callback`: _Function_ The callback to call with result. 130 | 131 | Checks if query completion criteria are met. If there are any new nodes to add to the query, organizes them accordingly and sets the query state to incorporate new node information. Stops and returns failure or success otherwise. 132 | 133 | #### discover.register(contact) 134 | 135 | * `contact`: _Object_ Contact object to register. 136 | * `id`: _String (base64)_ _(Default: `crypto.randomBytes(20).toString('base64'`)_ The contact id, base 64 encoded; will be created if not present. 137 | * `data`: _Any_ Data to be included with the contact, it is guaranteed to be returned for anyone querying for this `contact` by `id`. 138 | * Return: _Object_ Contact that was registered with `id` and generated arbiter defaults if necessary. 139 | 140 | Registers a new node on the network with `contact.id`. Returns a `contact`: 141 | 142 | ```javascript 143 | discover.register({ 144 | id: 'Zm9v', // base64 encoded String representing nodeId 145 | data: 'foo' 146 | }); 147 | ``` 148 | 149 | _NOTE: Current implementation creates a new k-bucket for every registered node id. It is important to remember that a k-bucket could store up to k*lg(n) contacts, where lg is log base 2, n is the number of registered node ids on the network, and k is the size of each k-bucket (by default 20). For 1 billion registered nodes on the network, each k-bucket could store around 20 * lg (1,000,000,000) = ~ 598 contacts. This isn't bad, until you have 1 million local entities for a total of 598,000,000 contacts plus k-bucket overhead, which starts to put real pressure on Node.js/V8 memory limit._ 150 | 151 | #### discover.timerEndInMilliseconds(type, key) 152 | 153 | _**CAUTION: reserved for internal use**_ 154 | 155 | * `type`: _String_ Timer type. 156 | * `key`: _String_ Timer key. 157 | * Return: _Number_ Milliseconds since the first time in the timer. 158 | 159 | Calculates a millisecond interval between now and the first timer that was stored at `type` and `key`. 160 | 161 | #### discover.timerStart(type, key) 162 | 163 | _**CAUTION: reserved for internal use**_ 164 | 165 | * `type`: _String_ Timer type. 166 | * `key`: _String_ Timer key. 167 | 168 | Starts a new timer indexed by `type` and `key`. Multiple starts will result in start times being stored in an array for use by `discover.timerEndInMilliseconds()` later. 169 | 170 | #### discover.trace(message) 171 | 172 | * `message`: _String_ Message to trace. 173 | 174 | Logs or emits a `~trace` for debugging purposes. 175 | 176 | #### discover.unreachable(contact) 177 | 178 | * `contact`: _Object_ Contact object to report unreachable 179 | * `id`: _String (base64)_ The previously registered contact id, base 64 encoded. 180 | 181 | Reports the `contact` as unreachable in case Discover is storing outdated information. This can happen because Discover is a local cache of the global state of the network. If a change occurs, it may not immediately propagate to the local Discover instance. 182 | 183 | If it is desired to get the latest `contact` that is unreachable, the following code shows an example: 184 | 185 | ```javascript 186 | discover.find("Zm9v", function (error, contact) { 187 | // got contact 188 | // attempt to connect ... and fail :( 189 | discover.unreachable(contact); 190 | discover.find(contact.id, function (error, contact) { 191 | // new contact will be found in the network 192 | // or an error if it cannot be found 193 | }); 194 | }); 195 | ``` 196 | 197 | #### discover.unregister(contact) 198 | 199 | * `contact`: _Object_ Contact object to register 200 | * `id`: _String (base64)_ The previously registered contact id, base 64 encoded. 201 | 202 | Unregisters previously registered `contact` (if `arbiter` returns `contact` and not other stored value) from the network. 203 | 204 | #### Event: `stats.timers.find.ms` 205 | 206 | * `function (latency) {}` 207 | * `latency`: _Number_ Latency of `discover.find()` in milliseconds. 208 | 209 | #### Event: `stats.timers.find.request.ms` 210 | 211 | * `function (latency) {}` 212 | * `latency`: _Number_ Latency of a single request to another Discover noder as part of a round of `discover.find()` DHT lookups. 213 | 214 | #### Event: `stats.timers.find.round.ms` 215 | 216 | * `function (latency) {}` 217 | * `latency`: _Number_ Latency of a single round of `discover.find()` DHT lookups in milliseconds. 218 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | index.js - "discover": Node discovery based on Kademlia DHT protocol 4 | 5 | The MIT License (MIT) 6 | 7 | Copyright (c) 2013-2014 Tristan Slominski 8 | 9 | Permission is hereby granted, free of charge, to any person 10 | obtaining a copy of this software and associated documentation 11 | files (the "Software"), to deal in the Software without 12 | restriction, including without limitation the rights to use, 13 | copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | copies of the Software, and to permit persons to whom the 15 | Software is furnished to do so, subject to the following 16 | conditions: 17 | 18 | The above copyright notice and this permission notice shall be 19 | included in all copies or substantial portions of the Software. 20 | 21 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 22 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 23 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 24 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 25 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 26 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 27 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 28 | OTHER DEALINGS IN THE SOFTWARE. 29 | 30 | */ 31 | "use strict"; 32 | 33 | var clone = require('clone'), 34 | crypto = require('crypto'), 35 | events = require('events'), 36 | KBucket = require('k-bucket'), 37 | LruCache = require('lru-cache'), 38 | util = require('util'); 39 | 40 | /* 41 | * `options`: 42 | * `CONCURRENCY_CONSTANT`: _Integer_ _(Default: 3)_ Number of concurrent 43 | FIND-NODE requests to the network per `find` request. 44 | * `arbiter`: _Function_ _(Default: vector clock arbiter)_ 45 | `function (incumbent, candidate) {}` An optional arbiter function. 46 | `arbiter` function is used in three places. First, it is used as the 47 | k-bucket `arbiter` function. Second, it is used to determine whether 48 | a new remote contact should be inserted into the LRU cache (if 49 | `arbiter` returns something `!==` to the cached contact the remote 50 | contact will be inserted). Third, it is used to determine if 51 | unregistering a contact will succeed (if `arbiter` returns contact 52 | `===` to the stored contact, unregister will fail). 53 | * `arbiterDefaults`: _Function_ _(Default: vector clock arbiter defaults)_ 54 | `function (contact) {}` An optional arbiter defaults function that 55 | sets `contact` arbiter defaults when a `contact` is first registered. 56 | Remote contacts that are added via `add` are assumed to have 57 | appropriate arbiter properties already set. 58 | * `eventTrace`: _Boolean_ _(Default: false)_ If set to `true`, Discover will 59 | emit `~trace` events for debugging purposes. 60 | * `inlineTrace`: _Boolean_ _(Default: false)_ If set to `true`, Discover 61 | will log to console `~trace` messages for debugging purposes. 62 | * `maxCacheSize`: _Number_ _(Default: 1000)_ Maximum number of `contacts` to 63 | keep in non-kBucket cache (see #6) 64 | * `noCache`: _Boolean_ _(Default: false)_ If `true`, non-kBucket cache is not 65 | used. 66 | * `seeds`: _Array_ _(Default: [])_ An array of seed `contact` Objects that 67 | the `transport` understands. 68 | * `transport`: _Object_ _(Default: `discover-tcp-transport`)_ An optional 69 | initialized and ready to use transport module for sending 70 | communications that conforms to the Transport Protocol. 71 | If `transport` is not provided, a new instance of 72 | `discover-tcp-transport` will be created and used with default 73 | settings. 74 | */ 75 | var Discover = module.exports = function Discover (options) { 76 | var self = this; 77 | events.EventEmitter.call(self); 78 | 79 | options = options || {}; 80 | 81 | self.CONCURRENCY_CONSTANT = options.CONCURRENCY_CONSTANT || 3; 82 | self.maxCacheSize = options.maxCacheSize || 1000; 83 | self.noCache = options.noCache === undefined ? false : options.noCache; 84 | 85 | self.arbiterDefaults = options.arbiterDefaults || function arbiterDefaults(contact) { 86 | if (!contact.vectorClock) { 87 | contact.vectorClock = 0; 88 | } 89 | return contact; 90 | }; 91 | 92 | self.arbiter = options.arbiter || function arbiter(incumbent, candidate) { 93 | if (!incumbent 94 | || (incumbent && !incumbent.vectorClock) 95 | || (incumbent && incumbent.vectorClock && candidate.vectorClock 96 | && (candidate.vectorClock >= incumbent.vectorClock))) { 97 | 98 | return candidate; 99 | } 100 | return incumbent; 101 | }; 102 | 103 | self.seeds = options.seeds || []; 104 | // configure tracing for debugging purposes 105 | // TODO: probably change this to some sort of sane logging 106 | self.options = options; 107 | 108 | self.tracing = (options.inlineTrace || options.eventTrace); 109 | 110 | if (self.tracing) 111 | self.trace('tracing is enabled'); 112 | 113 | self.transport = options.transport; 114 | if (!self.transport) { 115 | var TcpTransport = require('discover-tcp-transport'); 116 | self.transport = new TcpTransport(options); 117 | } 118 | 119 | // register a listener to update our k-buckets with nodes that we manage 120 | // contact with 121 | self.transport.on('node', function (error, contact, nodeId, response) { 122 | var latency = self.timerEndInMilliseconds('find.request.ms', contact.id + nodeId); 123 | if (error) { 124 | self.emit('stats.timers.find.request.ms', latency); 125 | return; // failed contact 126 | } 127 | 128 | if (Array.isArray(response)) { 129 | // arbiter "closer" responses against locally registered contacts 130 | response.forEach(function (res) { 131 | if (self.kBuckets[res.id]) { 132 | self.kBuckets[res.id].contact = 133 | self.arbiter(self.kBuckets[res.id].contact, res); 134 | } 135 | }); 136 | } else if (response && response.id) { 137 | // arbiter exact match response against locally registered contacts 138 | if (self.kBuckets[response.id]) { 139 | self.kBuckets[response.id].contact = 140 | self.arbiter(self.kBuckets[response.id].contact, response); 141 | } 142 | } 143 | 144 | // we successfully contacted the "contact", add it 145 | self.add(contact); 146 | self.emit('stats.timers.find.request.ms', latency); 147 | }); 148 | 149 | // register a listener to handle transport 'findNode' events 150 | self.transport.on('findNode', function (nodeId, sender, callback) { 151 | if (self.tracing) 152 | self.trace("on 'findNode' - nodeId: " + nodeId + ", sender: " + util.inspect(sender)); 153 | // check if nodeId is one of the locally registered nodes 154 | var localContactKBucket = self.kBuckets[nodeId]; 155 | if (localContactKBucket) { 156 | callback(null, localContactKBucket.contact); 157 | } 158 | 159 | var closestKBuckets = self.getClosestKBuckets(nodeId); 160 | var closestContacts = self.getClosestContacts(nodeId, closestKBuckets); 161 | 162 | // add the sender if it exists 163 | if (sender) { 164 | // we do it only after we already got the closest contact to prevent 165 | // always responding with exact match to the sender if the sender is 166 | // announcing (searching for itself) 167 | 168 | // first, check if sender contact id is locally registered 169 | if (self.kBuckets[sender.id]) { 170 | // sender is the same as locally registered contact 171 | // need to arbiter which contact version should be retained 172 | self.kBuckets[sender.id].contact = 173 | self.arbiter(self.kBuckets[sender.id].contact, sender); 174 | } else { 175 | var senderClosestKBuckets = self.getClosestKBuckets(sender.id); 176 | if (senderClosestKBuckets.length == 0) { 177 | if (self.tracing) 178 | self.trace('no kBuckets for findNode sender ' 179 | + util.inspect(sender)); 180 | } else { 181 | var senderClosestKBucketId = senderClosestKBuckets[0].id; 182 | var senderClosestKBucket = 183 | self.kBuckets[senderClosestKBucketId].kBucket; 184 | if (!senderClosestKBucket) { 185 | if (self.tracing) 186 | self.trace('no closest kBucket for findNode sender ' 187 | + util.inspect(sender)); 188 | } else { 189 | var clonedSender = clone(sender); 190 | if (self.tracing) 191 | self.trace('adding ' + util.inspect(clonedSender) 192 | + ' to kBucket ' + senderClosestKBucketId); 193 | clonedSender.id = new Buffer(clonedSender.id, "base64"); 194 | senderClosestKBucket.add(clonedSender); 195 | } 196 | } 197 | } 198 | } 199 | 200 | // check if we already responded prior to processing the sender 201 | if (localContactKBucket) 202 | return; 203 | 204 | if (closestContacts.length == 0) { 205 | return callback(null, []); 206 | } 207 | 208 | // check for exact match 209 | if (closestContacts[0].id.toString("base64") == nodeId) { 210 | var contact = clone(closestContacts[0]); 211 | contact.id = contact.id.toString("base64"); 212 | return callback(null, contact); 213 | } 214 | 215 | // return closest contacts 216 | var contacts = []; 217 | closestContacts.forEach(function (closeContact) { 218 | var contact = clone(closeContact); 219 | contact.id = contact.id.toString("base64"); 220 | // hide implementation details 221 | delete contact.distance; 222 | contacts.push(contact); 223 | }); 224 | 225 | return callback(null, contacts); 226 | }); 227 | 228 | // register a listener to handle transport 'ping' events 229 | self.transport.on('ping', function (nodeId, sender, callback) { 230 | if (self.tracing) 231 | self.trace("on 'ping' - nodeId: " + nodeId + ", sender: " + util.inspect(sender)); 232 | 233 | // add the sender 234 | // in contrast to self.transport.on('findNode', ...) we can add the 235 | // sender prior to responding because it is verifying a ping for a 236 | // specific node and not searching for one, the 'findNode' consideration 237 | // to update K-Buckets only after responding does not apply in this case 238 | var senderClosestKBuckets = self.getClosestKBuckets(sender.id); 239 | if (senderClosestKBuckets.length == 0) { 240 | if (self.tracing) 241 | self.trace('no kBuckets for ping sender ' + util.inspect(sender)); 242 | } else { 243 | var senderClosestKBucketId = senderClosestKBuckets[0].id; 244 | var senderClosestKBucket = self.kBuckets[senderClosestKBucketId].kBucket; 245 | if (!senderClosestKBucket) { 246 | if (self.tracing) 247 | self.trace('no closest kBucket for ping sender ' + util.inspect(sender)); 248 | } else { 249 | var clonedSender = clone(sender); 250 | if (self.tracing) 251 | self.trace('adding ' + util.inspect(clonedSender) + ' to kBucket ' + senderClosestKBucketId); 252 | clonedSender.id = new Buffer(clonedSender.id, "base64"); 253 | senderClosestKBucket.add(clonedSender); 254 | } 255 | } 256 | 257 | // check if nodeId is one of the locally registered nodes 258 | var localContactKBucket = self.kBuckets[nodeId]; 259 | if (localContactKBucket) { 260 | callback(null, localContactKBucket.contact); 261 | } else { 262 | // the nodeId is not one of the locally registered nodes, ping fail 263 | callback(true); 264 | } 265 | }); 266 | 267 | // register a listener to handle transport 'reached' events 268 | self.transport.on('reached', function (contact) { 269 | if (self.tracing) 270 | self.trace('reached ' + util.inspect(contact)); 271 | 272 | // we successfully reached a contact, add it (refreshes the contact) 273 | self.add(contact); 274 | }); 275 | 276 | // register a listener to handle transport 'unreachable' events 277 | self.transport.on('unreachable', function (contact) { 278 | self.unreachable(contact); 279 | }); 280 | 281 | self.kBuckets = {}; 282 | if (self.noCache) { 283 | self.lruCache = { 284 | del: function () {}, 285 | get: function () {}, 286 | set: function () {}, 287 | }; 288 | } else { 289 | self.lruCache = LruCache(self.maxCacheSize); 290 | } 291 | self.timers = { 292 | 'find.ms': {}, 293 | 'find.request.ms': {}, 294 | 'find.round.ms': {} 295 | }; 296 | }; 297 | 298 | util.inherits(Discover, events.EventEmitter); 299 | 300 | /* 301 | * `remoteContact`: _Object_ Contact object to add that is not managed by this 302 | Discover node. 303 | * `id`: _String (base64)_ The contact id, base64 encoded. 304 | * `data`: _Any_ Data to be included with the contact, it is guaranteed to be 305 | returned for anyone querying for this `contact` by `id`. 306 | * Return: _Object_ Contact that was added. 307 | */ 308 | Discover.prototype.add = function add (remoteContact) { 309 | var self = this; 310 | 311 | if (!remoteContact.id || typeof remoteContact.id !== 'string') { 312 | throw new Error("Invalid or missing contact.id"); 313 | } 314 | 315 | // even if we don't have kBuckets to update, we can still store information 316 | // in LRU cache (check using arbiter to update cache with latest only) 317 | var cached = self.lruCache.get(remoteContact.id); 318 | var selection = self.arbiter(cached, remoteContact); 319 | if (selection !== cached) { 320 | self.lruCache.set(remoteContact.id, remoteContact); 321 | } 322 | 323 | if (Object.keys(self.kBuckets).length == 0) { 324 | return null; // no k-buckets to update 325 | } 326 | 327 | // first, check if remote contact id is locally registered 328 | if (self.kBuckets[remoteContact.id]) { 329 | // remote contact id is same as locally registered contact id 330 | // need to arbiter which contact version should be retained 331 | // (we already calculated the selection) 332 | self.kBuckets[remoteContact.id].contact = selection; 333 | } else { 334 | // we pick the closest kBucket to the node id of our contact to store the 335 | // data in, since they have the most space to accomodate near-by node ids 336 | // (inherent KBucket property) 337 | var closestKBuckets = self.getClosestKBuckets(remoteContact.id); 338 | var closestKBucketId = closestKBuckets[0].id; 339 | var closestKBucket = self.kBuckets[closestKBucketId].kBucket; 340 | var clonedContact = clone(remoteContact); 341 | if (self.tracing) { 342 | self.trace('adding ' + util.inspect(clonedContact) + ' to kBucket ' 343 | + closestKBucketId); 344 | } 345 | // convert id from string to Buffer 346 | clonedContact.id = new Buffer(clonedContact.id, "base64"); 347 | closestKBucket.add(clonedContact); 348 | } 349 | 350 | return remoteContact; 351 | }; 352 | 353 | /* 354 | * `query`: _Object_ Object containing query state for this request. 355 | * `nodeId`: _String (base64)_ Base64 encoded node id to find. 356 | * `nodes`: _Array_ `contact`s to query for `nodeId` arranged from closest to 357 | farthest. 358 | * `node`: 359 | * `id`: _String (base64)_ Base64 encoded contact id. 360 | * `nodesMap`: _Object_ A map to the same `contact`s already present in 361 | `nodes` for O(1) access. 362 | * `callback`: _Function_ The callback to call with result. 363 | */ 364 | Discover.prototype.executeQuery = function executeQuery (query, callback) { 365 | var self = this; 366 | self.timerStart('find.round.ms', query.nodeId); 367 | 368 | if (self.tracing) 369 | self.trace('executeQuery(' + util.inspect(query, false, null) + ')'); 370 | 371 | // query already successfully completed 372 | if (query.done) 373 | return; 374 | 375 | // if we have no nodes, we can't query anything 376 | if (!query.nodes || query.nodes.length == 0) { 377 | var latency = self.timerEndInMilliseconds('find.ms', query.nodeId); 378 | var roundLatency = self.timerEndInMilliseconds('find.round.ms', query.nodeId); 379 | callback(new Error("No known nodes to query")); 380 | self.emit('stats.timers.find.ms', latency); 381 | self.emit('stats.timers.find.round.ms', roundLatency); 382 | return; 383 | } 384 | 385 | if (query.index === undefined) 386 | query.index = 0; 387 | if (query.closest === undefined) 388 | query.closest = query.nodes[0]; 389 | if (query.ongoingRequests === undefined) 390 | query.ongoingRequests = 0; 391 | if (query.newNodes === undefined) 392 | query.newNodes = []; 393 | 394 | // we listen for `node` events that contain the nodeId we asked for 395 | // this helps to decouple discover from the transport and allows us to 396 | // benefit from other ongoing queries (TODO: "prove" this) 397 | // 398 | // because executeQuery can be called multiple times on the same query, 399 | // we keep the state 400 | if (!query.listener) { 401 | // TODO: maybe there is an opportunity here to generate events 402 | // uniquely named by "nodeId" so I don't have to have tons of listeners 403 | // listen to everything and throw away what they don't want? 404 | query.listener = function (error, contact, nodeId, response) { 405 | // filter other queries 406 | if (nodeId != query.nodeId) 407 | return; 408 | 409 | // query already successfully completed 410 | if (query.done) 411 | return; 412 | 413 | // request has been handled 414 | // TODO: what happens if two requests for the same nodeId are 415 | // happening at the same time? 416 | // maybe do a check prior to executeQuery to not duplicate searches 417 | // for the same nodeId across the network? 418 | query.ongoingRequests--; 419 | 420 | if (error) { 421 | if (self.tracing) { 422 | self.trace('error response from ' + util.inspect(contact) + 423 | ' looking for ' + nodeId + ': ' + util.inspect(error)); 424 | } 425 | var contactRecord = query.nodesMap[contact.id]; 426 | 427 | if (!contactRecord) 428 | return; 429 | 430 | if (contactRecord.kBucket) { 431 | // we have a kBucket to report unreachability to 432 | // remove from kBucket 433 | var kBucketInfo = self.kBuckets[contactRecord.kBucket.id]; 434 | if (!kBucketInfo) { 435 | return; 436 | } 437 | 438 | var kBucket = kBucketInfo.kBucket; 439 | if (!kBucket) { 440 | return; 441 | } 442 | 443 | var contactRecordToRemove = clone(contactRecord); 444 | contactRecordToRemove.id = 445 | new Buffer(contactRecord.id, 'base64'); 446 | kBucket.remove(contactRecordToRemove); 447 | } 448 | 449 | contactRecord.contacted = true; 450 | 451 | // console.dir(query); 452 | 453 | // initiate next request if there are still queries to be made 454 | if (query.index < query.nodes.length 455 | && query.ongoingRequests < self.CONCURRENCY_CONSTANT) { 456 | process.nextTick(function () { 457 | self.executeQuery(query, callback); 458 | }); 459 | } else { 460 | self.queryCompletionCheck(query, callback); 461 | } 462 | return; // handled error 463 | } 464 | 465 | // we have a response, it could be an Object or Array 466 | 467 | if (self.tracing) { 468 | self.trace('response from ' + util.inspect(contact) + 469 | ' looking for ' + nodeId + ': ' + util.inspect(response)); 470 | } 471 | if (Array.isArray(response)) { 472 | // add the closest contacts to new nodes 473 | query.newNodes = query.newNodes.concat(response); 474 | 475 | // TODO: same code inside error handler 476 | // initiate next request if there are still queries to be made 477 | if (query.index < query.nodes.length 478 | && query.ongoingRequests < self.CONCURRENCY_CONSTANT) { 479 | process.nextTick(function () { 480 | self.executeQuery(query, callback); 481 | }); 482 | } else { 483 | self.queryCompletionCheck(query, callback); 484 | } 485 | return; 486 | } 487 | 488 | // we have a response Object, found the contact! 489 | // add the new contact to the closestKBucket 490 | var finalClosestKBuckets = self.getClosestKBuckets(response.id); 491 | if (finalClosestKBuckets.length > 0) { 492 | var finalClosestKBucket = 493 | self.kBuckets[finalClosestKBuckets[0].id].kBucket; 494 | var contact = clone(response); 495 | contact.id = new Buffer(contact.id, "base64"); 496 | finalClosestKBucket.add(contact); 497 | } 498 | 499 | // return the response and stop querying 500 | var latency = self.timerEndInMilliseconds('find.ms', nodeId); 501 | var roundLatency = self.timerEndInMilliseconds('find.round.ms', nodeId); 502 | callback(null, response); 503 | query.done = true; 504 | self.transport.removeListener('node', query.listener); 505 | self.emit('stats.timers.find.ms', latency); 506 | self.emit('stats.timers.find.round.ms', roundLatency); 507 | return; 508 | }; 509 | self.transport.on('node', query.listener); 510 | } 511 | 512 | for (query.index = query.index || 0; 513 | query.index < query.nodes.length 514 | && query.ongoingRequests < self.CONCURRENCY_CONSTANT; 515 | query.index++) { 516 | 517 | query.ongoingRequests++; 518 | self.timerStart('find.request.ms', query.nodes[query.index].id + query.nodeId); 519 | self.transport.findNode(query.nodes[query.index], query.nodeId, query.sender); 520 | } 521 | 522 | // console.log("INSIDE EXECUTE QUERY"); 523 | // console.dir(query); 524 | self.queryCompletionCheck(query, callback); 525 | }; 526 | 527 | /* 528 | * `nodeId`: _String (base64)_ The node id to find, base64 encoded. 529 | * `callback`: _Function_ The callback to call with the result of searching for 530 | `nodeId`. 531 | * `announce`: _Object_ _(Default: undefined)_ _**CAUTION: reserved for 532 | internal use**_ Contact object, if specified, it indicates an 533 | announcement to the network so we ask the network instead of 534 | satisfying request locally and the sender is the `announce` contact 535 | object. 536 | */ 537 | Discover.prototype.find = function find (nodeId, callback, announce) { 538 | var self = this; 539 | self.timerStart('find.ms', nodeId); 540 | 541 | var traceHeader; 542 | 543 | if (self.tracing) { 544 | traceHeader = "find(" + nodeId + "): "; 545 | } 546 | 547 | // see if we have a local match, and return it if not announcing 548 | if (!announce && self.kBuckets[nodeId]) { 549 | var latency = self.timerEndInMilliseconds('find.ms', nodeId); 550 | callback(null, self.kBuckets[nodeId].contact); 551 | self.emit('stats.timers.find.ms', latency); 552 | return; 553 | } 554 | 555 | // see if we have a cache match, and return it if not announcing 556 | var cached = self.lruCache.get(nodeId); 557 | if (!announce && cached) { 558 | var latency = self.timerEndInMilliseconds('find.ms', nodeId); 559 | callback(null, cached); 560 | self.emit('stats.timers.find.ms', latency); 561 | return; 562 | } 563 | 564 | // if we have no kBuckets, that means we haven't registered any nodes yet 565 | // the only nodes we are aware of are the seed nodes 566 | if (Object.keys(self.kBuckets).length == 0) { 567 | if (self.tracing) 568 | self.trace(traceHeader + 'no kBuckets, delegating to findViaSeeds()'); 569 | return self.findViaSeeds(nodeId, callback, announce); 570 | } 571 | 572 | var closestKBuckets = self.getClosestKBuckets(nodeId); 573 | 574 | if (self.tracing) { 575 | self.trace(traceHeader + 'have ' + closestKBuckets.length + ' kBuckets'); 576 | self.trace(traceHeader + 'kBuckets: ' + util.inspect(closestKBuckets, false, null)); 577 | } 578 | 579 | var closestContacts = self.getClosestContacts(nodeId, closestKBuckets); 580 | 581 | // if none of our local kBuckets have any contacts (should only happen 582 | // when bootstrapping), talk to the seeds 583 | if (closestContacts.length == 0) { 584 | if (self.tracing) 585 | self.trace(traceHeader + 'no contacts in kBuckets, delegating to findViaSeeds()'); 586 | return self.findViaSeeds(nodeId, callback, announce); 587 | } 588 | 589 | if (self.tracing) 590 | self.trace(traceHeader + 'have ' + closestContacts.length + ' closest contacts'); 591 | 592 | // check if the closest contact is actually the node we are looking for 593 | if (closestContacts[0].id.toString("base64") == nodeId) { 594 | var contact = clone(closestContacts[0]); 595 | contact.id = contact.id.toString("base64"); 596 | // hide internal implementation details 597 | delete contact.distance; 598 | var latency = self.timerEndInMilliseconds('find.ms', nodeId); 599 | callback(null, contact); 600 | self.emit('stats.timers.find.ms', latency); 601 | return; 602 | } 603 | 604 | // closestContacts will contain contacts with id as a Buffer, we clone 605 | // the contacts so that we can have id be a base64 encoded String 606 | var closestNodes = []; 607 | var nodesMap = {}; 608 | closestContacts.forEach(function (contact) { 609 | var clonedContact = clone(contact); 610 | clonedContact.id = clonedContact.id.toString("base64"); 611 | clonedContact.kBucket = closestKBuckets[0]; // for reference later 612 | closestNodes.push(clonedContact); 613 | nodesMap[clonedContact.id] = clonedContact; 614 | }); 615 | 616 | var query = { 617 | nodeId: nodeId, 618 | nodes: closestNodes, 619 | nodesMap: nodesMap, 620 | sender: self.kBuckets[closestKBuckets[0].id].contact 621 | }; 622 | self.executeQuery(query, callback); 623 | }; 624 | 625 | /* 626 | * `nodeId`: _String (base64)_ Base64 encoded node id to find. 627 | * `callback`: _Function_ The callback to call with the result of searching for 628 | `nodeId`. 629 | * `announce`: _Object_ _(Default: undefined)_ Contact object, if specified, it 630 | indicates an announcement and the sender is the `announce` contact 631 | object. 632 | */ 633 | Discover.prototype.findViaSeeds = function findViaSeeds (nodeId, callback, announce) { 634 | var self = this; 635 | var traceHeader = "findViaSeeds(" + nodeId + "): "; 636 | 637 | // if we have no seeds, that means we don't know of any other nodes to query 638 | if (!self.seeds || self.seeds.length == 0) { 639 | if (self.tracing) { 640 | self.trace(traceHeader + 'No known seeds to query'); 641 | } 642 | var latency = self.timerEndInMilliseconds('find.ms', nodeId); 643 | callback(new Error("No known seeds to query")); 644 | self.emit('stats.timers.find.ms', latency); 645 | return; 646 | } 647 | 648 | var closestNodes = []; 649 | var nodesMap = {}; 650 | var nodeIdBuffer = new Buffer(nodeId, "base64"); 651 | self.seeds.forEach(function (seed) { 652 | var seedIdBuffer = new Buffer(seed.id, "base64"); 653 | var clonedSeed = clone(seed); 654 | clonedSeed.distance = KBucket.distance(nodeIdBuffer, seedIdBuffer); 655 | closestNodes.push(clonedSeed); 656 | nodesMap[clonedSeed.id] = clonedSeed; 657 | }); 658 | 659 | closestNodes = closestNodes.sort(function (a, b) { 660 | return a.distance - b.distance; 661 | }); 662 | 663 | // distances are now sorted, closest being first 664 | // TODO: probably refactor Query object to be it's own thing 665 | var query = { 666 | nodeId: nodeId, 667 | nodes: closestNodes, 668 | nodesMap: nodesMap, 669 | sender: announce 670 | }; 671 | self.executeQuery(query, callback); 672 | }; 673 | 674 | /* 675 | * `nodeId`: _String (base64)_ Base64 encoded node id to find closest contacts 676 | to. 677 | * `closestKBuckets`: _Array_ Sorted array of `KBucket`s from closest to 678 | furthest from `nodeId`. 679 | * Return: _Array_ List of closest contacts. 680 | */ 681 | Discover.prototype.getClosestContacts = function getClosestContacts (nodeId, closestKBuckets) { 682 | var self = this; 683 | 684 | // we pick the closest kBucket to chose nodes from as it will have the 685 | // most information about nodes closest to nodeId, if closest kBucket has 686 | // no nodes, we pick the next one, until we find a kBucket with nodes in it 687 | // or reach the end 688 | var closestContacts = []; 689 | var closestKBucketsIndex = 0; 690 | // TODO: it is possible for closestKBucket.closest({...}, 3) to return 691 | // less than 3 contacts, in that case a list of less than 3 will 692 | // be returned, changing while condition to use 693 | // `closestContacts.length < 3` may be a bette heuristic choice 694 | while (closestContacts.length == 0 695 | && closestKBucketsIndex < closestKBuckets.length) { 696 | var closestKBucketId = closestKBuckets[closestKBucketsIndex].id; 697 | var closestKBucket = self.kBuckets[closestKBucketId].kBucket; 698 | // get three closest nodes 699 | closestContacts = closestKBucket.closest( 700 | {id: new Buffer(nodeId, "base64")}, 3); 701 | closestKBucketsIndex++; 702 | } 703 | 704 | return closestContacts; 705 | }; 706 | 707 | /* 708 | * `nodeId`: _String (base64)_ Base64 encoded node id to find closest contacts 709 | to. 710 | * Return: _Array_ List of closest `KBucket`s. 711 | */ 712 | Discover.prototype.getClosestKBuckets = function getClosestKBuckets (nodeId) { 713 | var self = this; 714 | 715 | // TODO: change self.kBuckets data structure so that this operation is 716 | // O(log n) instead of O(n), although, in proritizing this task 717 | // remember the use-case of having more than one kBucket in self.kBuckets 718 | // means that we are doing discover.register(contact) quite many times. 719 | // It seems that for *most* use-cases, only one contact will ever be 720 | // `discover.register`'ed (this registers identity of the local node) 721 | var closestKBuckets = []; 722 | var nodeIdBuffer = new Buffer(nodeId, "base64"); 723 | Object.keys(self.kBuckets).forEach(function (kBucketKey) { 724 | var kBucket = self.kBuckets[kBucketKey]; 725 | var kBucketIdBuffer = new Buffer(kBucket.id, "base64"); 726 | closestKBuckets.push({ 727 | id: kBucket.id, 728 | distance: KBucket.distance(nodeIdBuffer, kBucketIdBuffer) 729 | }); 730 | }); 731 | 732 | closestKBuckets = closestKBuckets.sort(function (a, b) { 733 | return a.distance - b.distance; 734 | }); 735 | 736 | return closestKBuckets; 737 | }; 738 | 739 | /* 740 | * `query`: _Object_ Object containing query state for this request. 741 | * `nodeId`: _String (base64)_ Base64 encoded node id to find. 742 | * `nodes`: _Array_ `contact`s to query for `nodeId` arranged from closest to 743 | furthest. 744 | * `node`: 745 | * `id`: _String (base64)_ Base64 encoded contact id. 746 | * `nodesMap`: _Object_ A map to the same `contact`s already present in 747 | `nodes` for O(1) access. 748 | * `callback`: _Function_ The callback to call with result. 749 | */ 750 | Discover.prototype.queryCompletionCheck = function queryCompletionCheck (query, callback) { 751 | var self = this; 752 | // console.log("QUERY COMPLETION CHECK"); 753 | // are we done? 754 | if (query.index == query.nodes.length 755 | && query.ongoingRequests == 0 && !query.done) { 756 | // find out if any new nodes are closer than the closest 757 | // node in order to determine if we should keep going or 758 | // stop 759 | self.emit('stats.timers.find.round.ms', 760 | self.timerEndInMilliseconds('find.round.ms', query.nodeId)); 761 | 762 | // console.log('sorting new nodes'); 763 | // sort the new nodes according to distance 764 | var newNodes = []; 765 | var nodeIdBuffer = new Buffer(query.nodeId, "base64"); 766 | query.newNodes.forEach(function (newNode) { 767 | var clonedNewNode = clone(newNode); 768 | var newNodeIdBuffer = new Buffer(newNode.id, "base64"); 769 | clonedNewNode.distance = 770 | KBucket.distance(nodeIdBuffer, newNodeIdBuffer); 771 | // only add nodes that are closer to short circuit the 772 | // computation 773 | if (clonedNewNode.distance < query.closest.distance) { 774 | newNodes.push(clonedNewNode); 775 | } 776 | }); 777 | 778 | // console.log('sorted new nodes'); 779 | 780 | // if we don't have any closer nodes, we didn't find 781 | // what we are looking for 782 | if (newNodes.length == 0) { 783 | // we are done done 784 | self.transport.removeListener('node', query.listener); 785 | 786 | // console.log('listener removed'); 787 | // console.dir(query); 788 | // sanity check, just in case closest node is the one 789 | // we are looking for and wasn't short-circuited 790 | // somewhere else 791 | if (query.closest.id == query.nodeId) { 792 | // console.log("returning closest node", query.closest); 793 | return callback(null, query.closest); 794 | } else { 795 | // console.log("returning not found error"); 796 | var latency = self.timerEndInMilliseconds('find.ms', query.nodeId); 797 | callback(new Error("not found")); 798 | self.emit('stats.timers.find.ms', latency); 799 | return; 800 | } 801 | } 802 | 803 | // console.log("found closer nodes", newNodes); 804 | 805 | // found closer node, sort according to length 806 | newNodes = newNodes.sort(function (a, b) { 807 | return a.distance - b.distance; 808 | }); 809 | 810 | // update query state and go another round 811 | query.index = 0; 812 | query.ongoingRequests = 0; 813 | query.nodes = newNodes; // these are sorted by distance (unlike query.newNodes) 814 | query.nodesMap = {}; 815 | query.nodes.forEach(function (node) { 816 | query.nodesMap[node.id] = node; 817 | }); 818 | query.closest = query.nodes[0]; 819 | query.newNodes = []; 820 | 821 | return process.nextTick(function () { 822 | self.executeQuery(query, callback); 823 | }); 824 | } // are we done? 825 | // console.log("FAILED QUERY COMPLETION CHECK >>> KEEP GOING"); 826 | }; 827 | 828 | /* 829 | * `contact`: _Object_ Contact object to register. 830 | * `id`: _String (base64)_ _(Default: `crypto.randomBytes(20).toString('base64'`)_ 831 | The contact id, base 64 encoded; will be created if not present. 832 | * `data`: _Any_ Data to be included with the contact, it is guaranteed to be 833 | returned for anyone querying for this `contact` by `id`. 834 | * Return: _Object_ Contact that was registered with `id` and generated arbiter 835 | defaults if necessary. 836 | */ 837 | Discover.prototype.register = function register (contact) { 838 | var self = this; 839 | contact = contact || {}; 840 | contact = clone(contact); // separate references from outside 841 | 842 | if (!contact.id) { 843 | contact.id = crypto.randomBytes(20).toString('base64'); 844 | } 845 | 846 | // add transport information to the stored contact 847 | contact = self.transport.setTransportInfo(contact); 848 | 849 | contact = self.arbiterDefaults(contact); 850 | 851 | var traceHeader = "register(" + contact.id + "): "; 852 | 853 | if (!self.kBuckets[contact.id]) { 854 | if (self.tracing) 855 | self.trace(traceHeader + 'creating new bucket for ' + util.inspect(contact)); 856 | var kBucket = new KBucket({arbiter: self.arbiter, localNodeId: contact.id}); 857 | kBucket.on('ping', function (oldContacts, newContact) { 858 | // ping all the old contacts and if one does not respond, remove it 859 | var oldContactIdsBase64 = []; 860 | var reachedContactIdsBase64 = []; 861 | var unreachableListener = function (contact) { 862 | if (oldContactIdsBase64.indexOf(contact.id) > -1) { 863 | self.transport.removeListener('unreachable', unreachableListener); 864 | self.transport.removeListener('reached', reachedListener); 865 | kBucket.remove({id: new Buffer(contact.id, "base64")}); 866 | kBucket.add(newContact); 867 | } 868 | }; 869 | var reachedListener = function (contact) { 870 | var index = reachedContactIdsBase64.indexOf(contact.id); 871 | if (index > -1) { 872 | reachedContactIdsBase64.splice(index, 1); 873 | if (reachedContactIdsBase64.length == 0) { 874 | // all contacts were reached, won't be adding new one 875 | self.transport.removeListener( 876 | 'unreachable', unreachableListener); 877 | self.transport.removeListener( 878 | 'reached', reachedListener); 879 | } 880 | } 881 | }; 882 | self.transport.on('reached', reachedListener); 883 | self.transport.on('unreachable', unreachableListener); 884 | var sender = self.kBuckets[contact.id].contact; 885 | oldContacts.forEach(function (oldContact) { 886 | var contact = clone(oldContact); 887 | contact.id = oldContact.id.toString("base64"); 888 | oldContactIdsBase64.push(contact.id); 889 | reachedContactIdsBase64.push(contact.id); 890 | self.transport.ping(contact, sender); 891 | }); 892 | }); 893 | self.kBuckets[contact.id] = { 894 | contact: contact, 895 | id: contact.id, 896 | kBucket: kBucket 897 | }; 898 | } else { 899 | if (self.tracing) 900 | self.trace(traceHeader + 'bucket already exists, updating contact with ' + util.inspect(contact)); 901 | self.kBuckets[contact.id].contact = contact; 902 | } 903 | 904 | // issuing find(contact.id) against own contact.id, populates the DHT 905 | // with contact 906 | self.find(contact.id, function () {}, contact /*announce*/); 907 | 908 | return clone(contact); // don't leak internal implementation 909 | }; 910 | 911 | /* 912 | * `type`: _String_ Timer type. 913 | * `key`: _String_ Timer key. 914 | * Return: _Number_ Milliseconds since the first time in the timer. 915 | */ 916 | Discover.prototype.timerEndInMilliseconds = function timerEndInMilliseconds(type, key) { 917 | var self = this; 918 | 919 | if (!self.timers[type] || !self.timers[type][key]) { 920 | return 0; 921 | } 922 | 923 | var diff = process.hrtime(self.timers[type][key].shift()); 924 | if (self.timers[type][key].length == 0) { 925 | delete self.timers[type][key]; 926 | } 927 | // diff[0] : seconds 928 | // diff[1] : nanoseconds 929 | // 1 second = 1e9 nanoseconds 930 | // 1 millisecond = 1e6 nanoseconds 931 | return Math.floor((diff[0] * 1e9 + diff[1]) / 1e6); 932 | }; 933 | 934 | /* 935 | * `type`: _String_ Timer type. 936 | * `key`: _String_ Timer key. 937 | */ 938 | Discover.prototype.timerStart = function timerStart(type, key) { 939 | var self = this; 940 | self.timers[type][key] = self.timers[type][key] || []; 941 | self.timers[type][key].push(process.hrtime()); 942 | }; 943 | 944 | /* 945 | * `message`: _String_ Message to trace. 946 | */ 947 | Discover.prototype.trace = function(message) { 948 | var self = this; 949 | var options = self.options; 950 | 951 | if (options.inlineTrace) 952 | console.log('~trace', message); 953 | if (options.eventTrace) 954 | self.emit('~trace', message); 955 | }; 956 | 957 | /* 958 | * `contact`: _Object_ Contact object to report unreachable 959 | * `id`: _String (base64)_ The previously registered contact id, base64 960 | encoded. 961 | */ 962 | Discover.prototype.unreachable = function unreachable (contact) { 963 | var self = this; 964 | if (self.tracing) 965 | self.trace('unreachable(' + util.inspect(contact) + ')'); 966 | 967 | // even if we don't have kBuckets, remove contact from LRU cache 968 | self.lruCache.del(contact.id); 969 | 970 | // find closest KBucket to remove unreachable contact from 971 | var closestKBuckets = self.getClosestKBuckets(contact.id); 972 | if (closestKBuckets.length == 0) { 973 | if (self.tracing) 974 | self.trace('no kBuckets for unreachable(contact) ' + util.inspect(contact)); 975 | return; 976 | } 977 | var closestKBucketId = closestKBuckets[0].id; 978 | var closestKBucket = self.kBuckets[closestKBucketId].kBucket; 979 | if (!closestKBucket) { 980 | if (self.tracing) 981 | self.trace('no closest kBucket for unreachable(contact) ' + util.inspect(contact)); 982 | return; 983 | } 984 | var clonedContact = clone(contact); 985 | if (self.tracing) 986 | self.trace('removing ' + util.inspect(contact) + ' from kBucket ' + closestKBucketId); 987 | clonedContact.id = new Buffer(contact.id, "base64"); 988 | closestKBucket.remove(clonedContact); 989 | }; 990 | 991 | /* 992 | * `contact`: _Object_ Contact object to register 993 | * `id`: _String (base64)_ The previously registered contact id, base 64 994 | encoded. 995 | */ 996 | Discover.prototype.unregister = function unregister (contact) { 997 | var self = this; 998 | var kBucket = self.kBuckets[contact.id]; 999 | if (kBucket) { 1000 | if (kBucket.contact === self.arbiter(kBucket.contact, contact) 1001 | && kBucket.contact !== contact) { 1002 | // by returning the stored contact, arbiter determined that 1003 | // unregister should fail as an attempt to unregister using 1004 | // old data has been made 1005 | return; 1006 | } 1007 | delete self.kBuckets[contact.id]; 1008 | } 1009 | 1010 | // current implemenation deletes all that "closest" contact information 1011 | // that was gathered in the unregistering kBucket 1012 | 1013 | // TODO: elaborate the implementation to distribute known nodes in this 1014 | // kBucket to ones that aren't being deleted 1015 | }; 1016 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "discover", 3 | "version": "0.4.3", 4 | "description": "Node discovery based on Kademlia DHT protocol", 5 | "scripts": { 6 | "localtest": "node scripts/localtest.js", 7 | "test": "node scripts/test.js" 8 | }, 9 | "main": "index.js", 10 | "devDependencies": { 11 | "nodeunit": "0.8.x" 12 | }, 13 | "dependencies": { 14 | "clone": "0.1.11", 15 | "discover-tcp-transport": "4.0.1", 16 | "k-bucket": "0.4.0", 17 | "lru-cache": "2.5.0" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "git@github.com:tristanls/discover.git" 22 | }, 23 | "keywords": [ 24 | "discover", 25 | "discovery", 26 | "node", 27 | "kademlia", 28 | "dht" 29 | ], 30 | "contributors": [ 31 | "Tristan Slominski ", 32 | "Mike de Boer ", 33 | "Eli Skeggs " 34 | ], 35 | "license": "MIT" 36 | } 37 | -------------------------------------------------------------------------------- /scripts/localtest.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | // run 5 nodes in the local environment and display trace info 3 | 4 | var assert = require('assert'), 5 | crypto = require('crypto'), 6 | Discover = require('../index.js'), 7 | TcpTransport = require('discover-tcp-transport'), 8 | util = require('util'); 9 | 10 | // TODO: maybe fix flow control, being really really lazy here with code layout :) 11 | var transport1, transport2, transport3, transport4, transport5; 12 | var discover1, discover2, discover3, discover4, discover5; 13 | var id1, id2, id3, id4, id5; 14 | 15 | transport1 = TcpTransport.listen({port: 6741}, function () { 16 | transport2 = TcpTransport.listen({port: 6742}, function () { 17 | transport3 = TcpTransport.listen({port: 6743}, function () { 18 | transport4 = TcpTransport.listen({port: 6744}, function () { 19 | transport5 = TcpTransport.listen({port: 6745}, function () { 20 | startLocalTest(); 21 | }); 22 | }); 23 | }); 24 | }); 25 | }); 26 | 27 | function startLocalTest() { 28 | 29 | id1 = crypto.randomBytes(20).toString('base64'); 30 | id2 = crypto.randomBytes(20).toString('base64'); 31 | id3 = crypto.randomBytes(20).toString('base64'); 32 | id4 = crypto.randomBytes(20).toString('base64'); 33 | id5 = crypto.randomBytes(20).toString('base64'); 34 | 35 | discover1 = new Discover({ 36 | inlineTrace: true, 37 | seeds: [ 38 | { 39 | id: id1, 40 | data: 'discover1', 41 | transport: { 42 | host: 'localhost', 43 | port: 6741 44 | } 45 | }, 46 | { 47 | id: id2, 48 | data: 'discover2', 49 | transport: { 50 | host: 'localhost', 51 | port: 6742 52 | } 53 | }, 54 | { 55 | id: id3, 56 | data: 'discover3', 57 | transport: { 58 | host: 'localhost', 59 | port: 6743 60 | } 61 | } 62 | ], 63 | transport: transport1}); 64 | discover2 = new Discover({ 65 | inlineTrace: true, 66 | seeds: [ 67 | { 68 | id: id1, 69 | data: 'discover1', 70 | transport: { 71 | host: 'localhost', 72 | port: 6741 73 | } 74 | }, 75 | { 76 | id: id2, 77 | data: 'discover2', 78 | transport: { 79 | host: 'localhost', 80 | port: 6742 81 | } 82 | }, 83 | { 84 | id: id3, 85 | data: 'discover3', 86 | transport: { 87 | host: 'localhost', 88 | port: 6743 89 | } 90 | } 91 | ], 92 | transport: transport2 93 | }); 94 | discover3 = new Discover({ 95 | inlineTrace: true, 96 | seeds: [ 97 | { 98 | id: id1, 99 | data: 'discover1', 100 | transport: { 101 | host: 'localhost', 102 | port: 6741 103 | } 104 | }, 105 | { 106 | id: id2, 107 | data: 'discover2', 108 | transport: { 109 | host: 'localhost', 110 | port: 6742 111 | } 112 | }, 113 | { 114 | id: id3, 115 | data: 'discover3', 116 | transport: { 117 | host: 'localhost', 118 | port: 6743 119 | } 120 | } 121 | ], 122 | transport: transport3 123 | }); 124 | discover4 = new Discover({ 125 | inlineTrace: true, 126 | seeds: [ 127 | { 128 | id: id1, 129 | data: 'discover1', 130 | transport: { 131 | host: 'localhost', 132 | port: 6741 133 | } 134 | }, 135 | { 136 | id: id2, 137 | data: 'discover2', 138 | transport: { 139 | host: 'localhost', 140 | port: 6742 141 | } 142 | }, 143 | { 144 | id: id3, 145 | data: 'discover3', 146 | transport: { 147 | host: 'localhost', 148 | port: 6743 149 | } 150 | } 151 | ], 152 | transport: transport4 153 | }); 154 | discover5 = new Discover({ 155 | inlineTrace: true, 156 | seeds: [ 157 | { 158 | id: id1, 159 | data: 'discover1', 160 | transport: { 161 | host: 'localhost', 162 | port: 6741 163 | } 164 | }, 165 | { 166 | id: id2, 167 | data: 'discover2', 168 | transport: { 169 | host: 'localhost', 170 | port: 6742 171 | } 172 | }, 173 | { 174 | id: id3, 175 | data: 'discover3', 176 | transport: { 177 | host: 'localhost', 178 | port: 6743 179 | } 180 | } 181 | ], 182 | transport: transport5 183 | }); 184 | 185 | console.log('~script five discover instances running'); 186 | console.log('~script starting self-registrations'); 187 | 188 | var node1 = {id: id1, data: 'discover1', transport: {host: 'localhost', port: 6741}}; 189 | var node2 = {id: id2, data: 'discover2', transport: {host: 'localhost', port: 6742}}; 190 | var node3 = {id: id3, data: 'discover3', transport: {host: 'localhost', port: 6743}}; 191 | var node4 = {id: id4, data: 'discover4', transport: {host: 'localhost', port: 6744}}; 192 | var node5 = {id: id5, data: 'discover5', transport: {host: 'localhost', port: 6745}}; 193 | 194 | discover1.register(node1); 195 | discover2.register(node2); 196 | discover3.register(node3); 197 | discover4.register(node4); 198 | discover5.register(node5); 199 | 200 | var discoverNodes = [discover1, discover2, discover3, discover4, discover5]; 201 | 202 | discoverNodes.forEach(function (discoverNode) { 203 | discoverNode.on('stats.timers.find.ms', function (latency) { 204 | console.log('~stats: stats.timers.find.ms ' + latency); 205 | }); 206 | discoverNode.on('stats.timers.find.round.ms', function (latency) { 207 | console.log('~stats: stats.timers.find.round.ms ' + latency); 208 | }); 209 | discoverNode.on('stats.timers.find.request.ms', function (latency) { 210 | console.log('~stats: stats.timers.find.request.ms ' + latency); 211 | }); 212 | }); 213 | 214 | console.log('~script self-registrations complete'); 215 | console.log('~script allowing nodes to communicate and settle'); 216 | 217 | setTimeout(continueLocalTest, 1000); 218 | }; 219 | 220 | function continueLocalTest() { 221 | console.log('~script listing node contents'); 222 | 223 | console.log('~script discover1:', util.inspect(discover1, false, null)); 224 | console.log('~script discover2:', util.inspect(discover2, false, null)); 225 | console.log('~script discover3:', util.inspect(discover3, false, null)); 226 | console.log('~script discover4:', util.inspect(discover4, false, null)); 227 | console.log('~script discover5:', util.inspect(discover5, false, null)); 228 | 229 | console.log('~script listing of node contents complete'); 230 | 231 | console.log('~script discover5 is asked to find discover4'); 232 | 233 | discover5.find(id4, function (error, contact) { 234 | 235 | console.log('~script find id4 response'); 236 | console.log('~script', error, util.inspect(contact, false, null)); 237 | 238 | console.log('~script listing node contents'); 239 | 240 | console.log('~script discover1:', util.inspect(discover1, false, null)); 241 | console.log('~script discover2:', util.inspect(discover2, false, null)); 242 | console.log('~script discover3:', util.inspect(discover3, false, null)); 243 | console.log('~script discover4:', util.inspect(discover4, false, null)); 244 | console.log('~script discover5:', util.inspect(discover5, false, null)); 245 | 246 | console.log('~script listing of node contents complete'); 247 | 248 | setTimeout(continueLocalTest2, 1000); 249 | }); 250 | }; 251 | 252 | var id6; 253 | 254 | function continueLocalTest2() { 255 | id6 = crypto.randomBytes(20).toString('base64'); 256 | var node6 = {id: id6, data: 'discover6', transport: {host: 'localhost', port: 6741}}; 257 | 258 | console.log('~script multiple nodes per discover instance'); 259 | 260 | discover1.register(node6); 261 | 262 | console.log('~script allowing nodes to communicate and settle'); 263 | 264 | setTimeout(continueLocalTest3, 1000); 265 | }; 266 | 267 | function continueLocalTest3() { 268 | console.log('~script retrieving node6 from discover5'); 269 | discover5.find(id6, function (error, contact) { 270 | assert.ok(!error); 271 | 272 | console.log('~script recevied contact: ' + util.inspect(contact, false, null)); 273 | 274 | console.log('~script second attempt should always be local (using cached value)'); 275 | discover5.find(id6, function (error, contact) { 276 | assert.ok(!error); 277 | console.log('~script received contact: ' + util.inspect(contact, false, null)); 278 | 279 | setTimeout(complete, 1000); 280 | }); 281 | }); 282 | }; 283 | 284 | function complete() { 285 | console.log('~script complete'); 286 | setTimeout(function () { process.exit(); }, 1000); 287 | }; -------------------------------------------------------------------------------- /scripts/test.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | var reporter = require('nodeunit').reporters.default; 3 | reporter.run(['test']); -------------------------------------------------------------------------------- /test/add.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | add.js - discover.add(remoteContact) test 4 | 5 | The MIT License (MIT) 6 | 7 | Copyright (c) 2014 Tristan Slominski 8 | 9 | Permission is hereby granted, free of charge, to any person 10 | obtaining a copy of this software and associated documentation 11 | files (the "Software"), to deal in the Software without 12 | restriction, including without limitation the rights to use, 13 | copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | copies of the Software, and to permit persons to whom the 15 | Software is furnished to do so, subject to the following 16 | conditions: 17 | 18 | The above copyright notice and this permission notice shall be 19 | included in all copies or substantial portions of the Software. 20 | 21 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 22 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 23 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 24 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 25 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 26 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 27 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 28 | OTHER DEALINGS IN THE SOFTWARE. 29 | 30 | */ 31 | 32 | "use strict"; 33 | 34 | var events = require('events'); 35 | 36 | var Discover = require('../index.js'); 37 | 38 | var test = module.exports = {}; 39 | 40 | test['add() should throw an exception if contact is missing id'] = function (test) { 41 | test.expect(1); 42 | var transport = new events.EventEmitter(); 43 | transport.setTransportInfo = function (contact) { 44 | return contact; 45 | }; 46 | var discover = new Discover({transport: transport}); 47 | test.throws(function () { 48 | discover.add({}); 49 | }, Error); 50 | test.done(); 51 | }; 52 | 53 | test['add() should throw an exception if contact id is not a string'] = function (test) { 54 | test.expect(1); 55 | var transport = new events.EventEmitter(); 56 | transport.setTransportInfo = function (contact) { 57 | return contact; 58 | }; 59 | var discover = new Discover({transport: transport}); 60 | test.throws(function () { 61 | discover.add({id: 1}); 62 | }, Error); 63 | test.done(); 64 | }; 65 | 66 | test['add() should return null if no KBuckets to add to'] = function (test) { 67 | test.expect(1); 68 | var barBase64 = new Buffer("bar").toString("base64"); 69 | var transport = new events.EventEmitter(); 70 | transport.setTransportInfo = function (contact) { 71 | return contact; 72 | }; 73 | var discover = new Discover({transport: transport}); 74 | var contact = discover.add({id: barBase64}); 75 | test.strictEqual(contact, null); 76 | test.done(); 77 | }; 78 | 79 | test['add() adds the remote contact to the closest KBucket'] = function (test) { 80 | test.expect(2); 81 | var fooBase64 = new Buffer("foo").toString("base64"); 82 | var barBase64 = new Buffer("bar").toString("base64"); 83 | var transport = new events.EventEmitter(); 84 | transport.setTransportInfo = function (contact) { 85 | return contact; 86 | }; 87 | var discover = new Discover({noCache: true, transport: transport}); 88 | discover.register({id: fooBase64}); 89 | // "foo" is the closest (and only) KBucket 90 | discover.add({id: barBase64}); 91 | discover.find(barBase64, function (error, contact) { 92 | test.ok(!error); 93 | test.deepEqual(contact, {id: barBase64}); 94 | }); 95 | test.done(); 96 | }; 97 | 98 | test['add() replaces local KBucket contact with remote contact if arbiter returns remote contact'] = function (test) { 99 | test.expect(3); 100 | var fooBase64 = new Buffer("foo").toString("base64"); 101 | 102 | var transport = new events.EventEmitter(); 103 | transport.setTransportInfo = function (contact) { 104 | return contact; 105 | }; 106 | 107 | var arbiter = function arbiter(incumbent, candidate) { 108 | return candidate; 109 | }; 110 | var arbiterDefaults = function arbiterDefaults(contact) { 111 | return contact; 112 | }; 113 | var discover = new Discover({ 114 | arbiter: arbiter, 115 | arbiterDefaults: arbiterDefaults, 116 | noCache: true, 117 | transport: transport 118 | }); 119 | discover.register({id: fooBase64, data: "foo"}); 120 | discover.add({id: fooBase64, data: "updated"}); 121 | discover.find(fooBase64, function (error, contact) { 122 | test.ok(!error); 123 | test.equal(contact.id, fooBase64); 124 | test.equal(contact.data, "updated"); 125 | }); 126 | test.done(); 127 | }; 128 | 129 | test['add() returns the contact on successful add'] = function (test) { 130 | test.expect(1); 131 | var fooBase64 = new Buffer("foo").toString("base64"); 132 | var barBase64 = new Buffer("bar").toString("base64"); 133 | var transport = new events.EventEmitter(); 134 | transport.setTransportInfo = function (contact) { 135 | return contact; 136 | }; 137 | var discover = new Discover({transport: transport}); 138 | discover.register({id: fooBase64}); 139 | // "foo" is the closest (and only) KBucket 140 | var contact = discover.add({id: barBase64}); 141 | test.deepEqual(contact, {id: barBase64}); 142 | test.done(); 143 | }; -------------------------------------------------------------------------------- /test/cache.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | cache.js - discover LRU cache test 4 | 5 | The MIT License (MIT) 6 | 7 | Copyright (c) 2014 Tristan Slominski 8 | 9 | Permission is hereby granted, free of charge, to any person 10 | obtaining a copy of this software and associated documentation 11 | files (the "Software"), to deal in the Software without 12 | restriction, including without limitation the rights to use, 13 | copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | copies of the Software, and to permit persons to whom the 15 | Software is furnished to do so, subject to the following 16 | conditions: 17 | 18 | The above copyright notice and this permission notice shall be 19 | included in all copies or substantial portions of the Software. 20 | 21 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 22 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 23 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 24 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 25 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 26 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 27 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 28 | OTHER DEALINGS IN THE SOFTWARE. 29 | 30 | */ 31 | 32 | "use strict"; 33 | 34 | var events = require('events'); 35 | 36 | var Discover = require('../index.js'); 37 | 38 | var test = module.exports = {}; 39 | 40 | test["'noCache' option set turns off caching"] = function (test) { 41 | test.expect(3); 42 | var barBase64 = new Buffer("bar").toString("base64"); 43 | var transport = new events.EventEmitter(); 44 | transport.setTransportInfo = function (contact) { 45 | return contact; 46 | }; 47 | var discover = new Discover({noCache: true, transport: transport}); 48 | var contact = discover.add({id: barBase64}); 49 | test.strictEqual(contact, null); 50 | discover.find(barBase64, function (error, contact) { 51 | test.ok(error); 52 | test.ok(!contact); 53 | test.done(); 54 | }); 55 | }; 56 | 57 | test["caching is on by default"] = function (test) { 58 | test.expect(3); 59 | var barBase64 = new Buffer("bar").toString("base64"); 60 | var transport = new events.EventEmitter(); 61 | transport.setTransportInfo = function (contact) { 62 | return contact; 63 | }; 64 | var discover = new Discover({transport: transport}); 65 | var contact = discover.add({id: barBase64}); 66 | test.strictEqual(contact, null); // no k-buckets to insert into 67 | discover.find(barBase64, function (error, contact) { 68 | test.ok(!error); 69 | test.deepEqual(contact, {id: barBase64}); // cached contact 70 | test.done(); 71 | }); 72 | }; 73 | 74 | test["'maxCacheSize' option sets maximum cache size"] = function (test) { 75 | test.expect(6); 76 | var fooBase64 = new Buffer("foo").toString("base64"); 77 | var barBase64 = new Buffer("bar").toString("base64"); 78 | var transport = new events.EventEmitter(); 79 | transport.setTransportInfo = function (contact) { 80 | return contact; 81 | }; 82 | var discover = new Discover({maxCacheSize: 1, transport: transport}); 83 | var contact = discover.add({id: barBase64}); // bar takes the 1 spot in cache 84 | test.strictEqual(contact, null); 85 | contact = discover.add({id: fooBase64}); // foo takes over only spot in cache 86 | test.strictEqual(contact, null); 87 | discover.find(barBase64, function (error, contact) { 88 | // bar was ejected out of the cache by foo 89 | test.ok(error); 90 | test.ok(!contact); 91 | discover.find(fooBase64, function (error, contact) { 92 | // foo was in cache 93 | test.ok(!error); 94 | test.deepEqual(contact, {id: fooBase64}); 95 | test.done(); 96 | }); 97 | }); 98 | }; -------------------------------------------------------------------------------- /test/find.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | find.js - discover.find(nodeId, callback) test 4 | 5 | The MIT License (MIT) 6 | 7 | Copyright (c) 2013-2014 Tristan Slominski 8 | 9 | Permission is hereby granted, free of charge, to any person 10 | obtaining a copy of this software and associated documentation 11 | files (the "Software"), to deal in the Software without 12 | restriction, including without limitation the rights to use, 13 | copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | copies of the Software, and to permit persons to whom the 15 | Software is furnished to do so, subject to the following 16 | conditions: 17 | 18 | The above copyright notice and this permission notice shall be 19 | included in all copies or substantial portions of the Software. 20 | 21 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 22 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 23 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 24 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 25 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 26 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 27 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 28 | OTHER DEALINGS IN THE SOFTWARE. 29 | 30 | */ 31 | 32 | "use strict"; 33 | 34 | var events = require('events'), 35 | Discover = require('../index.js'); 36 | 37 | var test = module.exports = {}; 38 | 39 | var expectDone = function expectDone(test, expected) { 40 | return function () { 41 | if (--expected == 0) { 42 | test.done(); 43 | } 44 | }; 45 | }; 46 | 47 | test['find() calls callback with an error if no registered nodes and no seeds'] = function (test) { 48 | test.expect(3); 49 | var fooBase64 = new Buffer("foo").toString("base64"); 50 | var transport = new events.EventEmitter(); 51 | var discover = new Discover({transport: transport}); 52 | 53 | var done = expectDone(test, 2); 54 | 55 | discover.on('stats.timers.find.ms', function (latency) { 56 | test.ok(true); 57 | done(); 58 | }); 59 | discover.on('stats.timers.find.request.ms', function (latency) { 60 | test.ok(false, 'stats.timers.find.request.ms should not be emitted'); 61 | }); 62 | discover.on('stats.timers.find.round.ms', function (latency) { 63 | test.ok(false, 'stats.timers.find.round.ms should not be emitted'); 64 | }); 65 | 66 | discover.find(fooBase64, function (error, contact) { 67 | test.ok(error instanceof Error); 68 | test.ok(!contact); 69 | done(); 70 | }); 71 | }; 72 | 73 | test['find() calls callback with an error if no registered nodes and seeds are unreachable'] = function (test) { 74 | test.expect(7); 75 | var fooBase64 = new Buffer("foo").toString("base64"); 76 | var transport = new events.EventEmitter(); 77 | transport.findNode = function (contact, nodeId, sender) { 78 | process.nextTick(function () { 79 | transport.emit('node', new Error('unreachable'), contact, nodeId); 80 | }); 81 | }; 82 | var discover = new Discover({ 83 | seeds: [ 84 | {id: "foo", transport: {host: '127.0.0.1', port: 6742}}, 85 | {id: "bar", transport: {host: '127.0.0.1', port: 6743}}, 86 | {id: "baz", transport: {host: '127.0.0.1', port: 6744}} 87 | ], 88 | transport: transport 89 | }); 90 | 91 | var done = expectDone(test, 6); 92 | 93 | discover.on('stats.timers.find.ms', function (latency) { 94 | test.ok(true); 95 | done(); 96 | }); 97 | discover.on('stats.timers.find.request.ms', function (latency) { 98 | test.ok(true); 99 | done(); 100 | }); 101 | discover.on('stats.timers.find.round.ms', function (latency) { 102 | test.ok(true); 103 | done(); 104 | }); 105 | 106 | discover.find(fooBase64, function (error, contact) { 107 | test.ok(error instanceof Error); 108 | test.ok(!contact); 109 | done(); 110 | }); 111 | }; 112 | 113 | test['find() calls transport.findNode() on the seeds if no registered nodes'] = function (test) { 114 | test.expect(6); 115 | var fooBase64 = new Buffer("foo").toString("base64"); 116 | var barBase64 = new Buffer("bar").toString("base64"); 117 | var bazBase64 = new Buffer("baz").toString("base64"); 118 | var seeds = [ 119 | {id: bazBase64, transport: {host: '127.0.0.1', port: 6744}}, 120 | {id: barBase64, transport: {host: '127.0.0.1', port: 6743}} 121 | ]; 122 | var transport = new events.EventEmitter(); 123 | var first = true; 124 | 125 | transport.findNode = function (contact, nodeId, sender) { 126 | if (first) { 127 | first = false; 128 | test.equal(contact.id, seeds[0].id); 129 | test.equal(contact.transport.host, seeds[0].transport.host); 130 | test.equal(contact.transport.port, seeds[0].transport.port); 131 | } else { 132 | test.equal(contact.id, seeds[1].id); 133 | test.equal(contact.transport.host, seeds[1].transport.host); 134 | test.equal(contact.transport.port, seeds[1].transport.port); 135 | test.done(); 136 | } 137 | }; 138 | var discover = new Discover({seeds: seeds, transport: transport}); 139 | 140 | // we never emit 'node' event on transport, so no stats will be unavailable 141 | discover.on('stats.timers.find.ms', function (latency) { 142 | test.ok(false, 'stats.timers.find.ms should not be called'); 143 | }); 144 | discover.on('stats.timers.find.request.ms', function (latency) { 145 | test.ok(false, 'stats.timers.find.request.ms should not be called'); 146 | }); 147 | discover.on('stats.timers.find.round.ms', function (latency) { 148 | test.ok(false, 'stats.timers.find.round.ms should not be called'); 149 | }); 150 | 151 | discover.find(fooBase64, function (error, contact) {}); 152 | }; 153 | 154 | test['find() returns found node if found node while contacting a seed on first round'] = function (test) { 155 | test.expect(6); 156 | var fooBase64 = new Buffer("foo").toString("base64"); 157 | var barBase64 = new Buffer("bar").toString("base64"); 158 | var bazBase64 = new Buffer("baz").toString("base64"); 159 | var seeds = [ 160 | {id: bazBase64, transport: {host: '127.0.0.1', port: 6744}}, 161 | {id: barBase64, transport: {host: '127.0.0.1', port: 6743}} 162 | ]; 163 | var transport = new events.EventEmitter(); 164 | transport.findNode = function (contact, nodeId, sender) { 165 | if (contact.id == seeds[0].id) { 166 | process.nextTick(function () { 167 | transport.emit('node', null, contact, nodeId, { 168 | id: fooBase64, 169 | data: { 170 | foo: 'bar' 171 | } 172 | }); 173 | }); 174 | } else { 175 | transport.emit('node', new Error("unreachable"), contact, nodeId); 176 | } 177 | }; 178 | var discover = new Discover({seeds: seeds, transport: transport}); 179 | 180 | var done = expectDone(test, 5); 181 | 182 | discover.on('stats.timers.find.ms', function (latency) { 183 | test.ok(true); 184 | done(); 185 | }); 186 | discover.on('stats.timers.find.request.ms', function (latency) { 187 | test.ok(true); 188 | done(); 189 | }); 190 | discover.on('stats.timers.find.round.ms', function (latency) { 191 | test.ok(true); 192 | done(); 193 | }); 194 | 195 | discover.find(fooBase64, function (error, contact) { 196 | test.equal(contact.id, fooBase64); 197 | test.deepEqual(contact.data, {foo: 'bar'}); 198 | done(); 199 | }); 200 | }; 201 | 202 | test['find() queries closest nodes if not found on first round by querying seed nodes'] = function (test) { 203 | test.expect(7); 204 | var fooBase64 = new Buffer("foo").toString("base64"); 205 | var barBase64 = new Buffer("bar").toString("base64"); 206 | var bazBase64 = new Buffer("baz").toString("base64"); 207 | var fopBase64 = new Buffer("fop").toString("base64"); 208 | var seeds = [ 209 | {id: bazBase64, transport: {host: '127.0.0.1', port: 6744}}, 210 | {id: barBase64, transport: {host: '127.0.0.1', port: 6743}} 211 | ]; 212 | var transport = new events.EventEmitter(); 213 | transport.findNode = function (contact, nodeId, sender) { 214 | if (contact.id == seeds[1].id) { 215 | process.nextTick(function () { 216 | transport.emit('node', null, contact, nodeId, [{ 217 | id: fopBase64, transport: {host: '192.168.0.1', port: 5553} 218 | }]); 219 | }); 220 | } else if (contact.id == fopBase64) { 221 | process.nextTick(function () { 222 | transport.emit('node', null, contact, nodeId, { 223 | id: fooBase64, 224 | data: { 225 | foo: 'bar' 226 | } 227 | }); 228 | }); 229 | } else { 230 | transport.emit('node', new Error("unreachable"), contact, nodeId); 231 | } 232 | }; 233 | var discover = new Discover({seeds: seeds, transport: transport}); 234 | 235 | var done = expectDone(test, 6); 236 | 237 | discover.on('stats.timers.find.ms', function (latency) { 238 | test.ok(true); // 1 239 | done(); 240 | }); 241 | discover.on('stats.timers.find.request.ms', function (latency) { 242 | test.ok(true); // 3 243 | done(); 244 | }); 245 | discover.on('stats.timers.find.round.ms', function (latency) { 246 | test.ok(true); // 2 247 | done(); 248 | }); 249 | 250 | discover.find(fooBase64, function (error, contact) { 251 | test.equal(contact.id, fooBase64); 252 | test.deepEqual(contact.data, {foo: 'bar'}); 253 | done(); 254 | }); 255 | }; 256 | 257 | test['find() queries nodes from closest kBucket of a registered node'] = function (test) { 258 | test.expect(14); 259 | // this test has one kBucket for registered nodeId "bar" 260 | // within this kBucket is one node with id "baz" (closer than "bar" to "foo") 261 | // querying "baz" responds with closer node "fop" 262 | // querying "fop" responds with node "foo" which we are looking for 263 | var fooBase64 = new Buffer("foo").toString("base64"); 264 | var barBase64 = new Buffer("bar").toString("base64"); 265 | var bazBase64 = new Buffer("baz").toString("base64"); 266 | var fopBase64 = new Buffer("fop").toString("base64"); 267 | // console.log("foo", fooBase64); 268 | // console.log("bar", barBase64); 269 | // console.log("baz", bazBase64); 270 | // console.log("fop", fopBase64); 271 | var transport = new events.EventEmitter(); 272 | transport.setTransportInfo = function (contact) { 273 | return contact; 274 | }; 275 | transport.findNode = function (contact, nodeId, sender) { 276 | if (contact.id == bazBase64) { 277 | process.nextTick(function () { 278 | transport.emit('node', null, contact, nodeId, [{ 279 | id: fopBase64, transport: {host: '192.168.0.1', port: 5553} 280 | }]); 281 | }); 282 | } else if (contact.id == fopBase64) { 283 | process.nextTick(function () { 284 | transport.emit('node', null, contact, nodeId, { 285 | id: fooBase64, 286 | data: { 287 | foo: 'bar' 288 | } 289 | }); 290 | }); 291 | } else if (nodeId == bazBase64) { 292 | // someone is looking for bazBase64, want to respond to it 293 | process.nextTick(function () { 294 | transport.emit('node', null, contact, nodeId, { 295 | id: bazBase64, 296 | transport: { 297 | host: '192.168.0.2', 298 | port: 5554 299 | } 300 | }); 301 | }); 302 | } else { 303 | transport.emit('node', new Error("unreachable"), contact, nodeId); 304 | } 305 | }; 306 | var seeds = [{id: bazBase64, transport: {host: '192.168.0.2', port: 5554}}]; 307 | var discover = new Discover({ 308 | // inlineTrace: true, 309 | seeds: seeds, 310 | transport: transport 311 | }); 312 | 313 | var done = expectDone(test, 9); 314 | 315 | discover.on('stats.timers.find.ms', function (latency) { 316 | test.ok(true); // find baz, find foo = 2 317 | done(); 318 | }); 319 | discover.on('stats.timers.find.request.ms', function (latency) { 320 | test.ok(true); // find baz, find foo, find fop = 3 321 | done(); 322 | }); 323 | discover.on('stats.timers.find.round.ms', function (latency) { 324 | test.ok(true); // find baz, find foo, find fop = 3 325 | done(); 326 | }); 327 | 328 | discover.register({id: barBase64}); // creates kBucket with "bar" node id 329 | discover.find(bazBase64, function (error, contact) { 330 | test.ok(!error, error); 331 | // make sure our testing setup is proceeding correctly 332 | test.equal(contact.id, bazBase64); 333 | test.equal(contact.transport.host, '192.168.0.2'); 334 | test.equal(contact.transport.port, 5554); 335 | // "baz" node should now be present in "bar" kBucket 336 | discover.find(fooBase64, function (error, contact) { 337 | // should select "bar" kBucket 338 | // should find closest node "baz" 339 | // should query "baz" and get back "fop" 340 | // should queyr "fop" and get back "foo" 341 | test.equal(contact.id, fooBase64); 342 | test.deepEqual(contact.data, {foo: 'bar'}); 343 | done(); 344 | }); 345 | }); 346 | }; 347 | 348 | test['find() returns local node without querying the network'] = function (test) { 349 | test.expect(4); 350 | var fooBase64 = new Buffer("foo").toString("base64"); 351 | var transport = new events.EventEmitter(); 352 | transport.setTransportInfo = function (contact) { 353 | return contact; 354 | }; 355 | var discover = new Discover({transport: transport}); 356 | 357 | discover.register({id: fooBase64, data: 'my data'}); 358 | 359 | var done = expectDone(test, 2); 360 | 361 | discover.on('stats.timers.find.ms', function (latency) { 362 | test.ok(true); // 1 363 | done(); 364 | }); 365 | discover.on('stats.timers.find.request.ms', function (latency) { 366 | test.ok(false, 'stats.timers.find.request.ms should not be called'); 367 | }); 368 | discover.on('stats.timers.find.round.ms', function (latency) { 369 | test.ok(false, 'stats.timers.find.round.ms should not be called'); 370 | }); 371 | 372 | discover.find(fooBase64, function (error, contact) { 373 | test.ok(!error, error); 374 | test.equal(contact.id, fooBase64); 375 | test.equal(contact.data, 'my data'); 376 | done(); 377 | }); 378 | }; 379 | 380 | test['find() returns local kBucket data on node without querying the network'] = function (test) { 381 | test.expect(3); 382 | var fooBase64 = new Buffer("foo").toString("base64"); 383 | var barBase64 = new Buffer("bar").toString("base64"); 384 | var transport = new events.EventEmitter(); 385 | transport.setTransportInfo = function (contact) { 386 | return contact; 387 | }; 388 | transport.findNode = function () { 389 | test.fail("queried the network via transport.findNode()"); 390 | }; 391 | var discover = new Discover({transport: transport}); 392 | discover.register({id: fooBase64, data: 'my data'}); 393 | transport.emit('reached', {id: barBase64, data: 'bar data'}); 394 | 395 | var done = expectDone(test, 2); 396 | 397 | discover.on('stats.timers.find.ms', function (latency) { 398 | test.ok(true); // 1 399 | done(); 400 | }); 401 | discover.on('stats.timers.find.request.ms', function (latency) { 402 | test.ok(false, 'stats.timers.find.request.ms should not be called'); 403 | }); 404 | discover.on('stats.timers.find.round.ms', function (latency) { 405 | test.ok(false, 'stats.timers.find.round.ms should not be called'); 406 | }); 407 | 408 | discover.find(barBase64, function (error, contact) { 409 | test.ok(!error, error); 410 | test.deepEqual(contact, {id: barBase64, data: 'bar data'}); 411 | done(); 412 | }); 413 | }; 414 | -------------------------------------------------------------------------------- /test/new.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | new.js - new Discover(options) test 4 | 5 | The MIT License (MIT) 6 | 7 | Copyright (c) 2013 Tristan Slominski 8 | 9 | Permission is hereby granted, free of charge, to any person 10 | obtaining a copy of this software and associated documentation 11 | files (the "Software"), to deal in the Software without 12 | restriction, including without limitation the rights to use, 13 | copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | copies of the Software, and to permit persons to whom the 15 | Software is furnished to do so, subject to the following 16 | conditions: 17 | 18 | The above copyright notice and this permission notice shall be 19 | included in all copies or substantial portions of the Software. 20 | 21 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 22 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 23 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 24 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 25 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 26 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 27 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 28 | OTHER DEALINGS IN THE SOFTWARE. 29 | 30 | */ 31 | 32 | "use strict"; 33 | 34 | var events = require('events'), 35 | TcpTransport = require('discover-tcp-transport'), 36 | Discover = require('../index.js'); 37 | 38 | var test = module.exports = {}; 39 | 40 | test['default transport should be discover-tcp-transport'] = function (test) { 41 | test.expect(1); 42 | var discover = new Discover(); 43 | test.ok(discover.transport instanceof TcpTransport); 44 | test.done(); 45 | }; 46 | 47 | test['transport provided via options should be used over the default transport'] = function (test) { 48 | test.expect(1); 49 | var transport = new events.EventEmitter(); 50 | var discover = new Discover({transport: transport}); 51 | test.equal(discover.transport, transport); 52 | test.done(); 53 | }; -------------------------------------------------------------------------------- /test/onFindNode.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | onFindNode.js - transport.emit('findNode', ...) test 4 | 5 | The MIT License (MIT) 6 | 7 | Copyright (c) 2013-2014 Tristan Slominski 8 | 9 | Permission is hereby granted, free of charge, to any person 10 | obtaining a copy of this software and associated documentation 11 | files (the "Software"), to deal in the Software without 12 | restriction, including without limitation the rights to use, 13 | copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | copies of the Software, and to permit persons to whom the 15 | Software is furnished to do so, subject to the following 16 | conditions: 17 | 18 | The above copyright notice and this permission notice shall be 19 | included in all copies or substantial portions of the Software. 20 | 21 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 22 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 23 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 24 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 25 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 26 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 27 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 28 | OTHER DEALINGS IN THE SOFTWARE. 29 | 30 | */ 31 | 32 | "use strict"; 33 | 34 | var events = require('events'), 35 | Discover = require('../index.js'); 36 | 37 | var test = module.exports = {}; 38 | 39 | test["on 'findNode' returns the contact with id and data if node is one of registered nodes"] = function (test) { 40 | test.expect(2); 41 | var fooBase64 = new Buffer("foo").toString("base64"); 42 | 43 | var sender = {id: fooBase64}; 44 | var transport = new events.EventEmitter(); 45 | transport.setTransportInfo = function (contact) { 46 | return contact; 47 | }; 48 | var discover = new Discover({ 49 | transport: transport 50 | }); 51 | discover.register({id: fooBase64}); 52 | transport.emit('findNode', fooBase64, sender, function (error, contact) { 53 | test.ok(!error); 54 | test.deepEqual(contact, {id: fooBase64, vectorClock: 0}); 55 | test.done(); 56 | }); 57 | }; 58 | 59 | test["on 'findNode' returns the contact with id and data if node is one of registered nodes and sender is undefined"] = function (test) { 60 | test.expect(2); 61 | var fooBase64 = new Buffer("foo").toString("base64"); 62 | 63 | var sender = undefined; 64 | var transport = new events.EventEmitter(); 65 | transport.setTransportInfo = function (contact) { 66 | return contact; 67 | }; 68 | var discover = new Discover({ 69 | transport: transport 70 | }); 71 | discover.register({id: fooBase64}); 72 | transport.emit('findNode', fooBase64, sender, function (error, contact) { 73 | test.ok(!error); 74 | test.deepEqual(contact, {id: fooBase64, vectorClock: 0}); 75 | test.done(); 76 | }); 77 | }; 78 | 79 | test["on 'findNode' returns the contact with id and data if node has been 'reached'"] = function (test) { 80 | test.expect(2); 81 | var fooBase64 = new Buffer("foo").toString("base64"); 82 | var barBase64 = new Buffer("bar").toString("base64"); 83 | 84 | var sender = {id: fooBase64}; 85 | var transport = new events.EventEmitter(); 86 | transport.setTransportInfo = function (contact) { 87 | return contact; 88 | }; 89 | transport.findNode = function () { 90 | test.fail("used transport.findNode()"); 91 | }; 92 | var discover = new Discover({transport: transport}); 93 | discover.register({id: fooBase64}); 94 | transport.emit('reached', {id: barBase64}); 95 | // "bar" should now be in "foo" kBucket 96 | transport.emit('findNode', barBase64, sender, function (error, contact) { 97 | test.ok(!error); 98 | test.equal(contact.id, barBase64); 99 | test.done(); 100 | }); 101 | }; 102 | 103 | test["on 'findNode' returns the contact with id and data if node has been 'reached' and sender is undefined"] = function (test) { 104 | test.expect(2); 105 | var fooBase64 = new Buffer("foo").toString("base64"); 106 | var barBase64 = new Buffer("bar").toString("base64"); 107 | 108 | var sender = undefined; 109 | var transport = new events.EventEmitter(); 110 | transport.setTransportInfo = function (contact) { 111 | return contact; 112 | }; 113 | transport.findNode = function () { 114 | test.fail("used transport.findNode()"); 115 | }; 116 | var discover = new Discover({transport: transport}); 117 | discover.register({id: fooBase64}); 118 | transport.emit('reached', {id: barBase64}); 119 | // "bar" should now be in "foo" kBucket 120 | transport.emit('findNode', barBase64, sender, function (error, contact) { 121 | test.ok(!error); 122 | test.equal(contact.id, barBase64); 123 | test.done(); 124 | }); 125 | }; 126 | 127 | test["on 'findNode' returns closest nodes if node is not one of registered nodes"] = function (test) { 128 | test.expect(2); 129 | // this test has one kBucket for registered nodeId "bar" 130 | // within this kBucket is two nodes closer to "bar" than "foo": "bas", "baz" 131 | var fooBase64 = new Buffer("foo").toString("base64"); 132 | var barBase64 = new Buffer("bar").toString("base64"); 133 | var basBase64 = new Buffer("bas").toString("base64"); 134 | var bazBase64 = new Buffer("baz").toString("base64"); 135 | 136 | var sender = {id: fooBase64}; 137 | var transport = new events.EventEmitter(); 138 | transport.setTransportInfo = function (contact) { 139 | return contact; 140 | }; 141 | var discover = new Discover({ 142 | transport: transport 143 | }); 144 | discover.register({id: fooBase64}); 145 | transport.emit('reached', {id: basBase64}); 146 | transport.emit('reached', {id: bazBase64}); 147 | // "bas" and "baz" should now be in "foo" kBucket 148 | transport.emit('findNode', barBase64, sender, function (error, contacts) { 149 | test.ok(!error); 150 | test.deepEqual(contacts, [{id: basBase64}, {id: bazBase64}]); 151 | test.done(); 152 | }); 153 | }; 154 | 155 | test["on 'findNode' returns closest nodes if node is not one of registered nodes and sender is undefined"] = function (test) { 156 | test.expect(2); 157 | // this test has one kBucket for registered nodeId "bar" 158 | // within this kBucket is two nodes closer to "bar" than "foo": "bas", "baz" 159 | var fooBase64 = new Buffer("foo").toString("base64"); 160 | var barBase64 = new Buffer("bar").toString("base64"); 161 | var basBase64 = new Buffer("bas").toString("base64"); 162 | var bazBase64 = new Buffer("baz").toString("base64"); 163 | 164 | var sender = undefined; 165 | var transport = new events.EventEmitter(); 166 | transport.setTransportInfo = function (contact) { 167 | return contact; 168 | }; 169 | var discover = new Discover({ 170 | transport: transport 171 | }); 172 | discover.register({id: fooBase64}); 173 | transport.emit('reached', {id: basBase64}); 174 | transport.emit('reached', {id: bazBase64}); 175 | // "bas" and "baz" should now be in "foo" kBucket 176 | transport.emit('findNode', barBase64, sender, function (error, contacts) { 177 | test.ok(!error); 178 | test.deepEqual(contacts, [{id: basBase64}, {id: bazBase64}]); 179 | test.done(); 180 | }); 181 | }; 182 | 183 | test["on 'findNode' adds the sender to the closest KBucket to the sender"] = function (test) { 184 | test.expect(4); 185 | var fooBase64 = new Buffer("foo").toString("base64"); 186 | var barBuffer = new Buffer("bar"); 187 | var barBase64 = barBuffer.toString("base64"); 188 | 189 | var sender = {id: barBase64, data: 'bar', host: '127.0.0.1', port: 6999}; 190 | var transport = new events.EventEmitter(); 191 | transport.setTransportInfo = function (contact) { 192 | return contact; 193 | }; 194 | var discover = new Discover({ 195 | // inlineTrace: true, 196 | transport: transport 197 | }); 198 | discover.register({id: fooBase64, data: 'foo'}); 199 | var kBucket = discover.kBuckets[fooBase64].kBucket; 200 | transport.emit('findNode', barBase64, sender, function (error, contacts) { 201 | test.ok(!error); 202 | test.deepEqual(contacts, []); 203 | var closest = kBucket.closest({id: barBuffer}); 204 | test.equal(closest.length, 1); 205 | test.equal(closest[0].id.toString("base64"), barBase64); 206 | test.done(); 207 | }); 208 | }; 209 | 210 | test["on 'findNode' adds the sender as the local KBucket contact if id matches a local KBucket and arbiter returns candidate"] = function (test) { 211 | test.expect(4); 212 | var fooBase64 = new Buffer("foo").toString("base64"); 213 | var barBuffer = new Buffer("bar"); 214 | var barBase64 = barBuffer.toString("base64"); 215 | 216 | var arbiter = function arbiter(incumbent, candidate) { 217 | return candidate; 218 | }; 219 | var arbiterDefaults = function arbiterDefaults(contact) { 220 | return contact; 221 | }; 222 | var sender = {id: fooBase64, data: 'updated'}; 223 | var transport = new events.EventEmitter(); 224 | transport.setTransportInfo = function (contact) { 225 | return contact; 226 | }; 227 | var discover = new Discover({ 228 | arbiter: arbiter, 229 | arbiterDefaults: arbiterDefaults, 230 | // inlineTrace: true, 231 | transport: transport 232 | }); 233 | discover.register({id: fooBase64, data: 'foo'}); 234 | transport.emit('findNode', barBase64, sender, function (error, contacts) { 235 | test.ok(!error); 236 | test.deepEqual(contacts, []); 237 | var contact = discover.kBuckets[fooBase64].contact; 238 | test.equal(contact.id, fooBase64); 239 | test.equal(contact.data, "updated"); 240 | test.done(); 241 | }); 242 | }; 243 | -------------------------------------------------------------------------------- /test/onKBucketPing.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | onKBucketPing.js - kBucket.emit('ping', ...) test 4 | 5 | The MIT License (MIT) 6 | 7 | Copyright (c) 2013 Tristan Slominski 8 | 9 | Permission is hereby granted, free of charge, to any person 10 | obtaining a copy of this software and associated documentation 11 | files (the "Software"), to deal in the Software without 12 | restriction, including without limitation the rights to use, 13 | copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | copies of the Software, and to permit persons to whom the 15 | Software is furnished to do so, subject to the following 16 | conditions: 17 | 18 | The above copyright notice and this permission notice shall be 19 | included in all copies or substantial portions of the Software. 20 | 21 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 22 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 23 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 24 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 25 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 26 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 27 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 28 | OTHER DEALINGS IN THE SOFTWARE. 29 | 30 | */ 31 | 32 | "use strict"; 33 | 34 | var events = require('events'), 35 | Discover = require('../index.js'), 36 | KBucket = require('k-bucket'); 37 | 38 | var test = module.exports = {}; 39 | 40 | test["on 'ping' forces transport.ping() of all old contacts"] = function (test) { 41 | test.expect(4); 42 | var fooBuffer = new Buffer("foo"); 43 | var fooBufferBase64 = fooBuffer.toString("base64"); 44 | var barBuffer = new Buffer("bar"); 45 | var barBufferBase64 = barBuffer.toString("base64"); 46 | var bazBuffer = new Buffer("baz"); 47 | var bazBufferBase64 = bazBuffer.toString("base64"); 48 | var fuzBuffer = new Buffer("fuz"); 49 | var fuzBufferBase64 = fuzBuffer.toString("base64"); 50 | 51 | var transport = new events.EventEmitter(); 52 | transport.setTransportInfo = function (contact) { 53 | return contact; 54 | }; 55 | transport.findNode = function (contact, nodeId) {}; 56 | var count = 0; 57 | transport.ping = function (contact) { 58 | test.ok([fooBufferBase64, barBufferBase64, bazBufferBase64].indexOf(contact.id) > -1); 59 | count++; 60 | if (count == 3) { 61 | test.done(); 62 | } 63 | }; 64 | var discover = new Discover({transport: transport}); 65 | var contact = discover.register({data: 'foo'}); 66 | test.ok(contact.id); 67 | var kBucket = discover.kBuckets[contact.id].kBucket; 68 | kBucket.emit('ping', 69 | [{id: fooBuffer}, {id: barBuffer}, {id: bazBuffer}], 70 | {id: fuzBuffer}); 71 | }; 72 | 73 | test["on 'ping' removes old contact from this bucket if old contact is unreachable"] = function (test) { 74 | // this test exists to make sure that if a closer k-bucket was inserted 75 | // while we were checking reachability, while discover at top level will 76 | // remove unreachable node from the closest k-bucket, this k-bucket might 77 | // still have the unreachable node and no room to insert the new one 78 | // even though it should be able to isnert the new node 79 | test.expect(4); 80 | var fooBuffer = new Buffer("foo"); 81 | var fooBufferBase64 = fooBuffer.toString("base64"); 82 | var barBuffer = new Buffer("bar"); 83 | var barBufferBase64 = barBuffer.toString("base64"); 84 | var bazBuffer = new Buffer("baz"); 85 | var bazBufferBase64 = bazBuffer.toString("base64"); 86 | var fopBuffer = new Buffer("fop").toString("base64"); 87 | var fopBufferBase64 = fopBuffer.toString("base64"); 88 | 89 | // assert test assumption that "baz" is closer to "foo" than "bar" 90 | test.ok(KBucket.distance(bazBuffer, fooBuffer) < KBucket.distance(barBuffer, fooBuffer)); 91 | 92 | var transport = new events.EventEmitter(); 93 | transport.setTransportInfo = function (contact) { 94 | return contact; 95 | }; 96 | transport.findNode = function () {}; 97 | transport.ping = function (contact) { 98 | test.equal(contact.id, fooBufferBase64); 99 | discover.register({id: bazBufferBase64, data: 'baz'}); 100 | // "baz" bucket will now be closest to foo so top level 101 | // 'unreachable' handler will remove foo contact from "baz" bucket 102 | // removing from "bar" bucket is tested in kBucket.remove override 103 | transport.emit('unreachable', contact); 104 | }; 105 | 106 | var discover = new Discover({ 107 | // inlineTrace: true, 108 | transport: transport 109 | }); 110 | var contact = discover.register({id: barBufferBase64, data: 'bar'}); 111 | transport.emit('reached', {id: fooBufferBase64}); 112 | test.ok(contact.id); 113 | var kBucket = discover.kBuckets[contact.id].kBucket; 114 | kBucket.remove = function (contact) { 115 | test.equal(contact.id.toString("base64"), fooBufferBase64); 116 | test.done(); 117 | } 118 | kBucket.emit('ping', [{id: fooBuffer}], {id: fopBuffer}); 119 | }; 120 | 121 | test["on 'ping' adds new contact if old contact is unreachable"] = function (test) { 122 | test.expect(3); 123 | var fooBuffer = new Buffer("foo"); 124 | var fooBufferBase64 = fooBuffer.toString("base64"); 125 | var barBuffer = new Buffer("bar"); 126 | var barBufferBase64 = barBuffer.toString("base64"); 127 | 128 | var transport = new events.EventEmitter(); 129 | transport.setTransportInfo = function (contact) { 130 | return contact; 131 | }; 132 | transport.ping = function (contact) { 133 | test.equal(contact.id, fooBufferBase64); 134 | transport.emit('unreachable', contact); 135 | }; 136 | var discover = new Discover({transport: transport}); 137 | var contact = discover.register({data: 'foo'}); 138 | transport.emit('reached', {id: fooBufferBase64}); 139 | test.ok(contact.id); 140 | var kBucket = discover.kBuckets[contact.id].kBucket; 141 | kBucket.add = function (contact) { 142 | test.equal(contact.id.toString("base64"), barBufferBase64); 143 | test.done(); 144 | }; 145 | kBucket.emit('ping', [{id: fooBuffer}], {id: barBuffer}); 146 | }; 147 | 148 | test["on 'ping' removes reached and unreachable listeners if all old contacts are reached"] = function (test) { 149 | test.expect(6); 150 | var fooBuffer = new Buffer("foo"); 151 | var fooBufferBase64 = fooBuffer.toString("base64"); 152 | var barBuffer = new Buffer("bar"); 153 | var barBufferBase64 = barBuffer.toString("base64"); 154 | 155 | var transport = new events.EventEmitter(); 156 | var kBucket; 157 | transport.setTransportInfo = function (contact) { 158 | return contact; 159 | }; 160 | transport.ping = function (contact) { 161 | test.equal(contact.id, fooBufferBase64); 162 | // discover and the 'ping' handler listening to 'reached' 163 | test.equal(transport.listeners('reached').length, 2); 164 | test.equal(transport.listeners('unreachable').length, 2); 165 | transport.emit('reached', contact); 166 | // 'ping' handler listening to 'reached' is removed 167 | test.equal(transport.listeners('reached').length, 1); 168 | test.equal(transport.listeners('unreachable').length, 1); 169 | test.done(); 170 | }; 171 | var discover = new Discover({transport: transport}); 172 | var contact = discover.register({data: 'foo'}); 173 | transport.emit('reached', {id: fooBufferBase64}); 174 | test.ok(contact.id); 175 | kBucket = discover.kBuckets[contact.id].kBucket; 176 | kBucket.emit('ping', [{id: fooBuffer}], {id: barBuffer}); 177 | }; 178 | 179 | 180 | test["on 'ping' removes reached and unreachable listeners if one of old contacts is unreachable"] = function (test) { 181 | test.expect(6); 182 | var fooBuffer = new Buffer("foo"); 183 | var fooBufferBase64 = fooBuffer.toString("base64"); 184 | var barBuffer = new Buffer("bar"); 185 | var barBufferBase64 = barBuffer.toString("base64"); 186 | 187 | var transport = new events.EventEmitter(); 188 | var kBucket; 189 | transport.setTransportInfo = function (contact) { 190 | return contact; 191 | }; 192 | transport.ping = function (contact) { 193 | test.equal(contact.id, fooBufferBase64); 194 | // discover and the 'ping' handler listening to 'reached' 195 | test.equal(transport.listeners('reached').length, 2); 196 | test.equal(transport.listeners('unreachable').length, 2); 197 | transport.emit('unreachable', contact); 198 | // 'ping' handler listening to 'reached' is removed 199 | test.equal(transport.listeners('reached').length, 1); 200 | test.equal(transport.listeners('unreachable').length, 1); 201 | test.done(); 202 | }; 203 | var discover = new Discover({transport: transport}); 204 | var contact = discover.register({data: 'foo'}); 205 | transport.emit('reached', {id: fooBufferBase64}); 206 | test.ok(contact.id); 207 | kBucket = discover.kBuckets[contact.id].kBucket; 208 | kBucket.emit('ping', [{id: fooBuffer}], {id: barBuffer}); 209 | }; -------------------------------------------------------------------------------- /test/onNode.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | onNode.js - transport.emit('node', ...) test 4 | 5 | The MIT License (MIT) 6 | 7 | Copyright (c) 2013-2014 Tristan Slominski 8 | 9 | Permission is hereby granted, free of charge, to any person 10 | obtaining a copy of this software and associated documentation 11 | files (the "Software"), to deal in the Software without 12 | restriction, including without limitation the rights to use, 13 | copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | copies of the Software, and to permit persons to whom the 15 | Software is furnished to do so, subject to the following 16 | conditions: 17 | 18 | The above copyright notice and this permission notice shall be 19 | included in all copies or substantial portions of the Software. 20 | 21 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 22 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 23 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 24 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 25 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 26 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 27 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 28 | OTHER DEALINGS IN THE SOFTWARE. 29 | 30 | */ 31 | 32 | "use strict"; 33 | 34 | var events = require('events'), 35 | Discover = require('../index.js'); 36 | 37 | var test = module.exports = {}; 38 | 39 | var expectDone = function expectDone(test, expected) { 40 | return function () { 41 | if (--expected == 0) { 42 | test.done(); 43 | } 44 | }; 45 | }; 46 | 47 | test["on 'node' adds the reached contact to the closest KBucket"] = function (test) { 48 | test.expect(4); 49 | var fooBase64 = new Buffer("foo").toString("base64"); 50 | var barBase64 = new Buffer("bar").toString("base64"); 51 | var transport = new events.EventEmitter(); 52 | transport.setTransportInfo = function (contact) { 53 | return contact; 54 | }; 55 | var discover = new Discover({ 56 | noCache: true, 57 | transport: transport 58 | }); 59 | discover.register({id: fooBase64}); 60 | // "foo" is the closest (and only) KBucket 61 | 62 | var done = expectDone(test, 3); 63 | 64 | discover.on('stats.timers.find.ms', function (latency) { 65 | test.ok(true); // 1 66 | done(); 67 | }); 68 | discover.on('stats.timers.find.request.ms', function (latency) { 69 | test.equal(latency, 0); // 1 70 | done(); 71 | }); 72 | discover.on('stats.timers.find.round.ms', function (latency) { 73 | test.ok(false, 'stats.timers.find.round.ms should not be called'); 74 | }); 75 | 76 | transport.emit('node', null, {id: barBase64}); 77 | discover.find(barBase64, function (error, contact) { 78 | test.ok(!error); 79 | test.deepEqual(contact, {id: barBase64}); 80 | }); 81 | done(); 82 | }; 83 | 84 | test["on 'node' adds the reached contact as the local KBucket contact if id matches a local KBucket and arbiter returns candidate"] = function (test) { 85 | test.expect(5); 86 | var fooBase64 = new Buffer("foo").toString("base64"); 87 | 88 | var arbiter = function arbiter(incumbent, candidate) { 89 | return candidate; 90 | }; 91 | var arbiterDefaults = function arbiterDefaults(contact) { 92 | return contact; 93 | }; 94 | 95 | var transport = new events.EventEmitter(); 96 | transport.setTransportInfo = function (contact) { 97 | return contact; 98 | }; 99 | var discover = new Discover({ 100 | arbiter: arbiter, 101 | arbiterDefaults: arbiterDefaults, 102 | // inlineTrace: true, 103 | noCache: true, 104 | transport: transport 105 | }); 106 | discover.register({id: fooBase64, data: "foo"}); 107 | 108 | var done = expectDone(test, 3); 109 | 110 | discover.on('stats.timers.find.ms', function (latency) { 111 | test.ok(true); // 1 112 | done(); 113 | }); 114 | discover.on('stats.timers.find.request.ms', function (latency) { 115 | test.equal(latency, 0); // 1 116 | done(); 117 | }); 118 | discover.on('stats.timers.find.round.ms', function (latency) { 119 | test.ok(false, 'stats.timers.find.round.ms should not be called'); 120 | }); 121 | 122 | transport.emit('node', null, {id: fooBase64, data: "updated"}); 123 | discover.find(fooBase64, function (error, contact) { 124 | test.ok(!error); 125 | test.equal(contact.id, fooBase64); 126 | test.equal(contact.data, "updated"); 127 | }); 128 | done(); 129 | }; 130 | 131 | test["on 'node' adds the response as the local KBucket contact if id matches a local KBucket and arbiter returns candidate"] = function (test) { 132 | test.expect(2); 133 | var fooBase64 = new Buffer("foo").toString("base64"); 134 | var barBuffer = new Buffer("bar"); 135 | var barBase64 = barBuffer.toString("base64"); 136 | 137 | var arbiter = function arbiter(incumbent, candidate) { 138 | return candidate; 139 | }; 140 | var arbiterDefaults = function arbiterDefaults(contact) { 141 | return contact; 142 | }; 143 | 144 | var sender = {id: fooBase64, data: 'updated'}; 145 | var response = {id: barBase64, data: 'updated'}; 146 | 147 | var transport = new events.EventEmitter(); 148 | transport.findNode = function () {}; 149 | transport.setTransportInfo = function (contact) { 150 | return contact; 151 | }; 152 | var discover = new Discover({ 153 | arbiter: arbiter, 154 | arbiterDefaults: arbiterDefaults, 155 | // inlineTrace: true, 156 | seeds: [ 157 | {id: fooBase64, transport: {}} 158 | ], 159 | transport: transport 160 | }); 161 | discover.register({id: barBase64, data: 'bar'}); 162 | transport.emit('node', null, sender, barBase64, response); 163 | var contact = discover.kBuckets[barBase64].contact; 164 | test.equal(contact.id, barBase64); 165 | test.equal(contact.data, "updated"); 166 | test.done(); 167 | }; 168 | 169 | test["on 'node' adds the responses as the local KBucket contacts if id matches a local KBucket and arbiter returns candidate"] = function (test) { 170 | test.expect(6); 171 | var fooBase64 = new Buffer("foo").toString("base64"); 172 | var barBase64 = new Buffer("bar").toString("base64"); 173 | var bazBase64 = new Buffer("baz").toString("base64"); 174 | var basBase64 = new Buffer("bas").toString("base64"); 175 | 176 | var arbiter = function arbiter(incumbent, candidate) { 177 | return candidate; 178 | }; 179 | var arbiterDefaults = function arbiterDefaults(contact) { 180 | return contact; 181 | }; 182 | 183 | var sender = {id: fooBase64, data: 'updated'}; 184 | var responses = [ 185 | {id: barBase64, data: 'updated'}, 186 | {id: bazBase64, data: 'updated'}, 187 | {id: basBase64, data: 'updated'} 188 | ]; 189 | 190 | var transport = new events.EventEmitter(); 191 | transport.findNode = function () {}; 192 | transport.setTransportInfo = function (contact) { 193 | return contact; 194 | }; 195 | var discover = new Discover({ 196 | arbiter: arbiter, 197 | arbiterDefaults: arbiterDefaults, 198 | // inlineTrace: true, 199 | seeds: [ 200 | {id: fooBase64, transport: {}} 201 | ], 202 | transport: transport 203 | }); 204 | 205 | discover.register({id: barBase64, data: 'bar'}); 206 | discover.register({id: bazBase64, data: 'baz'}); 207 | discover.register({id: basBase64, data: 'bas'}); 208 | 209 | transport.emit('node', null, sender, barBase64, responses); 210 | 211 | var barContact = discover.kBuckets[barBase64].contact; 212 | test.equal(barContact.id, barBase64); 213 | test.equal(barContact.data, "updated"); 214 | 215 | var bazContact = discover.kBuckets[bazBase64].contact; 216 | test.equal(bazContact.id, bazBase64); 217 | test.equal(bazContact.data, "updated"); 218 | 219 | var basContact = discover.kBuckets[basBase64].contact; 220 | test.equal(basContact.id, basBase64); 221 | test.equal(basContact.data, "updated"); 222 | 223 | test.done(); 224 | }; 225 | -------------------------------------------------------------------------------- /test/onReached.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | onReached.js - transport.emit('reached', ...) test 4 | 5 | The MIT License (MIT) 6 | 7 | Copyright (c) 2013 Tristan Slominski 8 | 9 | Permission is hereby granted, free of charge, to any person 10 | obtaining a copy of this software and associated documentation 11 | files (the "Software"), to deal in the Software without 12 | restriction, including without limitation the rights to use, 13 | copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | copies of the Software, and to permit persons to whom the 15 | Software is furnished to do so, subject to the following 16 | conditions: 17 | 18 | The above copyright notice and this permission notice shall be 19 | included in all copies or substantial portions of the Software. 20 | 21 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 22 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 23 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 24 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 25 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 26 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 27 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 28 | OTHER DEALINGS IN THE SOFTWARE. 29 | 30 | */ 31 | 32 | "use strict"; 33 | 34 | var events = require('events'), 35 | Discover = require('../index.js'); 36 | 37 | var test = module.exports = {}; 38 | 39 | test["on 'reached' adds the reached contact to the closest KBucket"] = function (test) { 40 | test.expect(2); 41 | var fooBase64 = new Buffer("foo").toString("base64"); 42 | var barBase64 = new Buffer("bar").toString("base64"); 43 | var transport = new events.EventEmitter(); 44 | transport.setTransportInfo = function (contact) { 45 | return contact; 46 | }; 47 | var discover = new Discover({ 48 | transport: transport 49 | }); 50 | discover.register({id: fooBase64}); 51 | // "foo" is the closest (and only) KBucket 52 | transport.emit('reached', {id: barBase64}); 53 | discover.find(barBase64, function (error, contact) { 54 | test.ok(!error); 55 | test.deepEqual(contact, {id: barBase64}); 56 | }); 57 | test.done(); 58 | }; -------------------------------------------------------------------------------- /test/onTransportPing.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | onTransportPing.js - transport.emit('ping', ...) test 4 | 5 | The MIT License (MIT) 6 | 7 | Copyright (c) 2013 Tristan Slominski 8 | 9 | Permission is hereby granted, free of charge, to any person 10 | obtaining a copy of this software and associated documentation 11 | files (the "Software"), to deal in the Software without 12 | restriction, including without limitation the rights to use, 13 | copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | copies of the Software, and to permit persons to whom the 15 | Software is furnished to do so, subject to the following 16 | conditions: 17 | 18 | The above copyright notice and this permission notice shall be 19 | included in all copies or substantial portions of the Software. 20 | 21 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 22 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 23 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 24 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 25 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 26 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 27 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 28 | OTHER DEALINGS IN THE SOFTWARE. 29 | 30 | */ 31 | 32 | "use strict"; 33 | 34 | var events = require('events'), 35 | Discover = require('../index.js'); 36 | 37 | var test = module.exports = {}; 38 | 39 | test["on 'ping' returns the contact with id and data if node is one of registered nodes"] = function (test) { 40 | test.expect(2); 41 | var fooBase64 = new Buffer("foo").toString("base64"); 42 | 43 | var sender = {id: fooBase64}; 44 | var transport = new events.EventEmitter(); 45 | transport.setTransportInfo = function (contact) { 46 | return contact; 47 | }; 48 | var discover = new Discover({transport: transport}); 49 | discover.register({id: fooBase64}); 50 | transport.emit('ping', fooBase64, sender, function (error, contact) { 51 | test.ok(!error); 52 | test.deepEqual(contact, {id: fooBase64, vectorClock: 0}); 53 | test.done(); 54 | }); 55 | }; 56 | 57 | test["on 'ping' does not return the contact if it has been 'reached' (instead of registered)"] = function (test) { 58 | test.expect(2); 59 | var fooBase64 = new Buffer("foo").toString("base64"); 60 | var barBase64 = new Buffer("bar").toString("base64"); 61 | 62 | var sender = {id: fooBase64}; 63 | var transport = new events.EventEmitter(); 64 | transport.setTransportInfo = function (contact) { 65 | return contact; 66 | }; 67 | transport.findNode = function () { 68 | test.fail("used transport.findNode()"); 69 | }; 70 | var discover = new Discover({transport: transport}); 71 | discover.register({id: fooBase64}); 72 | transport.emit('reached', {id: barBase64}); 73 | // "bar" should now be in "foo" kBucket 74 | transport.emit('ping', barBase64, sender, function (error, contact) { 75 | test.ok(error); 76 | test.ok(!contact); 77 | test.done(); 78 | }); 79 | }; 80 | 81 | test["on 'ping' adds the sender to the closest KBucket to the sender"] = function (test) { 82 | test.expect(4); 83 | var fooBase64 = new Buffer("foo").toString("base64"); 84 | var barBuffer = new Buffer("bar"); 85 | var barBase64 = barBuffer.toString("base64"); 86 | 87 | var sender = {id: barBase64, data: 'bar', host: '127.0.0.1', port: 6999}; 88 | var transport = new events.EventEmitter(); 89 | transport.setTransportInfo = function (contact) { 90 | return contact; 91 | }; 92 | var discover = new Discover({ 93 | // inlineTrace: true, 94 | transport: transport 95 | }); 96 | discover.register({id: fooBase64, data: 'foo'}); 97 | var kBucket = discover.kBuckets[fooBase64].kBucket; 98 | transport.emit('ping', barBase64, sender, function (error, contact) { 99 | test.ok(error); 100 | test.ok(!contact); 101 | // callback is called before sender processing 102 | // so delay the sender assertions by using process.nextTick 103 | process.nextTick(function () { 104 | var util = require('util'); 105 | var closest = kBucket.closest({id: barBuffer}); 106 | test.equal(closest.length, 1); 107 | test.equal(closest[0].id.toString("base64"), barBase64); 108 | test.done(); 109 | }); 110 | }); 111 | }; -------------------------------------------------------------------------------- /test/onUnreachable.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | onUnreachable.js - transport.emit('unreachable', ...) test 4 | 5 | The MIT License (MIT) 6 | 7 | Copyright (c) 2013 Tristan Slominski 8 | 9 | Permission is hereby granted, free of charge, to any person 10 | obtaining a copy of this software and associated documentation 11 | files (the "Software"), to deal in the Software without 12 | restriction, including without limitation the rights to use, 13 | copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | copies of the Software, and to permit persons to whom the 15 | Software is furnished to do so, subject to the following 16 | conditions: 17 | 18 | The above copyright notice and this permission notice shall be 19 | included in all copies or substantial portions of the Software. 20 | 21 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 22 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 23 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 24 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 25 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 26 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 27 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 28 | OTHER DEALINGS IN THE SOFTWARE. 29 | 30 | */ 31 | 32 | "use strict"; 33 | 34 | var events = require('events'), 35 | Discover = require('../index.js'); 36 | 37 | var test = module.exports = {}; 38 | 39 | test["on 'unreachable' removes the unreachable contact from the closest KBucket"] = function (test) { 40 | test.expect(8); 41 | var fooBase64 = new Buffer("foo").toString("base64"); 42 | var barBase64 = new Buffer("bar").toString("base64"); 43 | var transport = new events.EventEmitter(); 44 | var initial = true; 45 | transport.setTransportInfo = function (contact) { 46 | return contact; 47 | }; 48 | transport.findNode = function (contact, nodeId) { 49 | if (initial) { 50 | test.equal(contact.id, "baz"); 51 | test.equal(contact.transport.ip, "127.0.0.1"); 52 | test.equal(contact.transport.port, 6742); 53 | test.equal(nodeId, fooBase64); 54 | initial = false; 55 | } else { 56 | test.equal(contact.id, "baz"); 57 | test.equal(contact.transport.ip, "127.0.0.1"); 58 | test.equal(contact.transport.port, 6742); 59 | test.equal(nodeId, barBase64); 60 | test.done(); 61 | } 62 | }; 63 | var discover = new Discover({ 64 | // inlineTrace: true, 65 | noCache: true, 66 | seeds: [{id: "baz", transport: {ip: "127.0.0.1", port: 6742}}], 67 | transport: transport 68 | }); 69 | discover.register({id: fooBase64}); 70 | // "foo" is the closest (and only) KBucket 71 | transport.emit('reached', {id: barBase64}); 72 | // "bar" is put in "foo" KBucket 73 | transport.emit('unreachable', {id: barBase64}); 74 | discover.find(barBase64, function (error, contact) { 75 | // because unreachable removed "bar" 76 | // we expect findNode request to happen 77 | }); 78 | }; 79 | 80 | test["on 'unreachable' removes the unreachable contact from non-kBucket LRU cache"] = function (test) { 81 | test.expect(8); 82 | var fooBase64 = new Buffer("foo").toString("base64"); 83 | var barBase64 = new Buffer("bar").toString("base64"); 84 | var transport = new events.EventEmitter(); 85 | var initial = true; 86 | transport.setTransportInfo = function (contact) { 87 | return contact; 88 | }; 89 | transport.findNode = function (contact, nodeId) { 90 | if (initial) { 91 | test.equal(contact.id, "baz"); 92 | test.equal(contact.transport.ip, "127.0.0.1"); 93 | test.equal(contact.transport.port, 6742); 94 | test.equal(nodeId, fooBase64); 95 | initial = false; 96 | } else { 97 | test.equal(contact.id, "baz"); 98 | test.equal(contact.transport.ip, "127.0.0.1"); 99 | test.equal(contact.transport.port, 6742); 100 | test.equal(nodeId, barBase64); 101 | test.done(); 102 | } 103 | }; 104 | var discover = new Discover({ 105 | // inlineTrace: true, 106 | seeds: [{id: "baz", transport: {ip: "127.0.0.1", port: 6742}}], 107 | transport: transport 108 | }); 109 | discover.register({id: fooBase64}); 110 | // "foo" is the closest (and only) KBucket 111 | transport.emit('reached', {id: barBase64}); 112 | // "bar" is put in "foo" KBucket 113 | transport.emit('unreachable', {id: barBase64}); 114 | discover.find(barBase64, function (error, contact) { 115 | // because unreachable removed "bar" 116 | // we expect findNode request to happen 117 | }); 118 | }; 119 | 120 | test["on 'unreachable' removes the unreachable contact if no KBuckets"] = function (test) { 121 | test.expect(2); 122 | var fooBase64 = new Buffer("foo").toString("base64"); 123 | var barBase64 = new Buffer("bar").toString("base64"); 124 | var transport = new events.EventEmitter(); 125 | transport.setTransportInfo = function (contact) { 126 | return contact; 127 | }; 128 | transport.findNode = function (contact, nodeId) { 129 | test.equal(contact.id, "baz"); 130 | test.equal(nodeId, barBase64); 131 | test.done(); 132 | }; 133 | var discover = new Discover({ 134 | // inlineTrace: true, 135 | seeds: [{id: "baz", transport: {ip: "127.0.0.1", port: 6742}}], 136 | transport: transport 137 | }); 138 | // no KBuckets, but we have data in cache that needs to be removed 139 | discover.lruCache.set(barBase64, {id: barBase64}); 140 | transport.emit('unreachable', {id: barBase64}); 141 | discover.find(barBase64, function (error, contact) { 142 | // because unreachable removed "bar" 143 | // we expect findNode request to happen 144 | }); 145 | }; 146 | -------------------------------------------------------------------------------- /test/register.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | register.js - discover.register(nodeId, vectorClock) test 4 | 5 | The MIT License (MIT) 6 | 7 | Copyright (c) 2013 Tristan Slominski 8 | 9 | Permission is hereby granted, free of charge, to any person 10 | obtaining a copy of this software and associated documentation 11 | files (the "Software"), to deal in the Software without 12 | restriction, including without limitation the rights to use, 13 | copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | copies of the Software, and to permit persons to whom the 15 | Software is furnished to do so, subject to the following 16 | conditions: 17 | 18 | The above copyright notice and this permission notice shall be 19 | included in all copies or substantial portions of the Software. 20 | 21 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 22 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 23 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 24 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 25 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 26 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 27 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 28 | OTHER DEALINGS IN THE SOFTWARE. 29 | 30 | */ 31 | 32 | "use strict"; 33 | 34 | var events = require('events'), 35 | Discover = require('../index.js'); 36 | 37 | var test = module.exports = {}; 38 | 39 | test['register() creates a new kBucket from randomly generated contact.id'] = function (test) { 40 | test.expect(3); 41 | var transport = new events.EventEmitter(); 42 | transport.setTransportInfo = function (contact) { 43 | return contact; 44 | }; 45 | transport.findNode = function (contact, nodeId) {}; 46 | var discover = new Discover({transport: transport}); 47 | var contact = discover.register({data: 'foo'}); 48 | test.ok(contact.id); 49 | test.equal(contact.vectorClock, 0); 50 | // TODO: doing this check is a design smell.. why am I reaching in here? 51 | test.equal(Object.keys(discover.kBuckets).length, 1); 52 | test.done(); 53 | }; 54 | 55 | test["register() registers 'ping' listener on newly created kBucket"] = function (test) { 56 | test.expect(3); 57 | var transport = new events.EventEmitter(); 58 | transport.setTransportInfo = function (contact) { 59 | return contact; 60 | }; 61 | transport.findNode = function (contact, nodeId) {}; 62 | var discover = new Discover({transport: transport}); 63 | var contact = discover.register({data: 'foo'}); 64 | test.ok(contact.id); 65 | var kBucket = discover.kBuckets[contact.id].kBucket; 66 | test.ok(kBucket); 67 | test.equal(kBucket.listeners('ping').length, 1); 68 | test.done(); 69 | }; 70 | 71 | test['register() stores registered contact info (enriched with transport info) with newly created kBucket'] = function (test) { 72 | test.expect(2); 73 | var transport = new events.EventEmitter(); 74 | transport.setTransportInfo = function (contact) { 75 | test.ok(true); 76 | contact.transport = {host: 'foo.com', port: 8888}; 77 | return contact; 78 | }; 79 | transport.findNode = function () {}; 80 | var discover = new Discover({transport: transport}); 81 | var contact = discover.register({data: {foo: 'bar'}}); 82 | test.deepEqual(discover.kBuckets[contact.id].contact, contact); 83 | test.done(); 84 | }; 85 | 86 | test['register() does not re-create an existing kBucket'] = function (test) { 87 | test.expect(1); 88 | var transport = new events.EventEmitter(); 89 | transport.setTransportInfo = function (contact) { 90 | return contact; 91 | }; 92 | transport.findNode = function (contact, nodeId) {}; 93 | var discover = new Discover({transport: transport}); 94 | var contact = discover.register({data: 'foo'}); 95 | // TODO: doing this check is a design smell.. why am I reaching in here? 96 | var originalKBucketReference = discover.kBuckets[contact.id]; 97 | discover.register(contact); 98 | test.ok(originalKBucketReference === discover.kBuckets[contact.id]); 99 | test.done(); 100 | }; 101 | 102 | test['register() updates previous contact info when re-registering'] = function (test) { 103 | test.expect(1); 104 | var transport = new events.EventEmitter(); 105 | transport.setTransportInfo = function (contact) { 106 | return contact; 107 | }; 108 | transport.findNode = function () {}; 109 | var discover = new Discover({transport: transport}); 110 | var contact = discover.register({data: {foo: 'bar'}}); 111 | contact.data = 'something else'; 112 | discover.register(contact); 113 | test.deepEqual(discover.kBuckets[contact.id].contact, contact); 114 | test.done(); 115 | }; 116 | 117 | test['register() calls transport.findNode() on the seeds if there are no ' + 118 | 'previously registered nodes'] = function (test) { 119 | test.expect(6); 120 | var fooBase64 = new Buffer("foo").toString("base64"); 121 | var barBase64 = new Buffer("bar").toString("base64"); 122 | var bazBase64 = new Buffer("baz").toString("base64"); 123 | var seeds = [ 124 | {id: bazBase64, ip: '127.0.0.1', port: 6744}, 125 | {id: barBase64, ip: '127.0.0.1', port: 6743} 126 | ]; 127 | var transport = new events.EventEmitter(); 128 | var first = true; 129 | transport.setTransportInfo = function (contact) { 130 | return contact; 131 | }; 132 | transport.findNode = function (contact, nodeId) { 133 | if (first) { 134 | first = false; 135 | test.equal(contact.id, seeds[0].id); 136 | test.equal(contact.ip, seeds[0].ip); 137 | test.equal(contact.port, seeds[0].port); 138 | } else { 139 | test.equal(contact.id, seeds[1].id); 140 | test.equal(contact.ip, seeds[1].ip); 141 | test.equal(contact.port, seeds[1].port); 142 | test.done(); 143 | } 144 | }; 145 | var discover = new Discover({seeds: seeds, transport: transport}); 146 | discover.register({id: fooBase64, data: 'foo'}); 147 | }; 148 | 149 | test['register() arbiters local contact info with remote contact if found'] = function (test) { 150 | test.expect(9); 151 | var fooBase64 = new Buffer("foo").toString("base64"); 152 | var barBase64 = new Buffer("bar").toString("base64"); 153 | var bazBase64 = new Buffer("baz").toString("base64"); 154 | var seeds = [ 155 | {id: bazBase64, ip: '127.0.0.1', port: 6744}, 156 | {id: barBase64, ip: '127.0.0.1', port: 6743} 157 | ]; 158 | var transport = new events.EventEmitter(); 159 | var first = true; 160 | transport.setTransportInfo = function (contact) { 161 | return contact; 162 | }; 163 | transport.findNode = function (contact, nodeId) { 164 | if (first) { 165 | first = false; 166 | test.equal(contact.id, seeds[0].id); 167 | test.equal(contact.ip, seeds[0].ip); 168 | test.equal(contact.port, seeds[0].port); 169 | } else { 170 | test.equal(contact.id, seeds[1].id); 171 | test.equal(contact.ip, seeds[1].ip); 172 | test.equal(contact.port, seeds[1].port); 173 | transport.emit('node', null, {id: fooBase64, data: "updated"}); 174 | } 175 | }; 176 | var discover = new Discover({seeds: seeds, transport: transport}); 177 | discover.register({id: fooBase64, data: 'foo'}); 178 | discover.find(fooBase64, function (error, contact) { 179 | test.ok(!error); 180 | test.equal(contact.id, fooBase64); 181 | test.equal(contact.data, "updated"); 182 | test.done(); 183 | }); 184 | }; 185 | 186 | test['register() queries closest nodes if not found on first round by querying' + 187 | ' seed nodes'] = function (test) { 188 | test.expect(1); 189 | var fooBase64 = new Buffer("foo").toString("base64"); 190 | var barBase64 = new Buffer("bar").toString("base64"); 191 | var bazBase64 = new Buffer("baz").toString("base64"); 192 | var fopBase64 = new Buffer("fop").toString("base64"); 193 | var seeds = [ 194 | {id: bazBase64, ip: '127.0.0.1', port: 6744}, 195 | {id: barBase64, ip: '127.0.0.1', port: 6743} 196 | ]; 197 | var transport = new events.EventEmitter(); 198 | transport.setTransportInfo = function (contact) { 199 | return contact; 200 | }; 201 | transport.findNode = function (contact, nodeId) { 202 | if (contact.id == seeds[1].id) { 203 | process.nextTick(function () { 204 | transport.emit('node', null, contact, nodeId, [{ 205 | id: fopBase64, ip: '192.168.0.1', port: 5553 206 | }]); 207 | }); 208 | } else if (contact.id == fopBase64) { 209 | process.nextTick(function () { 210 | transport.emit('node', null, contact, nodeId, { 211 | id: fooBase64, 212 | data: { 213 | foo: 'bar' 214 | } 215 | }); 216 | test.ok(true); // assert being here 217 | test.done(); 218 | }); 219 | } else { 220 | transport.emit('node', new Error("unreachable"), contact, nodeId); 221 | } 222 | }; 223 | var discover = new Discover({seeds: seeds, transport: transport}); 224 | discover.register({id: fooBase64, data: 'foo'}); 225 | }; -------------------------------------------------------------------------------- /test/unreachable.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | unreachable.js - discover.unreachable(contact) test 4 | 5 | The MIT License (MIT) 6 | 7 | Copyright (c) 2013 Tristan Slominski 8 | 9 | Permission is hereby granted, free of charge, to any person 10 | obtaining a copy of this software and associated documentation 11 | files (the "Software"), to deal in the Software without 12 | restriction, including without limitation the rights to use, 13 | copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | copies of the Software, and to permit persons to whom the 15 | Software is furnished to do so, subject to the following 16 | conditions: 17 | 18 | The above copyright notice and this permission notice shall be 19 | included in all copies or substantial portions of the Software. 20 | 21 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 22 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 23 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 24 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 25 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 26 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 27 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 28 | OTHER DEALINGS IN THE SOFTWARE. 29 | 30 | */ 31 | 32 | "use strict"; 33 | 34 | var events = require('events'), 35 | Discover = require('../index.js'); 36 | 37 | var test = module.exports = {}; 38 | 39 | test["unreachable() removes the unreachable contact from the closest KBucket"] = function (test) { 40 | test.expect(8); 41 | var fooBase64 = new Buffer("foo").toString("base64"); 42 | var barBase64 = new Buffer("bar").toString("base64"); 43 | var transport = new events.EventEmitter(); 44 | var initial = true; 45 | transport.setTransportInfo = function (contact) { 46 | return contact; 47 | }; 48 | transport.findNode = function (contact, nodeId) { 49 | if (initial) { 50 | test.equal(contact.id, "baz"); 51 | test.equal(contact.ip, "127.0.0.1"); 52 | test.equal(contact.port, 6742); 53 | test.equal(nodeId, fooBase64); 54 | initial = false; 55 | } else { 56 | test.equal(contact.id, "baz"); 57 | test.equal(contact.ip, "127.0.0.1"); 58 | test.equal(contact.port, 6742); 59 | test.equal(nodeId, barBase64); 60 | test.done(); 61 | } 62 | }; 63 | var discover = new Discover({ 64 | // inlineTrace: true, 65 | seeds: [{id: "baz", ip: "127.0.0.1", port: 6742}], 66 | transport: transport 67 | }); 68 | discover.register({id: fooBase64}); 69 | // "foo" is the closest (and only) KBucket 70 | transport.emit('reached', {id: barBase64}); 71 | // "bar" is put in "foo" KBucket 72 | discover.unreachable({id: barBase64}); 73 | discover.find(barBase64, function (error, contact) { 74 | // because unreachable removed "bar" 75 | // we expect findNode request to happen 76 | }); 77 | }; 78 | 79 | test["unreachable() removes the unreachable contact from non-kBucket LRU cache"] = function (test) { 80 | test.expect(8); 81 | var fooBase64 = new Buffer("foo").toString("base64"); 82 | var barBase64 = new Buffer("bar").toString("base64"); 83 | var transport = new events.EventEmitter(); 84 | var initial = true; 85 | transport.setTransportInfo = function (contact) { 86 | return contact; 87 | }; 88 | transport.findNode = function (contact, nodeId) { 89 | if (initial) { 90 | test.equal(contact.id, "baz"); 91 | test.equal(contact.transport.ip, "127.0.0.1"); 92 | test.equal(contact.transport.port, 6742); 93 | test.equal(nodeId, fooBase64); 94 | initial = false; 95 | } else { 96 | test.equal(contact.id, "baz"); 97 | test.equal(contact.transport.ip, "127.0.0.1"); 98 | test.equal(contact.transport.port, 6742); 99 | test.equal(nodeId, barBase64); 100 | test.done(); 101 | } 102 | }; 103 | var discover = new Discover({ 104 | // inlineTrace: true, 105 | seeds: [{id: "baz", transport: {ip: "127.0.0.1", port: 6742}}], 106 | transport: transport 107 | }); 108 | discover.register({id: fooBase64}); 109 | // "foo" is the closest (and only) KBucket 110 | transport.emit('reached', {id: barBase64}); 111 | // "bar" is put in "foo" KBucket 112 | discover.unreachable({id: barBase64}); 113 | discover.find(barBase64, function (error, contact) { 114 | // because unreachable removed "bar" 115 | // we expect findNode request to happen 116 | }); 117 | }; 118 | -------------------------------------------------------------------------------- /test/unregister.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | unregister.js - discover.unregister(nodeId) test 4 | 5 | The MIT License (MIT) 6 | 7 | Copyright (c) 2013 Tristan Slominski 8 | 9 | Permission is hereby granted, free of charge, to any person 10 | obtaining a copy of this software and associated documentation 11 | files (the "Software"), to deal in the Software without 12 | restriction, including without limitation the rights to use, 13 | copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | copies of the Software, and to permit persons to whom the 15 | Software is furnished to do so, subject to the following 16 | conditions: 17 | 18 | The above copyright notice and this permission notice shall be 19 | included in all copies or substantial portions of the Software. 20 | 21 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 22 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 23 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 24 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 25 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 26 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 27 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 28 | OTHER DEALINGS IN THE SOFTWARE. 29 | 30 | */ 31 | 32 | "use strict"; 33 | 34 | var events = require('events'), 35 | Discover = require('../index.js'); 36 | 37 | var test = module.exports = {}; 38 | 39 | test['unregister() succeeds if kBucket does not exist'] = function (test) { 40 | test.expect(1); 41 | var discover = new Discover(); 42 | discover.unregister({id: new Buffer("foo").toString("base64")}); 43 | test.ok(true); // nothing breaks 44 | test.done(); 45 | }; 46 | 47 | test['unregister() removes existing kBucket'] = function (test) { 48 | test.expect(3); 49 | var transport = new events.EventEmitter(); 50 | transport.setTransportInfo = function (contact) { 51 | return contact; 52 | }; 53 | transport.findNode = function (contact, nodeId) {}; 54 | var discover = new Discover({transport: transport}); 55 | test.equal(Object.keys(discover.kBuckets).length, 0); 56 | var contact = discover.register(); 57 | test.equal(Object.keys(discover.kBuckets).length, 1); 58 | discover.unregister(contact); 59 | test.equal(Object.keys(discover.kBuckets).length, 0); 60 | test.done(); 61 | }; --------------------------------------------------------------------------------