├── .gitignore ├── .travis.yml ├── .vscode └── launch.json ├── README.md ├── init.cmd ├── package.json ├── process ├── datastore.js ├── index.js ├── kernel.js ├── membership.js ├── server.js └── topology.js ├── screenshot ├── membership.PNG ├── membership_fd.PNG └── membership_fd_24.PNG └── test ├── test_datastore.js ├── test_e2e.js ├── test_kernel.js ├── test_membership.js ├── test_server.js └── test_topology.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "stable" 4 | cache: 5 | directories: 6 | - "node_modules" 7 | after_success: 8 | - npm run coveralls -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible Node.js debug attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Launch Program", 11 | "program": "${workspaceRoot}/process/index.js", 12 | "args": [ 13 | "8081", 14 | "8080" 15 | ] 16 | } 17 | ] 18 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # dht-node 2 | 3 | [![Build Status](https://travis-ci.org/mebjas/dht-node.svg?branch=master)](https://travis-ci.org/mebjas/dht-node) 4 | 5 | I did a course on `Distributed Systems` during BTech and also took a course on on `Cloud Computing Concepts 1` by Dr Indranil Gupta (UIUC) long back and for long I have been thinking about trying out the principles explained in the course together as something meaningful. There are assignments in the course which you need to finish, but they don't require you to just club all of them together. I wish to try something out where most of the major concepts are used; 6 | 7 | We use distributed systems alot in our development pipelines but most of the concepts are abstracted out by the cloud solutions. I work for Microsoft Azure, and we use concepts like scheduling, queuing, distributed caching all the time but most of these are provided as a SaaS offering - hence it's mostly: 8 | 9 | ``` 10 | Azure.Client.CreateIfNotExists() 11 | Azure.Client.JustDoMostOfItForMe() 12 | Azure.Thanks() 13 | ``` 14 | 15 | Well I'm actually gratefull for all the offerings we have in 2017, it makes development much faster and takes a lot of headache away. So I'm picking up this fun exercise to refresh the underlying concepts `IN MEMORY :D`. 16 | 17 | ## Task: Building a Distributed Key Value Store 18 | 19 | ### A key-value store, or key-value database, is a data storage paradigm designed for storing, retrieving, and managing associative arrays, a data structure more commonly known today as a dictionary or hash. A distributed Key Value store is one where data is replicated across different nodes such that there is: 20 | - High Availability, and 21 | - No single point of failure 22 | 23 | ## Distributed Key Value Store 24 | This will provide simple abstraction for CRUD operation on a key value pair. The operations will be exposed using simple rest calls like 25 | ```` 26 | POST http://localhost:8000/ { key: , value: } 27 | GET http://localhost:8001/?key= 28 | DELETE http://localhost:8002/?key= 29 | ```` 30 | 31 | Plan is to follow a cassandra like architecture where nodes maintain a virtual ring topology and location of key is retrieved based on hash function. Few things I'm gonna follow: 32 | 33 | - Consistency type: Strong, Quorum based 34 | - Membership protocol: SWIM 35 | - No authentication or SSL support of as of now - plain old open `http` 36 | - Local clocks will be used, as they are already in sync with system clock. 37 | - The data will be stored in memory (in context of the process), no commit logs will be maintained; If all process die or some most die before replication data will be lost; 38 | 39 | ## Setup and tests 40 | Pre requisite 41 | - Download nodejs and npm for windows 42 | - Clone this repo: `git clone https://github.com/mebjas/dht-node.git` 43 | - Install the libraries - `npm install` 44 | - Run Tests - `npm test` 45 | - Initialize 24 nodes: `.\init.cmd` 46 | 47 | To run just `N` nodes skip the last step and manually invoke an instance using: 48 | ```cmd 49 | node process\index.js 50 | ``` 51 | `` - port number or this instance 52 | 53 | `` - port number of the introducer to which join request will be sent; Not needed for first instance - it will assume itself to be first instance because of this; I'm not going for any automatic discovery or centralized discovery mechanism here; 54 | 55 | ## Top level architecture 56 | ``` 57 | ___________________________ 58 | [ Rest + No Auth + No SSL ] 59 | --------------------------- 60 | _______________ 61 | [ Application ] 62 | --------------- 63 | 64 | ___________ _______________ _________________ 65 | [ Storage ] [ Replication ] [ Stabalization ] 66 | ----------- --------------- ----------------- 67 | _________________________ 68 | [ Virtual Ring Topology ] 69 | ------------------------- 70 | 71 | _______________________ 72 | [ Membership Protocol ] 73 | ----------------------- 74 | ``` 75 | 76 | ## TASK 1.1: Membership Protocol 77 | Implementing SWIM Protocol, where the membership gossips are piggibacked on PING, PING_REQ, JOINREQ, ACK messages. In a given protocol period a node will select a random node and send a ping. If it get's `ACK` well and good. It'd stay idle till completion of protocol period. 78 | 79 | If it doesn't get an `ACK`, it'd send a `PING_REQ` message to K random nodes, and they will ping the target node. If any of then send an `ACK` with in the target period the node is conisedered alive else it's moved to suspicions state; it no one confirms it's alive in time < 2 * protocol period - it's removed from the list; Eventually every other node will detect the node's failure by pinging it first hand or due to piggybacked response; 80 | 81 | These screenshots are when 8 nodes were joined and two of them crashed 82 | 83 | ![* nodes joined](https://raw.githubusercontent.com/mebjas/dht-node/master/screenshot/membership.PNG) 84 | 85 | ![* nodes joined](https://raw.githubusercontent.com/mebjas/dht-node/master/screenshot//membership_fd.PNG) 86 | 87 | Detecting failures in 24 nodes :D 88 | ![* nodes joined](https://raw.githubusercontent.com/mebjas/dht-node/master/screenshot/membership_fd_24.PNG) 89 | 90 | ## TASK 1.2: Testing Membership Protocol 91 | 1. Simple unit testing using mocha framework for node js. It's pretty new to me but seems pretty powerful. I have done basic testing will do advanced ones later. Also since the library depends on HTTP calls some mocks / stubs shall be needed. 92 | 2. To Be Done is e2e testing of membership protocol
93 | - Create nodes
94 | - JOIN nodes
95 | - FAILURES & Failure detection
96 | - Time to detection, accuracy of detection
97 | - Emulation of Congestion / Message Drop scenario
98 | - High load setup
99 | 100 | ## TASK 2.1: Virtual ring topology 101 | Now that underlying membership protocol works reasonably well and it can both detect failures and disemminate information in Gossip style to other nodes we can build the next layer - the virtual topology. In Cassandra nodes are placed in a virtual ring and the mapping of both keys and nodes position is done using a hash function. 102 | 103 | Each node is a member of the ring and their position is calculated based on a hash function. I have picked the most basic one, as simple as this: 104 | ```js 105 | hash: function(key) { 106 | return key - 8080; 107 | } 108 | 109 | positionInRing = hash(port_no) 110 | ``` 111 | ### Changed hash function to be as simple as `key - 8080`. It can take values between 8080 & 8336 and give a unique ring id between `[0, 256)`. Thus max ring size if 256 for now; 112 | 113 | ### Some other points 114 | - Ideally, any no of nodes can be created; given their hash shouldn't collide; 115 | - Max Replication per key was set to 3, can be tested with more; 116 | - Quorum count in this case was set to 2; 117 | 118 | Flow: 119 | 1. For every request (CRUD) the hash of key is computed and it gives an index in range `[0, no of nodes)`. Replications are done in this index to next `X` nodes in ring (given by `MaxReplicationCount`). If no of nodes is less than this value all nodes replicate all key value pair; 120 | 2. Requests are sent to all replicas and once the quorum has replied with +ve response the response is sent to client - CRUD; 121 | 3. In case of failure to obtain a quorum request is replied with failure; 122 | 123 | 124 | ## TASK 2.2: Test Virtual ring topology 125 | This part is to be done, need to add unit tests here; 126 | 127 | ## TASK 3.1: Storage Replication & Stabalisation 128 | Storage of data is abstracted out by `datastore.js`. Replication is done based on this approach: 129 | 130 | ### 3.1.1 WRITE REQUEST: 131 | Client can send a `WRITE REQUEST` to any node, with a key-value pair; During the life-cycle of this request, this node will act as a coordinator; The cordinator will calculate the hash of the key and will be able to identify the nodes where this key should be stored. Essentially the hash function will point to one node, let's call it primary store for the key. In my approach there will be `NoOfReplicas = MaxReplicationCount - 1` replicas as well. The `NoOfReplicas` nodes after the primary store for the key in the ring will be selected as replicas; The coordinator will send the internal write request to each of this node and wait for response; As soon as responses from `quorumCount` no of nodes come back, the write request will be set as done and success code is returned to client; Otherwise in case of timeout or failed requests `> MaxReplicationCount - quorumCount` write request will be considered as failed; Here, 132 | 133 | - Concepts like hinted handoff is not implemeted as out of scope; 134 | - In case of internal write if some of the nodes actually write the response in their memory, but qourum is not achieved, they should revert back - this is not implemented; 135 | 136 | ``` 137 | Note: Sending a WRITE REQUEST with a key that already exists wil act like Update 138 | ``` 139 | 140 | ### 3.1.2 READ REQUEST 141 | Pretty much similar to WRITE Request, the request is sent to replicas and once response is recieved from atlease `quorumCount` no of replicas and values are consistent, the value is responded back. In case some the replicas have older values - `READ REPAIR` is initiated for them by the coordinator; If response is not recieved from `quorumCount` with value - `404, Not Found` is responded back; If less than `quorumCount` respond with a value, it might be because `DELETE` failed them or `failed write` worked for them. In any case we can either initiate an internal `DELETE` request to these or leave it be; 142 | 143 | ### 3.1.3 DELETE REQUEST 144 | Similar to above two, initate request to all replicas and respond back `OK` if quorum responds with that number; 145 | 146 | ### Task 3.1.2: Stabalisation 147 | Stabalisation need to be done every time a new node joins or an existing one leaves the system. In both the cases the structure of the ring changes a little and mapping of the keys to the server changes; (In my implementation! There are better methods like [Consistent Hashing](https://en.wikipedia.org/wiki/Consistent_hashing) where only K/n keys need to be remapped) and every node that detect a failure or the joining of the node kicks in the stabalisation protocol; I have gone for most naive implementation where every key in the store is checked for it's mapping and respective mappings are sent to the replicas. These replicas will store the key value pair if they dont already have it. Once it has received response from each node, the node that detected will remove unwanted keys from it's store; 148 | 149 | ## TASK 3.2: Test Storage Replication & Stabalisation 150 | TODO: Add the test cases for storage class, replication & stabalisation 151 | 152 | ## TASK 4: Rest API + Final Test 153 | To the client the nodes expose three apis 154 | 155 | /Get a Key 156 | ``` 157 | GET /s/key?key=key9 HTTP/1.1 158 | Host: localhost:8083 159 | ``` 160 | 161 | /Set a key 162 | ``` 163 | POST /s/key HTTP/1.1 164 | Host: localhost:8080 165 | Content-Type: application/json 166 | 167 | { 168 | "key": "key9", 169 | "value": "value9" 170 | } 171 | ``` 172 | 173 | /Delete a key 174 | ``` 175 | DELETE /s/key?key=key9 HTTP/1.1 176 | Host: localhost:8081 177 | ``` 178 | #### Final test 179 | I'm yet to write automated E2E tests, however I have manually tested the code and it seem to work pretty well; You could Write / Update a key from one node, and read it from other; Since the data is stored in memory - the request response time is pretty fast; 180 | 181 | It's cool to see all nodes detect a new node has joined, and when all consoles are up you can actually see gossip spreading; Then every node kicks in stabalisation protocol and all keys can be accessed from this new node; 182 | 183 | Similarly, when a node fails, eventually each node detects the failure and kicks stabalisation so that data is not lost; However, if all replicas of a set of keys die together - that data is lost; 184 | 185 | ### TODOS: 186 | - Automated E2E testing, unit testing bug fixes 187 | - Change localhost hardcoding, test it in different machines on a network 188 | - Performance testing - Same Machine / Different Machines 189 | - Add more advanced functionalities, more optimised algorithms, 190 | - Leader Elections, Clock Syncing :O 191 | - Missile launcher and self driving car! 192 | 193 | ## Conclusion 194 | Distributed systems are super awesome; Concepts are pretty cool; It's fair to use abstractions provided to us by well tested libraries and cloud offerings but I'd totally recommed to try things out from scratch if you have a free weekend; The code for a node is available in [.\process](https://github.com/mebjas/dht-node/tree/master/process) directory; I know code design might not be upto mark - it was a more of a hackathon; I'll try to find some time to refactor it! 195 | 196 | ## References: 197 | - Coursera Cloud Computing Concepts, Part 1 - [https://www.coursera.org/learn/cloud-computing/home/welcome](https://www.coursera.org/learn/cloud-computing/home/welcome) 198 | - Building a Distributed Fault-Tolerant Key-Value Store - [http://blog.fourthbit.com/2015/04/12/building-a-distributed-fault-tolerant-key-value-store](http://blog.fourthbit.com/2015/04/12/building-a-distributed-fault-tolerant-key-value-store) 199 | - Consistent hashing in Cassandra - [https://blog.imaginea.com/consistent-hashing-in-cassandra/](https://blog.imaginea.com/consistent-hashing-in-cassandra/) 200 | -------------------------------------------------------------------------------- /init.cmd: -------------------------------------------------------------------------------- 1 | start cmd /C node process\index.js 8080 2 | timeout /t 1 3 | start cmd /C node process\index.js 8081 8080 4 | timeout /t 1 5 | start cmd /C node process\index.js 8082 8080 6 | start cmd /C node process\index.js 8083 8081 7 | timeout /t 1 8 | start cmd /C node process\index.js 8084 8080 9 | start cmd /C node process\index.js 8085 8081 10 | start cmd /C node process\index.js 8086 8081 11 | start cmd /C node process\index.js 8087 8081 12 | start cmd /C node process\index.js 8088 8081 13 | 14 | timeout /t 1 15 | start cmd /C node process\index.js 8089 8080 16 | start cmd /C node process\index.js 8090 8082 17 | start cmd /C node process\index.js 8091 8082 18 | start cmd /C node process\index.js 8092 8081 19 | start cmd /C node process\index.js 8093 8081 20 | start cmd /C node process\index.js 8094 8080 21 | 22 | 23 | timeout /t 1 24 | start cmd /C node process\index.js 8095 8082 25 | start cmd /C node process\index.js 8096 8082 26 | start cmd /C node process\index.js 8097 8087 27 | start cmd /C node process\index.js 8098 8087 28 | start cmd /C node process\index.js 8099 8081 -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dht-node", 3 | "version": "0.0.0", 4 | "description": "a distributed key value store from scratch", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "mocha", 8 | "cover": "istanbul cover _mocha", 9 | "coveralls": "npm run cover -- --report lcovonly && cat ./coverage/lcov.info | coveralls" 10 | }, 11 | "keywords": [ 12 | "distributed", 13 | "systems" 14 | ], 15 | "author": "minhazav@gmail.com", 16 | "license": "MIT", 17 | "dependencies": { 18 | "body-parser": "^1.17.2", 19 | "chai": "^4.1.2", 20 | "chai-http": "^3.0.0", 21 | "express": "^4.15.4", 22 | "mocha": "^3.5.0", 23 | "request": "^2.81.0", 24 | "sha1": "^1.1.1", 25 | "sprintf": "^0.1.5" 26 | }, 27 | "devDependencies": { 28 | "coveralls": "^2.13.1", 29 | "istanbul": "^0.4.5", 30 | "mocha-lcov-reporter": "^1.3.0" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /process/datastore.js: -------------------------------------------------------------------------------- 1 | // file stores data store class 2 | const Kernel = require('./kernel.js') 3 | 4 | // abstraction over store object 5 | var Datastore = function() { 6 | // private store object 7 | var store = {} 8 | 9 | // add / update an entry 10 | this.set = function(key, value, timestamp = null) { 11 | if (!key || !value) { 12 | throw Error("ArgumentException") 13 | } 14 | 15 | if (!timestamp) { 16 | timestamp = Kernel.getTimestamp() 17 | } else { 18 | timestamp = parseInt(timestamp); 19 | } 20 | 21 | if (key in store) { 22 | if (store[key].timestamp > timestamp) { 23 | throw Error ("store fresher than requested") 24 | } 25 | } 26 | 27 | store[key] = { 28 | value: value, 29 | timestamp: timestamp 30 | } 31 | } 32 | 33 | // get the value for the input key 34 | this.get = function(key) { 35 | if (!key) { 36 | throw Error("ArgumentException") 37 | } 38 | 39 | if (key in store) return store[key]; 40 | return null; 41 | } 42 | 43 | // check if the store has the key 44 | this.has = function(key) { 45 | if (!key) { 46 | throw Error("ArgumentException") 47 | } 48 | 49 | return key in store; 50 | } 51 | 52 | // delete a key 53 | this.delete = function(key) { 54 | if (!key) { 55 | throw Error("ArgumentException") 56 | } 57 | 58 | if (key in store) { 59 | delete store[key]; 60 | } else { 61 | throw Error('key not found in store') 62 | } 63 | } 64 | 65 | // Remove the keys that do not belong to self 66 | this.removeStabalsiedKeys = function (index, max, maxReplicas) { 67 | if (index === null || index === undefined || !max || !maxReplicas) { 68 | throw Error("ArgumentException") 69 | } 70 | 71 | var $this = this; 72 | Object.keys(store).forEach(function(key) { 73 | var indexes = Kernel.hashKey(key, max, maxReplicas); 74 | if (indexes.indexOf(index) == -1) { 75 | $this.delete(key); 76 | } 77 | }); 78 | } 79 | 80 | // iterate through all keys and look for remapping 81 | this.getRemappedData = function(index, max, maxReplicas) { 82 | if (index === null || index === undefined || !max || !maxReplicas) { 83 | throw Error("ArgumentException") 84 | } 85 | 86 | var $this = this; 87 | var stabalisationMetadata = {}; 88 | Object.keys(store).forEach(function(key) { 89 | var indexes = Kernel.hashKey(key, max, maxReplicas); 90 | // if (indexes.indexOf(index) == -1) { 91 | indexes.forEach(function(_index) { 92 | if (_index === index) return; 93 | 94 | if (!(_index in stabalisationMetadata)) { 95 | stabalisationMetadata[_index] = []; 96 | } 97 | 98 | stabalisationMetadata[_index].push({key: key, value: $this.get(key)}); 99 | }); 100 | // } 101 | }); 102 | 103 | return stabalisationMetadata; 104 | } 105 | } 106 | 107 | module.exports = Datastore; 108 | 109 | 110 | 111 | -------------------------------------------------------------------------------- /process/index.js: -------------------------------------------------------------------------------- 1 | // entry point for the code 2 | // the node process should be initialized with 3 | // 1. the port no for this process 4 | // 2. port no of the introducer; if this is empty this process 5 | // is the first process. 6 | const sprintf = require("sprintf").sprintf; 7 | const Server = require("./server.js") 8 | 9 | if (process.argv.length < 3) { 10 | console.log("Incorrect number of arguments; abort;") 11 | process.exit() 12 | } 13 | 14 | var port = parseInt(process.argv[2]), introducer; 15 | if (port < 1) { 16 | console.log("Incorrect port number; abort;") 17 | process.exit() 18 | } 19 | 20 | if (process.argv.length > 3) { 21 | introducer = parseInt(process.argv[3]) 22 | if (introducer < 1) { 23 | console.log("Incorrect introducer port number; abort;") 24 | process.exit() 25 | } else if (port == introducer) { 26 | console.log("port cannont be same as introducer; abort;") 27 | process.exit() 28 | } 29 | } 30 | 31 | console.log( 32 | sprintf("Process spawned with port: %d introducer: %s", 33 | port, (introducer) ? introducer : "NONE")); 34 | 35 | // Bind to port; and INIT 36 | var server = new Server(port, introducer) 37 | -------------------------------------------------------------------------------- /process/kernel.js: -------------------------------------------------------------------------------- 1 | //// library to maintain core methods 2 | //// Most of the network calls shall be made from here. 3 | //// Mutliple instances of this class are not needed; 4 | //// this can pretty well be a singleton class; 5 | 6 | const request = require('request'); 7 | const sha1 = require('sha1'); 8 | const sprintf = require('sprintf').sprintf; 9 | 10 | var Kernel = { 11 | // types of request 12 | RequestTypes: { 13 | POST: "POST", 14 | GET: "GET", 15 | DELETE: "DELETE" 16 | }, 17 | 18 | // Constants 19 | Constants: { 20 | TestEnv: "test", 21 | VerboseEnv: "verbose", 22 | SilentEnv: "silent" 23 | }, 24 | 25 | // disemminator of any request 26 | // @param: port (int) 27 | send: function(port, path, type, object, callback, errcallback) { 28 | if (!port) { 29 | throw Error("ArgumentException - port"); 30 | } 31 | 32 | if (!path) { 33 | throw Error("ArgumentException - path"); 34 | } 35 | 36 | if (!type) { 37 | throw Error("ArgumentException - type"); 38 | } 39 | 40 | // TODO: add support for network urls 41 | var url = sprintf("http://localhost:%d/%s", port, path); 42 | 43 | // callback for requests 44 | var requestCallback = (err, response, body) => { 45 | if (err && errcallback) errcallback(err); 46 | if (!err && callback) callback(response, body); 47 | } 48 | 49 | switch (type) { 50 | case "GET": 51 | url += '?' +object; 52 | request.get(url, requestCallback); 53 | break; 54 | 55 | case "POST": 56 | request.post(url, {form: object}, requestCallback); 57 | break; 58 | 59 | case "DELETE": 60 | url += '?' +object; 61 | request.delete(url, requestCallback); 62 | break; 63 | 64 | default: 65 | throw Error(sprintf("Unknown request type: %s", type)) 66 | } 67 | }, 68 | 69 | // method to get current timestamp 70 | getTimestamp: function() { 71 | return (new Date()).getTime(); 72 | }, 73 | 74 | // random number generator 75 | random: function(min, max) { 76 | if (min > max) { 77 | throw Error("Invalid args; min need to be <= max"); 78 | } 79 | 80 | return Math.floor((Math.random() * (max - min) % (max-min))) + min; 81 | }, 82 | 83 | // Method to shuffle an array 84 | shuffle: function(array) { 85 | if (!array) { 86 | throw Error("ArgumentException"); 87 | } 88 | 89 | var currentIndex = array.length, temporaryValue, randomIndex; 90 | 91 | // While there remain elements to shuffle... 92 | while (0 !== currentIndex) { 93 | if (Math.random() > 0.5) continue; 94 | 95 | // Pick a remaining element... 96 | randomIndex = Math.floor(Math.random() * currentIndex); 97 | currentIndex -= 1; 98 | 99 | // And swap it with the current element. 100 | temporaryValue = array[currentIndex]; 101 | array[currentIndex] = array[randomIndex]; 102 | array[randomIndex] = temporaryValue; 103 | } 104 | 105 | // shallow copy? 106 | tmp = [] 107 | array.forEach(function(a) {tmp.push(a)}) 108 | return tmp; 109 | }, 110 | 111 | // Method to calculate hash count of any key 112 | hash: function(key) { 113 | if (!key) { 114 | throw Error("ArgumentException"); 115 | } 116 | 117 | var hash = 0 118 | for (i = 0; i < key.length; i++) { 119 | if (key[i].charCodeAt(0) < 97) { 120 | hash += (key[i].charCodeAt(0) - 48); 121 | } else { 122 | hash += ((key[i].charCodeAt(0) - 97) + 10); 123 | } 124 | } 125 | return hash; 126 | }, 127 | 128 | // hash function for port. Note: this is very naive method 129 | // But will give unique values for ports between [8080, 8336) 130 | hashPort: function(port) { 131 | if (!port) { 132 | throw Error("ArgumentException"); 133 | } 134 | 135 | if (port < 8080 || port >= 8336) { 136 | throw Error("port has to be in range of [8080, 8336)") 137 | } 138 | 139 | // return this.hash(sha1(port).substr(0, 40)) 140 | return port - 8080; 141 | }, 142 | 143 | // find the index position of given key in DHT 144 | hashKey: function(key, max, maxReplicas) { 145 | if (max < 1 || maxReplicas < 1 || !key) { 146 | throw Error("ArgumentException"); 147 | } 148 | 149 | var indexes = []; 150 | if (max < maxReplicas) { 151 | for(i = 0; i < max; i++) indexes.push(i); 152 | } else { 153 | var _hash = this.hash(sha1(key)); 154 | for (i = 0; i < maxReplicas; i++) { 155 | indexes.push((_hash + i) % max); 156 | } 157 | } 158 | return indexes; 159 | }, 160 | 161 | // Method to check if 162 | isAReplica: function(index1, index2, ringSize) { 163 | throw Error("NotImplementedException") 164 | } 165 | } 166 | 167 | module.exports = Kernel; 168 | -------------------------------------------------------------------------------- /process/membership.js: -------------------------------------------------------------------------------- 1 | // file has class and methods to deal with membership protocol 2 | // attempted to implement SWIM protocol 3 | 4 | const express = require('express') 5 | const sprintf = require('sprintf').sprintf; 6 | const Kernel = require('./kernel.js'); 7 | 8 | // An enum representing node state 9 | var NodeStateEnum = { 10 | Active: "active", 11 | Suspicious: "suspicious" 12 | } 13 | 14 | // The membership class - constuctor 15 | var Membership = function(app, port, joincb, churncb) { 16 | // ----------- PRIVATE VARS -------------------------------- 17 | const ENVIRONMENT = process.env.NODE_ENV; 18 | var $this = this; 19 | 20 | // ----------- PUBLIC VARS -------------------------------- 21 | // initialize with these variables 22 | // TODO: make all these private variables 23 | // bring in the public methods 24 | this.app = app; 25 | this.port = port; 26 | this.joincb = joincb; 27 | this.churncb = churncb; 28 | 29 | // Membership list - initialised with self 30 | // TODO: make this member private, provide public methods 31 | // to expose; 32 | this.list = {}; 33 | this.pinglist = []; 34 | 35 | // local counts 36 | this.joinReqRetryCount = 0; // count of attempts of joinreq 37 | this.joinReqRetryCountThreshold = 3; // threshold for above count 38 | this.joinReqRetryCountTimeout = 1000; // timeout for join req 39 | this.protocolPeriod = 2 * 1000; // protocol period 40 | this.KMax = 1; // no of K for ping_req 41 | this.suspisionLimit = 2; // No of times of protocol period 42 | // a node under suspision will be 43 | // kept in quarentine 44 | 45 | // ------------ private methods ---------------------------------------- 46 | // Method to generate a list entry 47 | var createListEntry = (heartbeat, timestamp, status) => { 48 | if (!status) status = NodeStateEnum.Active; 49 | 50 | return { 51 | heartbeat: heartbeat, 52 | timestamp: timestamp, 53 | status: status 54 | }; 55 | } 56 | 57 | // Get a random node to ping, round robin on all nodes 58 | // Algorithm: Every time the ping list is empty, it's filled 59 | // with shuffled array of all other nodes active in membership list 60 | // The keep shifting array untill an active node is found; 61 | var getNextNodeToPing = () => { 62 | // randomly choose an entry other than self 63 | var receiverPort = 0; 64 | while (!receiverPort) { 65 | if (Object.keys($this.list).length <= 1) return 0; 66 | if ($this.pinglist.length == 0) { 67 | 68 | tmp = [] 69 | Object.keys($this.list).forEach(function(key) { 70 | if (key != $this.port && $this.list[key].status == NodeStateEnum.Active) 71 | tmp.push(key); 72 | }); 73 | if (tmp.length == 0) return 0; 74 | $this.pinglist = Kernel.shuffle(tmp); 75 | } 76 | var key = $this.pinglist.shift(); 77 | if (key in $this.list && $this.list[key].status == NodeStateEnum.Active) 78 | receiverPort = key; 79 | } 80 | return parseInt(receiverPort); 81 | } 82 | 83 | // ----------- PUBLIC METHODS -------------------------------------------- 84 | // Helper method to add a new node entry to list 85 | this.addToList = (port, heartbeat) => { 86 | this.list[port] = createListEntry(heartbeat, Kernel.getTimestamp()); 87 | 88 | if (ENVIRONMENT != Kernel.Constants.TestEnv) { 89 | console.log("JOINED: " +port); 90 | } 91 | 92 | if (this.joincb) this.joincb(port); 93 | } 94 | 95 | // Helper class to update self heartbeat 96 | this.updateMyHeartbeat = () => { 97 | this.list[this.port].heartbeat = Kernel.getTimestamp(); 98 | } 99 | 100 | // Helper method to update the membership list 101 | this.updateList = (port, status, heartbeat = null) => { 102 | // todo: if during a gossip, the node recieves itself 103 | // as suspicious or failed to other node, it should send 104 | // a gossip with updated incarnation number 105 | 106 | if (port === this.port) return this.updateMyHeartbeat(); 107 | 108 | if (heartbeat == null) { 109 | if (!(port in this.list)) return; 110 | 111 | this.list[port] = 112 | createListEntry(Kernel.getTimestamp(), Kernel.getTimestamp(), status); 113 | } else if (this.list[port].heartbeat < heartbeat) { 114 | this.list[port] = 115 | createListEntry(heartbeat, Kernel.getTimestamp(), status); 116 | 117 | // if the state comes out to be suspicious 118 | // remove the entry after some timeout 119 | if (status === NodeStateEnum.Suspicious) { 120 | setTimeout(function() { 121 | if (!(port in this.list)) return; 122 | if (this.list[port].status !== NodeStateEnum.Active) { 123 | if (ENVIRONMENT !== Kernel.Constants.TestEnv) { 124 | console.log("failed: %s", port); 125 | } 126 | 127 | delete this.list[port]; 128 | if ($this.churncb) $this.churncb(port) 129 | } else { 130 | if (ENVIRONMENT !== Kernel.Constants.TestEnv) { 131 | console.log("suspicion over: %s", port) 132 | } 133 | } 134 | }.bind(this), this.protocolPeriod * this.suspisionLimit); 135 | } 136 | } 137 | } 138 | 139 | // Method to initialize listeners 140 | this.init = () => { 141 | // listener to join request 142 | this.app.get('/m/JOINREQ', function (req, res) { 143 | // TODO: null checks 144 | 145 | var reqPort = parseInt(req.query.port); 146 | if (ENVIRONMENT === Kernel.Constants.VerboseEnv) { 147 | console.log("m/JOINREQ FROM: ", req.query); 148 | } 149 | 150 | var heartbeat = parseInt(req.query.heartbeat); 151 | $this.addToList(reqPort, heartbeat) 152 | 153 | // send JOINREP 154 | $this.updateMyHeartbeat(); 155 | res.json({ list: $this.list }); 156 | }); 157 | 158 | // listener to ping request 159 | this.app.post('/m/PING', function( req, res) { 160 | if (ENVIRONMENT === Kernel.Constants.VerboseEnv) { 161 | console.log("m/PING from ", req.query.port); 162 | } 163 | 164 | var list = req.body.list; 165 | $this.mergeList(list); 166 | 167 | $this.updateMyHeartbeat(); 168 | res.json({list: $this.list}) 169 | }); 170 | 171 | // listener to ping_req 172 | this.app.post('/m/PINGREQ', function(req, res) { 173 | var list = req.body.list; 174 | $this.mergeList(list); 175 | 176 | var target = parseInt(req.query.target); 177 | if (ENVIRONMENT === Kernel.Constants.VerboseEnv) { 178 | console.log(sprintf("m/PINGREQ from %s, target = %d", 179 | req.query.port, target)); 180 | } 181 | 182 | // ping this targer if response - send ack to this request, else nack 183 | $this.updateMyHeartbeat(); 184 | $this.sendPing(res, target); 185 | }); 186 | 187 | // start pinging 188 | this.sendPing(); 189 | } 190 | 191 | // Helper method to merge an incoming membership list with 192 | // self membership list 193 | this.mergeList = (newlist) => { 194 | Object.keys(newlist).forEach(function(port) { 195 | port = parseInt(port) 196 | if (port < 1) throw Error(sprintf("Invalid port number: %d", port)) 197 | 198 | if (!(port in $this.list)) { 199 | // add only active entries 200 | if (newlist[port].status == NodeStateEnum.Active) { 201 | $this.addToList(port, newlist[port].heartbeat); 202 | } 203 | } else { 204 | $this.updateList(port, newlist[port].status, newlist[port].heartbeat); 205 | } 206 | }); 207 | } 208 | 209 | // Method to send a ping req, and ping_req req if failed 210 | this.sendPing = (_res = null, receiverPort = 0) => { 211 | this.updateMyHeartbeat(); 212 | 213 | // start of protocol period 214 | // randomly choose an entry other than self 215 | if (receiverPort === 0) receiverPort = getNextNodeToPing(); 216 | 217 | var errorCallback = (err) => { 218 | // console.log("PING to #%s failed", receiverPort); 219 | // TODO: send ping req to k random 220 | $this.updateMyHeartbeat(); 221 | // case when ping is sent because of ping_req 222 | if (_res !== null) { 223 | return _res.json({list: $this.list, ack: false}); 224 | } 225 | 226 | // Since ping has failed with in protocol period; send ping_req 227 | // to K random nodes to look for this target 228 | for (i = 0; i < $this.KMax; i++) { 229 | var _receiverPort = getNextNodeToPing(); 230 | if (_receiverPort === 0) return; 231 | var target = receiverPort; 232 | // ask _reciever port to ping target for you 233 | Kernel.send( 234 | _receiverPort, 235 | sprintf("m/PINGREQ?port=%s&target=%s", $this.port, target), 236 | Kernel.RequestTypes.POST, 237 | { list: $this.list }, 238 | function(resp, body) { 239 | try { 240 | $this.mergeList(JSON.parse(body)["list"]); 241 | receiverPort = 0; 242 | } catch (ex) { 243 | console.log(body); 244 | } 245 | } 246 | ); 247 | } 248 | } 249 | 250 | // console.log(sprintf("ping to #%s", receiverPort)) 251 | if (receiverPort > 0) { 252 | Kernel.send( 253 | receiverPort, 254 | "m/PING?port=" +this.port, 255 | Kernel.RequestTypes.POST, 256 | { list: $this.list }, 257 | function (resp, body) { 258 | // case when ping is sent because of ping_req 259 | if (_res != null) { 260 | return _res.json({list: $this.list, ack: true}); 261 | } 262 | 263 | try { 264 | $this.mergeList(JSON.parse(body)["list"]); 265 | receiverPort = 0; 266 | } catch (ex) { 267 | errorCallback(ex); 268 | } 269 | }, errorCallback); 270 | } 271 | 272 | setTimeout(function() { 273 | $this.sendPingEnd(receiverPort); 274 | }, this.protocolPeriod); 275 | } 276 | 277 | // Method to send join request to a known introducer 278 | this.sendJoinReq = (receiverPort) => { 279 | console.log(sprintf("#%s joinreq to #%s", this.port, receiverPort)) 280 | 281 | var errorCallback = function (err) { 282 | if ($this.joinReqRetryCount < $this.joinReqRetryCountThreshold) { 283 | $this.joinReqRetryCount++; 284 | console.log("JOINREQ FAIL, Retry Count: ", $this.joinReqRetryCount); 285 | setTimeout(function() { 286 | $this.sendJoinReq(receiverPort); 287 | }, $this.joinReqRetryCountTimeout * $this.joinReqRetryCount); 288 | } else { 289 | console.log("JOINREQ FAIL, Retry count exceeded limit, abort") 290 | process.exit() 291 | } 292 | } 293 | 294 | Kernel.send( 295 | receiverPort, 296 | "m/JOINREQ", 297 | Kernel.RequestTypes.GET, 298 | sprintf("port=%d&heartbeat=%s", this.port, Kernel.getTimestamp()), 299 | function(resp, body) { 300 | try { 301 | $this.mergeList(JSON.parse(body)["list"]); 302 | } catch (ex) { 303 | errorCallback(ex); 304 | } 305 | }, errorCallback); 306 | } 307 | 308 | // Method to mark end of one protocol period - garbage collection 309 | this.sendPingEnd = (receiverPort) => { 310 | // TODO: check if things went well, else remove the receiver port 311 | if (receiverPort != 0) { 312 | // mark receiverPort as suspicious now / or removed 313 | console.log("suspicious: %s", receiverPort) 314 | this.updateList(receiverPort, NodeStateEnum.Suspicious, null) 315 | setTimeout(function() { 316 | if (!(receiverPort in $this.list)) return; 317 | 318 | delete $this.list[receiverPort]; 319 | console.log("failed: %s", receiverPort) 320 | if ($this.churncb) $this.churncb(receiverPort); 321 | }, this.protocolPeriod * this.suspisionLimit); 322 | } 323 | this.sendPing(); 324 | }; 325 | 326 | // ----------- Constructor Code --------------------------------------- 327 | // Add self to membership list 328 | this.list[this.port] = createListEntry( 329 | Kernel.getTimestamp(), Kernel.getTimestamp()); 330 | 331 | this.init(); 332 | console.log(sprintf("Membership list initialized for #%d", this.port)); 333 | }; 334 | 335 | module.exports = Membership; -------------------------------------------------------------------------------- /process/server.js: -------------------------------------------------------------------------------- 1 | // File deals with server 2 | const express = require('express') 3 | const bodyParser = require('body-parser') 4 | const sprintf = require('sprintf').sprintf; 5 | const Topology = require('./topology.js') 6 | 7 | var Server = function(port, introducer) { 8 | // Private variables 9 | var $this = this; 10 | var topology = null; 11 | 12 | app = express(); 13 | app.use(bodyParser.json()); 14 | app.use(bodyParser.urlencoded({ 15 | extended: true 16 | })); 17 | 18 | function initializeListeners() { 19 | // rest method to get key 20 | app.get('/s/key', function(req, res) { 21 | if (!req.query.key) { 22 | res.status(400).send('Key missing in query'); 23 | } else { 24 | topology.get(req.query.key, function(value) { 25 | if (!value) { 26 | res.status(400).send('Key Not Found'); 27 | } else { 28 | res.json(value); 29 | } 30 | }); 31 | } 32 | }); 33 | 34 | // rest method to set key 35 | app.post('/s/key', function(req, res) { 36 | // validate input 37 | if (!req.body.key || !req.body.value) { 38 | res.status(400).send('Key or Value missing in body'); 39 | } else { 40 | topology.set(req.body.key, req.body.value, function(err) { 41 | if (err) { 42 | res.status(400).send(err.message); 43 | } else { 44 | res.status(200).send('OK'); 45 | } 46 | }) 47 | } 48 | }); 49 | 50 | // rest method to delete key 51 | app.delete('/s/key', function(req, res) { 52 | // validate input 53 | if (!req.query.key) { 54 | res.status(400).send('Key missing in query'); 55 | } else { 56 | topology.delete(req.query.key, function(err) { 57 | if (err) { 58 | res.status(400).send(err.message); 59 | } else { 60 | res.status(200).send('OK'); 61 | } 62 | }) 63 | } 64 | }); 65 | } 66 | 67 | // member function: bind the app to the port 68 | this.bind = function(callback) { 69 | app.listen(port, function () { 70 | console.log(sprintf("Listening to port: %d", port)) 71 | if (callback) callback(); 72 | }); 73 | }; 74 | 75 | // Gets the app object 76 | this.getApp = function() { 77 | return app; 78 | } 79 | 80 | // Initialize the process - bind to the port 81 | this.bind(function() { 82 | topology = new Topology(app, port, introducer); 83 | 84 | // initialize the rest methods 85 | initializeListeners(); 86 | }); 87 | } 88 | 89 | module.exports = Server; -------------------------------------------------------------------------------- /process/topology.js: -------------------------------------------------------------------------------- 1 | // file has class and methods to deal with ring topology in cassandra 2 | // like architecture 3 | 4 | const express = require('express') 5 | const sprintf = require('sprintf').sprintf; 6 | const Membership = require('./membership.js') 7 | const Datastore = require('./datastore.js') 8 | const Kernel = require('./kernel.js') 9 | 10 | // topology class 11 | var Topology = function(app, port, introducer = null) { 12 | // REGION: Public variables 13 | this.app = app; 14 | this.port = port; 15 | this.datastore = new Datastore() 16 | this.maxReplicas = 3; // Max no of replica of a data 17 | this.quorumCount = 2; // Votes needed in Quorum 18 | 19 | // REGION: private variables --------------------------------------------- 20 | var $this = this; 21 | var id = Kernel.hashPort(this.port); // self id 22 | var list = [id]; // list of virtual IDS 23 | var listPortMapping = {}; // mapping of id to port 24 | listPortMapping[id] = this.port; 25 | 26 | var stabalisationInProcess = false; // stabalisation state 27 | var stabalisationWaitTimeout = 1000; // timeout for stabalisation 28 | var stabalisationMethodTimeout = 5000; // timeout for stabalisation method, so that 29 | // it doesn't wait infinitely 30 | var stabalisationRetryLimit = 5; // retry limit for stabalisation wait; 31 | 32 | // REGION: Private methods --------------------------------------------- 33 | // sending stabalisation message to all; 34 | var sendStabalisationMessage = (port, data, callback, retryCount = 0) => { 35 | if (retryCount > stabalisationRetryLimit) { 36 | console.log("SendStabalisation retry exceeded, port", port); 37 | if (callback) callback(); 38 | return; 39 | } 40 | 41 | port = parseInt(port); 42 | if (port == $this.port) return callback(); 43 | 44 | Kernel.send( 45 | parseInt(port), 46 | "d/stabalisation", 47 | Kernel.RequestTypes.POST, 48 | { data: data }, 49 | function(response, body) { 50 | callback(); 51 | }, function(err) { 52 | sendStabalisationMessage(port, data, callback, retryCount + 1); 53 | } 54 | ); 55 | } 56 | 57 | // Common stabalisation method 58 | var stabalisation = () => { 59 | list.sort(function(a, b) { 60 | return (a > b) ? 1 : -1; 61 | }); 62 | 63 | var newIndex = list.indexOf(id); 64 | var stabalsiationMetadata = $this.datastore 65 | .getRemappedData(newIndex, list.length, $this.maxReplicas); 66 | 67 | if (Object.keys(stabalsiationMetadata).length == 0) { 68 | stabalisationInProcess = false; 69 | return; 70 | } 71 | 72 | var stabalised = 0; 73 | Object.keys(stabalsiationMetadata).forEach((_id) => { 74 | var port = listPortMapping[list[_id]]; 75 | sendStabalisationMessage(port, stabalsiationMetadata[_id], () => { 76 | ++stabalised; 77 | if (stabalised >= Object.keys(stabalsiationMetadata).length) { 78 | stabalisationInProcess = false; 79 | $this.datastore.removeStabalsiedKeys(newIndex, list.length, $this.maxReplicas); 80 | } 81 | }); 82 | }); 83 | } 84 | 85 | // stabalisation to perform when a new member joins 86 | var joinStabalisation = (joinPort, retryCount = 0) => { 87 | if (retryCount > stabalisationRetryLimit) { 88 | // not harmful as it sounds 89 | console.log("retry limit for stabalisation; port:", joinPort) 90 | return; 91 | } 92 | 93 | if (stabalisationInProcess) { 94 | setTimeout(function() { 95 | joinStabalisation(joinPort, retryCount + 1) 96 | }, stabalisationWaitTimeout); 97 | return; 98 | } 99 | 100 | stabalisationInProcess = true; 101 | console.log("[SIN] Stabalisation (+ve): ", joinPort) 102 | var joinPortId = Kernel.hashPort(joinPort); 103 | listPortMapping[joinPortId] = joinPort; 104 | 105 | if (list.indexOf(joinPortId) != -1) { 106 | console.log("[SOUT] STABALISATION_HALT, already in: ", joinPort, joinPortId); 107 | stabalisationInProcess = false; 108 | return; 109 | } 110 | 111 | // add to list 112 | list.push(joinPortId); 113 | stabalisation(); 114 | } 115 | 116 | // statbalisation to perform when an old member leaves 117 | var churnStabalisation = (chrunPort, retryCount = 0) => { 118 | if (retryCount > stabalisationRetryLimit) { 119 | // Not harmful as it sounds 120 | console.log("retry limit for stabalisation; port:", chrunPort) 121 | return; 122 | } 123 | 124 | if (stabalisationInProcess) { 125 | setTimeout(() => { 126 | churnStabalisation(chrunPort, retryCount + 1) 127 | }, stabalisationWaitTimeout); 128 | return; 129 | } 130 | 131 | stabalisationInProcess = true; 132 | console.log("[SIN] Stabalisation (-ve): ", chrunPort) 133 | var chrunPortId = Kernel.hashPort(chrunPort); 134 | 135 | if (list.indexOf(chrunPortId) == -1) { 136 | console.log( 137 | "[SOUT] STABALISATION_HALT, already not in: ", 138 | chrunPort, 139 | chrunPortId); 140 | stabalisationInProcess = false; 141 | return; 142 | } 143 | 144 | // remove this from list 145 | const index = list.indexOf(chrunPortId); 146 | list.splice(index, 1); 147 | stabalisation(); 148 | } 149 | 150 | // REGION: Constuctor code --------------------------------------------- 151 | this.membership = new Membership(app, port, joinStabalisation, churnStabalisation); 152 | if (introducer) { 153 | this.membership.sendJoinReq(introducer); 154 | } 155 | 156 | // Initialize the internal apis -------- 157 | // READ API 158 | this.app.get('/d/read', (req, res) => { 159 | var key = req.query.key; 160 | if (process.env.NODE_ENV !== Kernel.Constants.TestEnv) 161 | console.log(sprintf("dREAD: %s", key)); 162 | 163 | if (!key) { 164 | res.status(400).send('Key missing in query'); 165 | } else { 166 | res.json({value: $this.datastore.get(key)}); 167 | } 168 | }) 169 | 170 | // READ REPAIR API 171 | this.app.post('/d/readrepair', (req, res) => { 172 | var data = req.body.data; 173 | if (process.env.NODE_ENV != Kernel.Constants.TestEnv) 174 | console.log(sprintf("dREADREPAIR: %s", data.key)); 175 | 176 | if (!data || !data.key || !data.value) { 177 | return res.status(400).send('Key missing; bad request'); 178 | } 179 | 180 | data.value.timestamp = parseInt(data.value.timestamp); 181 | 182 | if (!$this.datastore.has(data.key)) { 183 | return res.status(400).send('key not found'); 184 | } else if ($this.datastore.get(data.key).timestamp < data.value.timestamp) { 185 | $this.datastore.set(data.key, data.value.value, data.value.timestamp); 186 | } 187 | res.json({ack: true}); 188 | }) 189 | 190 | // WRITE API 191 | this.app.post('/d/write', (req, res) => { 192 | var key = req.body.key; 193 | var value = req.body.value; 194 | var timestamp = req.body.timestamp; 195 | 196 | if (!key || !value || !timestamp) { 197 | return res.status(400).send("Key, Value or Timestamp missing"); 198 | } 199 | 200 | if (process.env.NODE_ENV != Kernel.Constants.TestEnv) 201 | console.log(sprintf("dWRITE: %s, val: %s", key, value)); 202 | 203 | try { 204 | $this.datastore.set(key, value, timestamp); 205 | } catch (ex) { 206 | console.log(ex); 207 | } 208 | res.json({ack: true}); 209 | }); 210 | 211 | // DELETE API 212 | this.app.delete('/d/delete', (req, res) => { 213 | var key = req.query.key; 214 | 215 | if (process.env.NODE_ENV != Kernel.Constants.TestEnv) 216 | console.log(sprintf("dDELETE: %s", key)); 217 | 218 | if (!key) { 219 | res.status(400).send('Key missing in query'); 220 | } else { 221 | try { 222 | $this.datastore.delete(key); 223 | res.json({ack: true}) 224 | } 225 | catch (ex) { 226 | console.log("Key delete error; " +ex.message); 227 | res.status(400).send('Key missing in query'); 228 | } 229 | } 230 | }); 231 | 232 | // STABALISATION API 233 | this.app.post('/d/stabalisation', (req, res) => { 234 | var data = req.body.data; 235 | 236 | if (process.env.NODE_ENV != Kernel.Constants.TestEnv) { 237 | console.log("dStabalisation: Count", data.length); 238 | } 239 | 240 | if (data && data.length) { 241 | data.forEach(function(d) { 242 | try { 243 | $this.datastore.set(d.key, d.value.value, d.value.timestamp); 244 | } catch (ex) { 245 | // expected; its ok 246 | } 247 | }); 248 | res.json({ack: true}); 249 | } else { 250 | console.log('undefined or empty payload'); 251 | res.json({ack: true, error: 'undefined or empty payload'}) 252 | } 253 | 254 | }); 255 | 256 | // ---------------------------------------------------------------- 257 | // REGION: public methods that shall use private variables 258 | // TODO: below three methods seems to have some code overlap 259 | // check what can be taken out of it; 260 | 261 | // Method to get the key 262 | this.get = (key, callback, retryCount = 0) => { 263 | if (!key || !callback) { 264 | throw Error("ArgumentException") 265 | } 266 | 267 | // TODO: if stabalisation going on wait for it to finish; 268 | // set a timeout and maybe fail with 5xx error if it 269 | // doesn't finish before that; 270 | var indexes = Kernel.hashKey(key, list.length, this.maxReplicas); 271 | var responses = []; 272 | 273 | var responseCallback = () => { 274 | if (responses.length != indexes.length) return; 275 | 276 | // look at +ve responses, count and get val; 277 | var val = null, positiveCount = 0; 278 | responses.forEach((response) => { 279 | if (response != null && response.value != null) { 280 | ++positiveCount; 281 | if (val) { 282 | if (response.value.timestamp > val.value.timestamp) { 283 | if (response.value.value != val.value.value) { 284 | // send a read-repair to by 285 | Kernel.send( 286 | val.by, 287 | "d/readrepair", 288 | Kernel.RequestTypes.POST, 289 | { data: {key: key, value: val.value} }, 290 | function(response, body) { 291 | // console.log(body); 292 | }, function(err) { 293 | console.log(err); 294 | } 295 | ); 296 | } 297 | val = response; 298 | } 299 | } else { 300 | val = response; 301 | } 302 | } 303 | }); 304 | 305 | if (indexes.length < $this.quorumCount) { 306 | if (positiveCount !== indexes.length) callback(null); 307 | else callback(val.value); 308 | } else if (positiveCount < $this.quorumCount) { 309 | callback(null); 310 | } else { 311 | callback(val.value); 312 | } 313 | } 314 | 315 | indexes.forEach((index) => { 316 | var port = listPortMapping[list[index]]; 317 | if (port == $this.port) { 318 | responses.push({ 319 | value: $this.datastore.get(key), 320 | by: port 321 | }); 322 | responseCallback(); 323 | } else { 324 | // send request to port 325 | Kernel.send( 326 | port, 327 | "d/read", 328 | Kernel.RequestTypes.GET, 329 | sprintf("key=%s", key), 330 | function(resp, body) { 331 | try { 332 | responses.push({ 333 | value: JSON.parse(body).value, 334 | by: port 335 | }); 336 | } catch (ex) { 337 | responses.push(null); 338 | } 339 | responseCallback(); 340 | }, function(err) { 341 | responses.push(null); 342 | responseCallback(); 343 | } 344 | ); 345 | } 346 | }); 347 | } 348 | 349 | // Method to set the key 350 | this.set = (key, value, callback, retryCount = 0) => { 351 | if (!key || !value || !callback) { 352 | throw Error("ArgumentException"); 353 | } 354 | 355 | // TODO: if stabalisation going on wait for it to finish; 356 | // set a timeout and maybe fail with 5xx error if it 357 | // doesn't finish before that; 358 | 359 | var indexes = Kernel.hashKey(key, list.length, this.maxReplicas); 360 | var responses = []; 361 | 362 | var responseCallback = function() { 363 | if (responses.length != indexes.length) return; 364 | var positiveCount = 0; 365 | responses.forEach(function(response) { 366 | if (response) positiveCount++; 367 | }); 368 | 369 | if (indexes.length < $this.quorumCount) { 370 | if (positiveCount != indexes.length) { 371 | callback(Error('Unable to write to quorum')); 372 | } else callback(null); 373 | } else if (positiveCount < $this.quorumCount) { 374 | callback(Error('Unable to write to quorum')); 375 | } else callback(null); 376 | } 377 | 378 | indexes.forEach(function(index) { 379 | var port = listPortMapping[list[index]]; 380 | if (port == $this.port) { 381 | $this.datastore.set(key, value); 382 | responses.push(true); 383 | responseCallback(); 384 | } else { 385 | // send request to port 386 | Kernel.send( 387 | port, 388 | "d/write", 389 | "POST", 390 | {key: key, value: value, timestamp: Kernel.getTimestamp()}, 391 | function(resp, body) { 392 | responses.push(true); 393 | responseCallback(); 394 | }, function(err) { 395 | responses.push(false); 396 | responseCallback(); 397 | } 398 | ); 399 | } 400 | }); 401 | } 402 | 403 | // Method to delete a key 404 | this.delete = (key, callback, retryCount = 0) => { 405 | if (!key || !callback) { 406 | throw Error("ArgumentException") 407 | } 408 | 409 | // TODO: if stabalisation going on wait for it to finish; 410 | // set a timeout and maybe fail with 5xx error if it 411 | // doesn't finish before that; 412 | 413 | var indexes = Kernel.hashKey(key, list.length, this.maxReplicas); 414 | var responses = []; 415 | 416 | var responseCallback = () => { 417 | if (responses.length != indexes.length) return; 418 | var positiveCount = 0; 419 | responses.forEach((response) => { 420 | if (response) positiveCount++; 421 | }); 422 | 423 | if (indexes.length < $this.quorumCount) { 424 | if (positiveCount != indexes.length) { 425 | callback(Error('Unable to delete from quorum')); 426 | } else callback(null); 427 | } else if (positiveCount < $this.quorumCount) { 428 | callback(Error('Unable to delete from quorum')); 429 | } else callback(null); 430 | } 431 | 432 | indexes.forEach((index) => { 433 | var port = listPortMapping[list[index]]; 434 | if (port == $this.port) { 435 | try { 436 | $this.datastore.delete(key); 437 | responses.push(true); 438 | } catch (ex) { 439 | console.log("EX while self delete; ", ex.message) 440 | responses.push(false); 441 | } 442 | responseCallback(); 443 | } else { 444 | // send request to port 445 | Kernel.send( 446 | port, 447 | "d/delete", 448 | Kernel.RequestTypes.DELETE, 449 | "key=" +key, 450 | function(resp, body) { 451 | responses.push(true); 452 | responseCallback(); 453 | }, function(err) { 454 | responses.push(false); 455 | responseCallback(); 456 | } 457 | ); 458 | } 459 | }); 460 | } 461 | } 462 | 463 | module.exports = Topology; -------------------------------------------------------------------------------- /screenshot/membership.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mebjas/dht-node/de70b694ea4f958112f315382b3f7fb205367a8c/screenshot/membership.PNG -------------------------------------------------------------------------------- /screenshot/membership_fd.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mebjas/dht-node/de70b694ea4f958112f315382b3f7fb205367a8c/screenshot/membership_fd.PNG -------------------------------------------------------------------------------- /screenshot/membership_fd_24.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mebjas/dht-node/de70b694ea4f958112f315382b3f7fb205367a8c/screenshot/membership_fd_24.PNG -------------------------------------------------------------------------------- /test/test_datastore.js: -------------------------------------------------------------------------------- 1 | //During the test the env variable is set to test 2 | process.env.NODE_ENV = 'test'; 3 | 4 | const assert = require('assert'); 5 | const chai = require('chai'); 6 | const express = require('express') 7 | const request = require('request') 8 | const bodyParser = require('body-parser') 9 | const sprintf = require("sprintf").sprintf; 10 | const Datastore = require("../process/datastore.js") 11 | 12 | var datastore = new Datastore() 13 | 14 | describe('Datastore', function() { 15 | describe('CRUD', function() { 16 | it ('Should set value', function() { 17 | datastore.set('key', 'value') 18 | assert.equal(datastore.get('key').value, 'value'); 19 | }); 20 | 21 | it ('Should update when set again value', function() { 22 | datastore.set('key', 'value') 23 | datastore.set('key', 'value2') 24 | assert.equal(datastore.get('key').value, 'value2'); 25 | }); 26 | 27 | it ('Should delete when deleted', function() { 28 | datastore.set('key', 'value') 29 | assert.equal(datastore.get('key').value, 'value'); 30 | datastore.delete('key') 31 | assert.equal(datastore.get('key'), null); 32 | }); 33 | 34 | it ('Should throw when deleting key no available', function() { 35 | try { 36 | datastore.delete('key') 37 | assert.ok(false); 38 | } catch (ex) { 39 | assert.ok(true); 40 | } 41 | }); 42 | }); 43 | }); -------------------------------------------------------------------------------- /test/test_e2e.js: -------------------------------------------------------------------------------- 1 | // Script to test e2e capabilities of the system 2 | //During the test the env variable is set to test 3 | process.env.NODE_ENV = 'test'; 4 | 5 | const {spawn} = require('child_process'); 6 | const assert = require('assert'); 7 | const express = require('express'); 8 | const chai = require('chai'); 9 | const chaiHttp = require('chai-http'); 10 | const request = require('request'); 11 | const sprintf = require('sprintf').sprintf; 12 | const kernel = require('../process/kernel.js'); 13 | chai.use(chaiHttp); 14 | 15 | function spawnANode(port, introducer) { 16 | console.log("Spawning", port) 17 | var proc = spawn('node', ['../index.js', port, (introducer) ? introducer : ""]); 18 | 19 | proc.stdout.on('data', function(data) { 20 | console.log(">>", port, data.toString()); 21 | }); 22 | return proc 23 | } 24 | 25 | ports = [8080, 8081, 8082, 8083, 8084] 26 | portProcesses = [] 27 | 28 | // Spawn all nodes 29 | ports.forEach(function(port, index) { 30 | if (!index) portProcesses[index] = spawnANode(port); 31 | else { 32 | setTimeout(function() { portProcesses[index] = spawnANode(port, ports[0]); }, 1000); 33 | } 34 | }, this); 35 | 36 | // -------------------- test --------------------------- 37 | describe('End to End', function() { 38 | describe('Should fail for keys that don\'t exist', function() { 39 | it ('Fail to get a key', function() { 40 | ports.forEach(function(port) { 41 | kernel.send( 42 | port, 43 | 's/key', 44 | 'GET', 45 | 'key=key1', 46 | function(resp, body) { 47 | // ping accepted 48 | assert.ok(true); 49 | 50 | assert.equal(resp.statusCode, 400); 51 | }, function( err) { 52 | assert.ok(false); 53 | } 54 | ) 55 | }) 56 | }); 57 | 58 | it ('SET /s/key', function() { 59 | ports.forEach(function(port, index) { 60 | kernel.send( 61 | port, 62 | 's/key', 63 | 'POST', 64 | {key: 'key' +index, value: 'value' +index}, 65 | function(resp, body) { 66 | // ping accepted 67 | assert.ok(true); 68 | assert.equal(resp.statusCode, 200); 69 | }, function( err) { 70 | assert.ok(false); 71 | } 72 | ) 73 | }) 74 | }); 75 | 76 | describe ('GET /s/key', function() { 77 | ports.forEach(function(port, index) { 78 | it ('In same order, port:' +port, function() { 79 | kernel.send( 80 | port, 81 | 's/key', 82 | 'GET', 83 | 'key=key' +index, 84 | function(resp, body) { 85 | // ping accepted 86 | assert.ok(true); 87 | assert.equal(resp.statusCode, 200); 88 | try { 89 | body = JSON.parse(body) 90 | assert.equal(body.value, 'value' +index) 91 | } catch (ex) { 92 | assert.ok(false); 93 | } 94 | }, function( err) { 95 | assert.ok(false); 96 | } 97 | ) 98 | }); 99 | }); 100 | 101 | ports.reverse().forEach(function(port, index) { 102 | it ('In reverse order, port: ' +port, function() { 103 | kernel.send( 104 | port, 105 | 's/key', 106 | 'GET', 107 | 'key=key' +index, 108 | function(resp, body) { 109 | // ping accepted 110 | assert.ok(true); 111 | assert.equal(resp.statusCode, 200); 112 | assert.equal(body.value, 'value' +index); 113 | }, function( err) { 114 | assert.ok(false); 115 | } 116 | ) 117 | }); 118 | }); 119 | }); 120 | }); 121 | }); 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | -------------------------------------------------------------------------------- /test/test_kernel.js: -------------------------------------------------------------------------------- 1 | //During the test the env variable is set to test 2 | process.env.NODE_ENV = 'test'; 3 | 4 | const assert = require('assert'); 5 | const Kernel = require('../process/kernel.js') 6 | 7 | describe('Kernel', function() { 8 | describe('#getTimestamp()', function() { 9 | var t1 = (new Date()).getTime(); 10 | it('should return current timestamp', function() { 11 | var t2 = Kernel.getTimestamp(); 12 | var t3 = (new Date()).getTime(); 13 | 14 | assert.ok(t2 >= t1); 15 | assert.ok(t3 >= t2); 16 | }); 17 | }); 18 | 19 | describe('#random()', function() { 20 | it('Should be with in range', function() { 21 | assert.ok(Kernel.random(0, 10) >= 0) 22 | assert.ok(Kernel.random(0, 10) <= 10) 23 | }) 24 | 25 | // TODO: check for randomness? 26 | }); 27 | 28 | describe('#shuffle()', function() { 29 | it('should shuffle the array', function() { 30 | arr = [1,2,3,4,5] 31 | arr1 = Kernel.shuffle(arr) 32 | // TODO: looks like a stupid test, does it even 33 | // compare the strucure of array? 34 | assert.ok(arr != arr1) 35 | }); 36 | }); 37 | 38 | describe('#hashPort()', function() { 39 | it('should have different hash for 8080 - 8100 port range', function() { 40 | var collisionDict = {}; 41 | for (j = 8080; j <= 8100; j++) { 42 | var hash = Kernel.hashPort(j); 43 | assert.ok(!(hash in collisionDict)) 44 | 45 | if (!(hash in collisionDict)) collisionDict[hash] = 0 46 | collisionDict[hash] += 1 47 | } 48 | 49 | assert.equal(0, Kernel.hashPort(8080)) 50 | assert.ok(Kernel.hashPort(8100) >= 0) 51 | assert.ok(Kernel.hashPort(8100) < 256) 52 | }); 53 | }); 54 | 55 | describe('#hash()', function() { 56 | it('should give same values each time', function() { 57 | assert.ok(Kernel.hash("something") > 0) 58 | assert.equal(Kernel.hash("something"), Kernel.hash("something")) 59 | }); 60 | 61 | it('should give correct value', function() { 62 | assert.equal(191, Kernel.hash("something")) 63 | }); 64 | }); 65 | 66 | describe('#hashKey()', function() { 67 | it('should give correct value in range', function() { 68 | var val = Kernel.hashKey("something", 10, 5); 69 | assert.equal(val.length, 5); 70 | }); 71 | 72 | it('should give correct value when max < max replica', function() { 73 | var val = Kernel.hashKey("something", 2, 5); 74 | assert.equal(val.length, 2); 75 | }); 76 | 77 | it('should throw exception for wrong input', function() { 78 | try { 79 | Kernel.hashKey("something", 0, 5); 80 | } catch (ex) { 81 | assert.ok(true); 82 | } 83 | }); 84 | 85 | it('should give same value each time', function() { 86 | var val = Kernel.hashKey("something", 10, 5); 87 | }); 88 | }); 89 | }); -------------------------------------------------------------------------------- /test/test_membership.js: -------------------------------------------------------------------------------- 1 | //During the test the env variable is set to test 2 | process.env.NODE_ENV = 'test'; 3 | 4 | const assert = require('assert'); 5 | const chai = require('chai'); 6 | const express = require('express') 7 | const request = require('request') 8 | const bodyParser = require('body-parser') 9 | const sprintf = require("sprintf").sprintf; 10 | const Membership = require("../process/membership.js") 11 | const Kernel = require('../process/kernel.js') 12 | 13 | var app = express() 14 | app.use(bodyParser.json()); 15 | app.use(bodyParser.urlencoded({extended: true})); 16 | 17 | var testPort = 8090; 18 | app.listen(testPort); 19 | 20 | var membership = new Membership(app, testPort); 21 | 22 | describe('Membership', function() { 23 | describe('PortNo', function() { 24 | it ('Should have port no same as assigned one', function() { 25 | assert.equal(testPort, membership.port); 26 | }); 27 | }); 28 | 29 | describe('MembershipList', function() { 30 | it('Should have atleast one item as of now', function() { 31 | assert.ok(1 == Object.keys(membership.list).length); 32 | }) 33 | 34 | it('Should have self as member', function() { 35 | assert.ok(testPort in membership.list) 36 | }) 37 | 38 | it('Should have self as active ', function() { 39 | assert.equal("active", membership.list[testPort].status) 40 | }); 41 | }); 42 | 43 | describe('#updateMyHeartbeat()', function() { 44 | var t1 = Kernel.getTimestamp(); 45 | 46 | it ('Should have heartbeat >= t1', function() { 47 | membership.updateMyHeartbeat() 48 | assert.ok(membership.list[testPort].heartbeat >= t1); 49 | }); 50 | }); 51 | 52 | describe('/m/JOINREQ', function() { 53 | it ('should add 8081 with joinreq', function() { 54 | request.get( 55 | sprintf('http://localhost:%d/m/JOINREQ?port=8081',testPort), 56 | function(err, response, body) { 57 | assert.ok(2 == Object.keys(membership.list).length); 58 | assert.ok(8081 in membership.list); 59 | assert.ok(!(8082 in membership.list)); 60 | }); 61 | }); 62 | }); 63 | 64 | describe('/m/PING', function() { 65 | it ('should add 8082 & 8083 based on ping', function() { 66 | request.post( 67 | sprintf('http://localhost:%d/m/PING?port=8081',testPort), 68 | {form: {list: { 69 | 8082: { 70 | heartbeat: Kernel.getTimestamp(), 71 | timestamp: Kernel.getTimestamp(), 72 | status: "active" 73 | }, 74 | 8083: { 75 | heartbeat: Kernel.getTimestamp(), 76 | timestamp: Kernel.getTimestamp(), 77 | status: "active" 78 | } 79 | }}}, function(err, response, body) { 80 | assert.ok(4 == Object.keys(membership.list).length); 81 | assert.ok(8082 in membership.list); 82 | assert.ok(8083 in membership.list); 83 | }); 84 | }); 85 | }); 86 | 87 | // TODO: test sendPing, sendJoinReq etc 88 | }); 89 | -------------------------------------------------------------------------------- /test/test_server.js: -------------------------------------------------------------------------------- 1 | //During the test the env variable is set to test 2 | process.env.NODE_ENV = 'test'; 3 | 4 | const assert = require('assert'); 5 | const chai = require('chai'); 6 | const chaiHttp = require('chai-http'); 7 | const express = require('express') 8 | const request = require('request') 9 | const bodyParser = require('body-parser') 10 | const sprintf = require("sprintf").sprintf; 11 | const Kernel = require("../process/kernel.js") 12 | const Server = require('../process/server.js') 13 | 14 | chai.use(chaiHttp) 15 | var server = new Server(8080, null) 16 | 17 | describe('Server', function() { 18 | describe('Should be able to get keys that exist', function() { 19 | it ('Fail to get a key - TBD', function() { 20 | chai.request(server.getApp()) 21 | .get('/d/read?key=key1') 22 | .end((err, res) => { 23 | res.should.have.status(200); 24 | res.body.should.be.a('array'); 25 | res.body.length.should.be.eql(0); 26 | done(); 27 | }); 28 | }); 29 | }); 30 | }); -------------------------------------------------------------------------------- /test/test_topology.js: -------------------------------------------------------------------------------- 1 | //During the test the env variable is set to test 2 | process.env.NODE_ENV = 'test'; 3 | 4 | const assert = require('assert'); 5 | const chai = require('chai'); 6 | const express = require('express') 7 | const request = require('request') 8 | const bodyParser = require('body-parser') 9 | const sprintf = require("sprintf").sprintf; 10 | const Kernel = require("../process/kernel.js") 11 | const Topology = require('../process/topology.js') 12 | 13 | var app = express() 14 | app.use(bodyParser.json()); 15 | app.use(bodyParser.urlencoded({extended: true})); 16 | 17 | var testPort = 8100; 18 | app.listen(testPort); 19 | 20 | var topology = new Topology(app, testPort) 21 | 22 | describe('Topology', function() { 23 | describe('Placeholder', function() { 24 | it ('Placeholder', function() { 25 | assert.equal(1,1); 26 | }); 27 | }); 28 | }); --------------------------------------------------------------------------------