├── .gitignore ├── .jshintrc ├── LICENSE ├── README.md ├── actor-config.json ├── architecture.png ├── architecture.pptx ├── config.json ├── db ├── data-load.js ├── data │ ├── account-balance.json │ └── accounts.json ├── db-clean.js ├── models │ ├── account-balance.js │ ├── account.js │ ├── actor-map.js │ ├── journal.js │ └── rogue-message.js └── services.js ├── images ├── ActorModel.png ├── chaos_monkey.jpg ├── concurrent_vs_parallel.png ├── containment.jpg ├── failure_recovery.png ├── fig-actor-hierarchy.png └── supervision.jpg ├── lib ├── actor-service.js ├── actor.js ├── forward-message-handler.js ├── generator-mapper.js ├── generators │ ├── account.js │ ├── accountBalance.js │ └── system-root.js ├── receiver.js └── system-message-handler.js ├── logs ├── failure.log └── system.log ├── package.json ├── routes ├── acto-dispatcher.js └── acto-node.js ├── test ├── actor.core.spec.js ├── integration.test.js └── test_data │ ├── test-data-100.json │ └── test-data.json └── utilities ├── actor-mapper.js ├── context.js ├── fault-line.js ├── journal.js ├── logger.js ├── router.js └── supervision ├── supervision-directive.js └── supervision-strategy.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dummy.js 3 | settings.json 4 | .vscode -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "esnext" :true, 3 | "globalstrict":true 4 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Pragyan Das 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Actor-Pattern implemented in Javascript using generators (co-routines) 2 | 3 | While a subroutine is executed sequentially and straightly, a coroutine can suspend and resume its execution at distinct points in code. Thus, coroutines are good primitives for cooperative task handling, as coroutines are able to yield execution. The advantages of cooperative task handling especially in case of massive parallelism of asynchronous operations such as I/O are in plenty. 4 | 5 | Coroutines are often used as low-level primitives, namely as an alternative to threads for implementing high-level concurrency concepts. For example, actor-based systems may use coroutines for their underlying implementation. This implementation attempts on the line of the above mentioned concepts. 6 | 7 | ## Parallelism vs Concurrency 8 | 9 | ![Parallelism vs Concurrency](images/concurrent_vs_parallel.png) 10 | 11 | The separation of the application into threads defines its concurrent model. The mapping of these threads on the available cores defines its level or parallelism. A concurrent system may run efficiently on a single processor, in which case it is not parallel. 12 | 13 | ## Generators - A brief Introduction 14 | 15 | Most programming languages have the concept of subroutines in the form of procedures, functions, or methods. When called, a typical subroutine completes at once and returns a single value. It does not hold any state between invocations. 16 | 17 | From the client code, a generator looks like a normal subroutine – it can be invoked and will return a value. However, a generator yields (rather than return!) a value and preserves its state – i.e. the values of the local variables. Again, this is known as continuation. When this generator is called again, its state is restored and the execution continues from the point of the last yielding until a new yield is encountered. 18 | 19 | ## Actors 20 | 21 | The actors are the instance of any particular generator. 22 | 23 | The included example is based on the scenario of fund transfer between various bank accounts involving the following models: 24 | 25 | * system-root 26 | * account 27 | * account-balance 28 | 29 | ## Actor Hierarchy 30 | 31 | ![Actor Hierarchy](images/fig-actor-hierarchy.png) 32 | 33 | The guardian is the root actor of the entire system. Every other actor that is created is always a child of some actor. 34 | 35 | ## Message Passing 36 | 37 | ![Message Passing](images/ActorModel.png) 38 | 39 | Every actor is associated with an address and to send message to any particular actor, an actor have to know the address of that actor. 40 | 41 | ## Fault Tolerance 42 | 43 | When things go wrong, that’s when! Whenever a child actor has an unhandled exception and is crashing, it reaches out to its parent for help and to tell it what to do. 44 | 45 | Specifically, the child will send its parent a message that is of the **Failure** class. Then it’s up to the parent to decide what to do. 46 | 47 | ### How Can the Parent Resolve the Error? 48 | 49 | There are two factors that determine how a failure is resolved: 50 | 51 | How the child failed (what type of **Exception** did the child include in its Failure message to its parent.) 52 | What **Directive** the parent actor executes in response to a child Failure. This is determined by the parent’s **SupervisionStrategy**. 53 | 54 | ![supervision](images/supervision.jpg) 55 | 56 | ### Supervision Directives 57 | 58 | When it receives an error from its child, a parent can take one of the following actions (“directives”). The supervision strategy maps different exception types to these directives, allowing you to handle different types of errors as appropriate. 59 | 60 | Types of supervision directives (i.e. what decisions a supervisor can make): 61 | 62 | * Restart the child (default): this is the common case, and the default. 63 | * Stop the child: this permanently terminates the child actor. 64 | * Escalate the error (and stop itself): this is the parent saying “I don’t know what to do! I’m gonna stop everything and ask MY parent!” 65 | * Resume processing (ignores the error): you generally won’t use this. Ignore it for now. 66 | 67 | ***The critical thing to know here is that whatever action is taken on a parent propagates to its children. If a parent is halted, all its children halt. If it is restarted, all its children restart.*** 68 | 69 | ### Supervision Strategies 70 | 71 | There are two built-in supervision strategies: 72 | 73 | * One-For-One Strategy (default) 74 | * All-For-One Strategy 75 | 76 | The basic difference between these is how widespread the effects of the error-resolution directive will be. 77 | 78 | One-For-One says that that the directive issued by the parent only applies to the failing child actor. It has no effect on the siblings of the failing child. This is the default strategy if you don’t specify one. (You can also define your own custom supervision strategy.) 79 | 80 | All-For-One says that that the directive issued by the parent applies to the failing child actor AND all of its siblings. 81 | 82 | ***The other important choice you make in a supervision strategy is how many times a child can fail within a given period of time before it is shut down (e.g. “no more than 10 errors within 60 seconds, or you’re shut down”).*** 83 | 84 | ### Containment of Error 85 | The whole point of supervision strategies and directives is to contain failure within the system and self-heal, so the whole system doesn’t crash. How do we do this? 86 | 87 | We push potentially-dangerous operations from a parent to a child, whose only job is to carry out the dangerous task. 88 | 89 | ![containment](images/containment.jpg) 90 | 91 | Suppose a particular network call is dangerous! If the request raises an error, it will crash the actor that started the call. So ***how do we protect ourselves?*** Simple, push the call to the child and it will take care of the network call isolating the error to itself and not kill the parent which might be holding a lot of other important data. 92 | 93 | We always push the dangerous parts to the child actor. That way, if the child crashes, it doesn’t affect the parent, which is holding on to all the important data. By doing this, we are ***localizing the failure and avoiding burning of the house***. 94 | 95 | ### What if Supervisor could not respond in time? 96 | 97 | What if there are a bunch of messages already in the supervisor’s mailbox waiting to be processed when a child reports an error? Won’t the crashing child actor have to wait until those are processed until it gets a response? 98 | 99 | Actually, no. When an actor reports an error to its supervisor, it is sent as a special type of ***“system message.”*** System messages jump to the front of the ***supervisor’s mailbox*** and are processed before the supervisor returns to its normal processing. 100 | 101 | ***System messages jump to the front of the supervisor’s mailbox*** and are processed before the supervisor returns to its normal processing. 102 | 103 | Parents come with a default SuperviserStrategy object (or you can provide a custom one) that makes decisions on how to handle failures with their child actors. 104 | 105 | ### How can we handle current message when an Actor Fails? 106 | 107 | The current message in case of failure (regardless of whether the failure happened to it or its parent) can be preserved for re-processing after restarting. The most common approach is using a ***"pre-restart"***, the actor can just send the message back to itself. This way, the message will be moved to the persistent mailbox rather than staying in memory. 108 | 109 | ## Context Object 110 | 111 | This object maintains the metadata of an object. It contains following actor information/api: 112 | 113 | * parentSupervisor - the parent actor 114 | * supervisedChildren - the list of supervisedChildren 115 | * actor-system-name - the actor system the actor belongs to 116 | * life-cycle monitoring - listens to system events 117 | * createChild - method to create a child actor 118 | 119 | Context object gets created for each actor during during initialization and can be passed to the other actors if needed. Whenever an actor is restarted the child reads this object to reset its internal state. 120 | 121 | ## Journal 122 | 123 | Whenever any message reach the dispatcher of the system it is channelized into a stream using RxJs subjects and are seamlessly written to the database without affecting the performance of the message passing in the entire actor system. 124 | 125 | Journal records the message with the following attributes: 126 | 127 | * message 128 | * actorId 129 | * timestamp 130 | 131 | Journal is used to recover and reset the internal state of an actor in case of crash or failure. 132 | 133 | ## Crash Recovery 134 | 135 | Crash Recovery kicks in when one of the ***node in the cluster*** crash. It results in the death of a lot of actors. 136 | 137 | The recovery mechanism: 138 | 139 | * Every actor subscribes to cluster event. 140 | * Actors are notified when node in the cluster crashes. 141 | * Parent actor checks if childen are alive. 142 | * It finds out the death of children if any. 143 | * It looks into the SupervisorStartegy and gets a directive. 144 | * It creates the child/children with previous ID as held by the dead child actor 145 | * The child actor resets its internal state to the state held just before the failure after consulting the journal using the ID. 146 | 147 | ![failure_recovery](images/failure_recovery.png) 148 | 149 | These steps are also used when ***down-scaling of cluster*** is required 150 | 151 | ## Actor Scavenger 152 | 153 | Scavenge job to ***remove dormant actors*** from the ***actor mapping*** periodically so that the instance can be de-refernced thus can be garbage collected. 154 | -------------------------------------------------------------------------------- /actor-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "account": { 3 | "id": ["id"] 4 | }, 5 | "accountBalance": { 6 | "id": ["accBal_", "id"] 7 | } 8 | } -------------------------------------------------------------------------------- /architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pragyandas/jsactor/876d50d604a091e8a18f10b2db7d1f4dddf40722/architecture.png -------------------------------------------------------------------------------- /architecture.pptx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pragyandas/jsactor/876d50d604a091e8a18f10b2db7d1f4dddf40722/architecture.pptx -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "initialNodes": ["http://127.0.0.1:3001", "http://127.0.0.1:3002","http://127.0.0.1:3003"], 3 | "dispatcherURL": "http://127.0.0.1", 4 | "dispatcherPort": 3000, 5 | "rootNodeServer" : "http://localhost:3001" 6 | 7 | } -------------------------------------------------------------------------------- /db/data-load.js: -------------------------------------------------------------------------------- 1 | var fs = require("fs"); 2 | var async = require("async"); 3 | var account = require('./models/account.js'); 4 | var accountBalance = require('./models/account-balance.js'); 5 | var mongoose = require("mongoose"); 6 | var dbClean = require('./db-clean'); 7 | 8 | var accountData = JSON.parse(fs.readFileSync('./data/accounts.json', 'utf8')); 9 | var accountBalanceData = JSON.parse(fs.readFileSync('./data/account-balance.json', 'utf8')); 10 | 11 | mongoose.connect('mongodb://127.0.0.1:27017/storedb', function (err) { 12 | if (err) { 13 | throw err; 14 | } 15 | 16 | dbClean(function () { 17 | async.each(accountData, function (item, cb) { 18 | account.create(item, cb); 19 | }, function (err) { 20 | if (err) {} 21 | console.log("account data loaded successfully"); 22 | async.each(accountBalanceData, function (item, cb) { 23 | accountBalance.create(item, cb); 24 | }, function (err) { 25 | if (err) {} 26 | console.log("account balance data loaded successfully"); 27 | mongoose.disconnect(); 28 | }); 29 | }); 30 | 31 | }); 32 | }); -------------------------------------------------------------------------------- /db/db-clean.js: -------------------------------------------------------------------------------- 1 | var mongoose = require("mongoose"); 2 | 3 | module.exports = function cleanDb(cb) { 4 | 5 | mongoose.connection.db.listCollections({ 6 | name: 'accounts' 7 | }) 8 | .next(function (err, account) { 9 | if (account) { 10 | mongoose.connection.db.dropCollection('accounts', function (err, result) { 11 | if (err) throw err; 12 | console.log("account dropped successfully"); 13 | cb(); 14 | }); 15 | } else { 16 | cb(); 17 | } 18 | }); 19 | mongoose.connection.db.listCollections({ 20 | name: 'accountbalances' 21 | }) 22 | .next(function (err, accountBalance) { 23 | if (accountBalance) { 24 | mongoose.connection.db.dropCollection('accountbalances', function (err, result) { 25 | if (err) throw err; 26 | console.log("account balance dropped successfully"); 27 | cb(); 28 | }); 29 | } else { 30 | cb(); 31 | } 32 | }); 33 | } -------------------------------------------------------------------------------- /db/models/account-balance.js: -------------------------------------------------------------------------------- 1 | var mongoose = require("mongoose"); 2 | 3 | var Schema = mongoose.Schema; 4 | 5 | // create a schema 6 | var accountBalanceSchema = new Schema({ 7 | "accountNo": String, 8 | "amount": Number 9 | }); 10 | 11 | // the schema is useless so far 12 | // we need to create a model using it 13 | var accountBalance = mongoose.model('accountBalance', accountBalanceSchema); 14 | 15 | module.exports = accountBalance; -------------------------------------------------------------------------------- /db/models/account.js: -------------------------------------------------------------------------------- 1 | var mongoose = require("mongoose"); 2 | 3 | var Schema = mongoose.Schema; 4 | 5 | // create a schema 6 | var accountSchema = new Schema({ 7 | accountNo: Number, 8 | name: String 9 | }); 10 | 11 | // the schema is useless so far 12 | // we need to create a model using it 13 | var account = mongoose.model('account', accountSchema); 14 | 15 | module.exports = account; -------------------------------------------------------------------------------- /db/models/actor-map.js: -------------------------------------------------------------------------------- 1 | var mongoose = require("mongoose"); 2 | 3 | var Schema = mongoose.Schema; 4 | 5 | // create a schema 6 | var actorMapSchema = new Schema({ 7 | id: String, 8 | parentId: String, 9 | node: Object 10 | }); 11 | 12 | // the schema is useless so far 13 | // we need to create a model using it 14 | var actorMap = mongoose.model('actorMap', actorMapSchema); 15 | 16 | module.exports = actorMap; -------------------------------------------------------------------------------- /db/models/journal.js: -------------------------------------------------------------------------------- 1 | var mongoose = require("mongoose"); 2 | 3 | var Schema = mongoose.Schema; 4 | 5 | // create a schema 6 | var journalSchema = new Schema({ 7 | msg: Object, 8 | actorId: String, 9 | timestamp: Number 10 | }); 11 | 12 | // the schema is useless so far 13 | // we need to create a model using it 14 | var journal = mongoose.model('journal', journalSchema); 15 | 16 | module.exports = journal; -------------------------------------------------------------------------------- /db/models/rogue-message.js: -------------------------------------------------------------------------------- 1 | var mongoose = require("mongoose"); 2 | 3 | var Schema = mongoose.Schema; 4 | 5 | // create a schema 6 | var rogueMessageSchema = new Schema({ 7 | messages: { 8 | type: Array, 9 | "default": [] 10 | }, 11 | actorId: String 12 | }); 13 | 14 | // the schema is useless so far 15 | // we need to create a model using it 16 | var rogueMessage = mongoose.model('rogueMessage', rogueMessageSchema); 17 | 18 | module.exports = rogueMessage; -------------------------------------------------------------------------------- /db/services.js: -------------------------------------------------------------------------------- 1 | var mongoose = require("mongoose"); 2 | var acc_bal = require('./models/account-balance.js'); 3 | var journal = require('./models/journal.js'); 4 | var actorMap = require('./models/actor-map.js'); 5 | var rogueMessage = require('./models/rogue-message.js'); 6 | mongoose.connect('mongodb://localhost/storedb'); 7 | 8 | 9 | function incrementBalance(accountNo, amount) { 10 | return new Promise(function (resolve, reject) { 11 | acc_bal.findOneAndUpdate({ 12 | "accountNo": accountNo 13 | }, { 14 | $inc: { 15 | "amount": amount 16 | } 17 | }, {}, function (err, result) { 18 | if (err) { 19 | console.log('IncrementBalance failed ~ ' + accountNo + ' ~ amount ~ ' + amount + ' ~ err ~ ' + JSON.stringify(err)); 20 | reject(err); 21 | } 22 | resolve(true); 23 | }); 24 | }); 25 | } 26 | 27 | function decrementBalance(accountNo, amount) { 28 | return new Promise(function (resolve, reject) { 29 | acc_bal.findOneAndUpdate({ 30 | "accountNo": accountNo 31 | }, { 32 | $inc: { 33 | "amount": -amount 34 | } 35 | }, {}, function (err, result) { 36 | if (err) { 37 | console.log('decrementBalance failed ~ ' + accountNo + ' ~ amount ~ ' + amount + ' ~ err ~ ' + JSON.stringify(err)); 38 | reject(err); 39 | } 40 | resolve(true); 41 | }); 42 | }); 43 | } 44 | 45 | function saveJournal(msg) { 46 | return new Promise(function (resolve, reject) { 47 | var j = {}; 48 | j.msg = msg.msg; 49 | j.timestamp = msg.timestamp; 50 | j.actorId = msg.actorId; 51 | journal.create(j, function (err, res) { 52 | if (err) { 53 | console.log('Journal err ~ ' + JSON.stringify(err)) 54 | reject(err); 55 | } 56 | resolve(true); 57 | }); 58 | }); 59 | } 60 | 61 | 62 | function retrieveJournal(startTime, endTime, actorId) { 63 | return new Promise(function (resolve, reject) { 64 | var result = journal.find({ 65 | "actorId": actorId, 66 | "timestamp": { 67 | $gte: startTime, 68 | $lte: endTime 69 | }, 70 | "msg.action":{ 71 | $ne: "system" 72 | }, 73 | "msg.subaction":{ 74 | $ne: "recover" 75 | } 76 | },function (err, res) { 77 | console.log(actorId + ' recovering between ' + startTime + ' - ' + endTime) 78 | if (err) { 79 | console.log('retreiveJournal err ~ ' + JSON.stringify(err)); 80 | reject(err); 81 | } 82 | console.log(JSON.stringify(res)); 83 | resolve(res); 84 | }); 85 | }); 86 | } 87 | 88 | 89 | 90 | function saveActorMap(data) { 91 | return new Promise(function (resolve, reject) { 92 | var aMap = {}; 93 | aMap.id = data.id; 94 | aMap.parentId = data.parentId; 95 | aMap.node = data.node; 96 | actorMap.findOneAndUpdate({"id":data.id, "parentId":data.parentId},aMap,{"upsert":true}, function (err, res) { 97 | if (err) { 98 | console.log('ActorMap err ~ ' + JSON.stringify(err)) 99 | reject(err); 100 | } 101 | resolve(true); 102 | }); 103 | }); 104 | } 105 | 106 | function retrieveChildActorMap(parentId) { 107 | return new Promise(function (resolve, reject) { 108 | actorMap.find({ 109 | "parentId": parentId 110 | },function (err, res) { 111 | if (err) { 112 | console.log('retreiveActorMap err ~ ' + JSON.stringify(err)); 113 | reject(err); 114 | return; 115 | } 116 | resolve(res); 117 | }); 118 | }); 119 | } 120 | 121 | function retrieveActorMap(parentId, actoNode) { 122 | return new Promise(function (resolve, reject) { 123 | actorMap.find({ 124 | "node.port": actoNode.port, 125 | "node.node": actoNode.node, 126 | "parentId": parentId 127 | },function (err, res) { 128 | if (err) { 129 | console.log('retreiveActorMap err ~ ' + JSON.stringify(err)); 130 | reject(err); 131 | return; 132 | } 133 | resolve(res); 134 | }); 135 | }); 136 | } 137 | 138 | function saveRogueMessages(id, messages) { 139 | var rogue_msg = { 140 | actorId: id, 141 | messages: messages 142 | }; 143 | return new Promise(function (resolve, reject) { 144 | rogueMessage.update({ 145 | actorId: id 146 | }, rogue_msg, { 147 | upsert: true 148 | }, function (err, res) { 149 | if (err) { 150 | reject(err); 151 | } 152 | resolve(true); 153 | }); 154 | }); 155 | } 156 | 157 | function retrieveRogueMessages(id) { 158 | return new Promise(function (resolve, reject) { 159 | rogueMessage.find({ 160 | "actorId": id 161 | }, function (err, msgs) { 162 | if (err) { 163 | reject(err); 164 | } 165 | 166 | resolve(msgs); 167 | }); 168 | }); 169 | } 170 | 171 | module.exports = { 172 | incrementBalance: incrementBalance, 173 | decrementBalance: decrementBalance, 174 | saveRogueMessages: saveRogueMessages, 175 | retrieveRogueMessages: retrieveRogueMessages, 176 | saveJournal: saveJournal, 177 | retrieveJournal: retrieveJournal, 178 | saveActorMap: saveActorMap, 179 | retrieveActorMap: retrieveActorMap, 180 | retrieveChildActorMap : retrieveChildActorMap 181 | } -------------------------------------------------------------------------------- /images/ActorModel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pragyandas/jsactor/876d50d604a091e8a18f10b2db7d1f4dddf40722/images/ActorModel.png -------------------------------------------------------------------------------- /images/chaos_monkey.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pragyandas/jsactor/876d50d604a091e8a18f10b2db7d1f4dddf40722/images/chaos_monkey.jpg -------------------------------------------------------------------------------- /images/concurrent_vs_parallel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pragyandas/jsactor/876d50d604a091e8a18f10b2db7d1f4dddf40722/images/concurrent_vs_parallel.png -------------------------------------------------------------------------------- /images/containment.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pragyandas/jsactor/876d50d604a091e8a18f10b2db7d1f4dddf40722/images/containment.jpg -------------------------------------------------------------------------------- /images/failure_recovery.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pragyandas/jsactor/876d50d604a091e8a18f10b2db7d1f4dddf40722/images/failure_recovery.png -------------------------------------------------------------------------------- /images/fig-actor-hierarchy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pragyandas/jsactor/876d50d604a091e8a18f10b2db7d1f4dddf40722/images/fig-actor-hierarchy.png -------------------------------------------------------------------------------- /images/supervision.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pragyandas/jsactor/876d50d604a091e8a18f10b2db7d1f4dddf40722/images/supervision.jpg -------------------------------------------------------------------------------- /lib/actor-service.js: -------------------------------------------------------------------------------- 1 | var genMap = require('generator-mapper.js'); 2 | var Actor = require('actor.js') 3 | var Context = require('../utilities/context.js') 4 | 5 | var Actor = function (id, msg){ 6 | if(msg.child_context){ 7 | var actor = new Actor(genMap(msg.child_context.type),new Context(id, msg.child_context.type, null)); 8 | actor.recieve(msg.message); 9 | return actor; 10 | }else{ 11 | 12 | } 13 | 14 | } 15 | 16 | module.export = { 17 | createRoot : createRoot 18 | 19 | } -------------------------------------------------------------------------------- /lib/actor.js: -------------------------------------------------------------------------------- 1 | "use strict" 2 | 3 | var supervision_strategy = require("../utilities/supervision/supervision-strategy.js"); 4 | var faultInfo = require("../utilities/fault-line.js"); 5 | var services = require("../db/services.js"); 6 | var receiver = require("./receiver.js"); 7 | var system_message_handler = require("./system-message-handler.js"); 8 | var forward_message_handler = require("./forward-message-handler.js"); 9 | var logger = require('../utilities/logger.js').systemLogger; 10 | var Context = require('../utilities/context.js'); 11 | 12 | class Actor { 13 | /** 14 | * [takes a generator and returns an actor instance] 15 | * @param {Generator} fn [generator] 16 | * @param {[object]} context [description] 17 | * @param {[object]} strategy [optional - strategy object] 18 | * @return {[Actor]} [Actor instance] 19 | */ 20 | constructor(fn, context, strategy) { 21 | //the context object stores all the metadata of the actor 22 | this.context = context; 23 | 24 | //startegy object maintains the failure recovery guidelines 25 | //that parent actor can send to the children 26 | //in case children complain "something has gone wrong!!!" 27 | this.strategy = strategy; 28 | 29 | this.generator = fn; 30 | 31 | //if strategy is not applied then the actor deafults to AllForOneStrategy 32 | //as if one/more children fail we revert all the children 33 | if (!this.strategy) { 34 | let maxRetries = 3; 35 | let duration = 500; 36 | this.strategy = new supervision_strategy.OneForOneStrategy(maxRetries, duration); 37 | } 38 | 39 | //collection of incoming mails to handle backpressure 40 | //queue-like implementation 41 | //except the case of system messages 42 | //system messages are shoved into the front of the collection rather than the back 43 | this._mailbox = []; 44 | 45 | //rougue messages - its trouble 46 | // : 47 | this.rogueMessages = {}; 48 | 49 | //prep the actor to receive incoming messages 50 | this._preStart(); 51 | } 52 | 53 | 54 | //preparing the mailbox after initailizing the statestore and the receiver. 55 | //To get value into a generator for processing we need it in suspended state 56 | 57 | //pre start takes care of creating the statestore 58 | //initiating the receiver 59 | _preStart() { 60 | 61 | //flag to indicate if actor is currently processing any message 62 | this._actor_running = false; 63 | 64 | //processes any message sent to it 65 | //and maintains the state of the actor 66 | this._stateStore = this.generator.call(this, this._peek.bind(this)); 67 | 68 | //receives messages 69 | this._receiver = receiver.call(this); 70 | 71 | //initializes the actor to recieve messages 72 | //_stateStore is an instance of the generator 73 | 74 | //initializing _stateStore - sending empty message 75 | this._stateStore.next(); 76 | 77 | //initializing _receiver - sending empty message 78 | this._receiver.next(); 79 | 80 | //subscribe to the cluster event for reacting to any node failure if any child exists on that node 81 | //if node fails then the actor consults the actor map in the db to restart all the dead children 82 | //with the same id so that the children can be restored to the previous state and resume 83 | faultInfo.subscribe(function (data) { 84 | // var ip = machine.node; 85 | // var port = machine.port; 86 | 87 | debugger; 88 | 89 | var self = this; 90 | var machine = { 91 | 'node': data.node, 92 | 'port': data.port 93 | }; 94 | 95 | services.retrieveActorMap(self.context.id, machine).then(function (childCollection) { 96 | childCollection.map(function (actorMap) { 97 | return actorMap.id; 98 | }).forEach(function (childId) { 99 | if (self.context.ifExists(childId)) { 100 | var msg = { 101 | action: "system", 102 | subaction: "recover", 103 | failedNode: machine, 104 | failureTime: data.failureTime 105 | }; 106 | console.log('recovery of ~ ', childId); 107 | var child_context = new Context(childId, self._child_type, self.context.id); 108 | self.context.send(childId, msg, child_context); 109 | } 110 | }); 111 | }, function (err) { 112 | //log error 113 | }) 114 | }.bind(this)); 115 | } 116 | 117 | //recover the actor if the actor has crashed 118 | //will restore the actor to the previously held state at the time of failure 119 | recover(startTime, node) { 120 | //call preRestart 121 | var endTime = this._preRecover(); 122 | var childToRecover = []; 123 | var self = this; 124 | 125 | services.retrieveChildActorMap(self.context.id).then(function (children) { 126 | var childrenIds = children.map(function (child) { 127 | if (child.node.node == node.node && child.node.port == node.port) { 128 | childToRecover.push(child.id); 129 | } 130 | return child.id; 131 | }); 132 | self.supervisedChildren = new Set(childrenIds); 133 | 134 | var msg = { 135 | action: "system", 136 | subaction: "recover", 137 | failedNode: node, 138 | failureTime: startTime 139 | }; 140 | 141 | childToRecover.forEach(function (childId) { 142 | var child_context = new Context(childId, self._child_type, self.context.id); 143 | self.context.send(childId, msg, child_context); 144 | }); 145 | 146 | return services.retrieveJournal(startTime, endTime, self.context.id) 147 | }) 148 | //get all the messages from the journal between the startTime and endTime 149 | //add it to the primary mailbox 150 | 151 | .then(function (journal_msgs) { 152 | self._mailbox = journal_msgs; 153 | //retrive all the rogue messages from the rogue store using the actor id 154 | return services.retrieveRogueMessages(self.context.id) 155 | }) 156 | .then(function (rogue_messages) { 157 | self.rogueMessages = rogue_messages; 158 | self._postRecover(); 159 | }) 160 | .catch(function (err) { 161 | logger.log('Recover error ' + err); 162 | }); 163 | } 164 | 165 | //create auxiliary storage 166 | //refresh the mail 167 | //route all the mails to an auxiliary mailbox - _mailbox=aux_mailbox 168 | _preRecover() { 169 | this._aux_mailbox = []; 170 | this._switch_to_auxiliary_mailbox = true; 171 | return Date.now(); 172 | } 173 | 174 | //add auxiliary message box to the to the end of primary mailbox 175 | //switch primary mailbox to recieve mails 176 | _postRecover() { 177 | this._mailbox = this._mailbox.concat(this._aux_mailbox); 178 | this._aux_mailbox = []; 179 | this._switch_to_auxiliary_mailbox = false; 180 | console.log(this.context.id + " mails after recovery ~ " + this._mailbox); 181 | //peek into the mailbox after it has been restored 182 | this._peek(); 183 | } 184 | 185 | //restart the actor 186 | restart() { 187 | this._preRestart(function () { 188 | var restart_msg = { 189 | action: "system", 190 | subaction: "restart" 191 | } 192 | 193 | //send restart message to each children 194 | for (let child of this.context.supervisedChildren) { 195 | this.context.send(child, restart_msg); 196 | } 197 | 198 | this._postRestart(); 199 | 200 | }.bind(this)); 201 | } 202 | 203 | _preRestart(cb) { 204 | this._suspend_mailbox = true; 205 | } 206 | 207 | _postRestart() { 208 | this._preStart(); 209 | this._peek(); 210 | } 211 | 212 | stop() { 213 | //suspend the mailbox 214 | this._suspend_mailbox = true; 215 | 216 | //check if supervisedChildren set it empty then directly call _postStop 217 | //as actor doesn't need the children to successfully shutdown 218 | if (!this.context.supervisedChildren.size) { 219 | //call _postStop 220 | this._postStop(); 221 | } else { 222 | this._watch_children_callback = function () { 223 | this._postStop(); 224 | 225 | //dump mailbox into system deadletters 226 | 227 | //clear up any other resources if necessary 228 | 229 | //remove actor from current node's actorMap 230 | 231 | }.bind(this); 232 | } 233 | 234 | //create terminate message 235 | var terminate_msg = { 236 | action: "system", 237 | subaction: "terminate" 238 | }; 239 | 240 | //send termination message to each children 241 | for (let child of this.context.supervisedChildren) { 242 | this.context.send(child, terminate_msg); 243 | } 244 | 245 | 246 | } 247 | 248 | registerChildTermination(childId) { 249 | if (this._watch_children_callback) { 250 | if (!this.terminated_set) { 251 | this.terminated_set = new Set(); 252 | } 253 | 254 | this.terminated_set.add(childId); 255 | 256 | if (eqSet(this.terminated_set, this.context.supervisedChildren)) { 257 | this._watch_children_callback(); 258 | } 259 | } 260 | 261 | function eqSet(as, bs) { 262 | if (as.size !== bs.size) return false; 263 | for (var a of as) 264 | if (!bs.has(a)) return false; 265 | return true; 266 | } 267 | } 268 | 269 | //send terminated message to parent 270 | _postStop() { 271 | 272 | //send "terminated" message to the parent 273 | var msg = { 274 | action: "system", 275 | subaction: "terminated" 276 | }; 277 | this.context.send(this.context.parentSupervisorId, msg); 278 | } 279 | 280 | //peek into the mailbox to look for messages 281 | //take one mail at a time to process it 282 | _peek() { 283 | if (!this._suspend_mailbox) { 284 | let msg = this._mailbox.shift(); 285 | 286 | //if we get no message from the mailbox we mark the actor as not running 287 | //i.e. the actor is not processing any messages so that when it recives any new message 288 | //it can restart processing the message by again calling peek 289 | if (msg) { 290 | this._actor_running = true; 291 | setImmediate(function () { 292 | this._stateStore.next(msg); 293 | }.bind(this)); 294 | } else { 295 | this._actor_running = false; 296 | } 297 | } 298 | } 299 | 300 | //takes message and sends it to the receiver 301 | //if auxiliary mailbox is activated 302 | //then store all the incoming messages in the auxiliary mailbox 303 | receive(msg) { 304 | if (this._switch_to_auxiliary_mailbox) { 305 | this._aux_mailbox.push(msg); 306 | } else { 307 | this._receiver.next(msg); 308 | } 309 | } 310 | 311 | //add a message to the rogueMessages object 312 | markRogue(msg) { 313 | this.rogueMessages[msg.msgId] = msg; 314 | } 315 | 316 | //remove message from the rogueMessages list 317 | //it removes and returns the message stored in the object 318 | unmarkRogue(msgId) { 319 | var msg = this.rogueMessages[msgId]; 320 | delete this.rogueMessages[msgId]; 321 | return msg; 322 | } 323 | 324 | //retry the message from the rogue messages list 325 | retry(msgId) { 326 | //get rogue message from the rogue list of messages 327 | var msg = this.unmarkRogue(msgId); 328 | 329 | //push the message into the mailbox - to be retrired 330 | this.receive(msg); 331 | } 332 | 333 | //apply parent provided directive to handle a particular exception 334 | applyDirective(directive, msgId) { 335 | //unmark message from the rogue list 336 | var msg = this.unmarkRogue(msgId); 337 | 338 | //apply directive 339 | } 340 | 341 | //handle forwarded messages 342 | /* forwarded messages are the one which are not processed by the actor 343 | rather forwarded to a particular child in its hierarchy*/ 344 | handleForwardedMessages(mail) { 345 | forward_message_handler.call(this, mail); 346 | } 347 | 348 | //handle system messages 349 | //perform actions based on the subactions specified in the messages 350 | handleSystemMessages(mail) { 351 | system_message_handler.call(this, mail); 352 | 353 | } 354 | 355 | /** 356 | [handleException wrap the exception into a system message 357 | and send it to the parent for processing] 358 | * @param {[object]} exp [exception] 359 | * @param {[object]} mail [mail which caused the exception] 360 | */ 361 | handleException(exp, mail) { 362 | //mark the mail as rogue 363 | this.markRogue(mail); 364 | 365 | //write the rogue message collection to the database 366 | //if the collection doesn't exist for the actor then create 367 | //or replace the existing collection to keep the rougue message collection fresh 368 | //to help in recovery of the actor during failures or enforced restarts 369 | services.saveRogueMessages(this.context.id, this.rogueMessages).then(function (d) { 370 | //log 371 | //logger.log("Rogue messages saved for actor", this.context.id, "successfully"); 372 | }, function (err) { 373 | //log 374 | //logger.log("Cannot save rogue messages for actor", this.context.id); 375 | }); 376 | 377 | //wrap the exception in a system message 378 | var msg = { 379 | action: "system", 380 | subaction: "exception", 381 | payload: exp, 382 | rogueMailId: mail.msgId 383 | }; 384 | 385 | //send it to the parent for processing 386 | this.context.send(this.context.parentSupervisorId, msg); 387 | } 388 | } 389 | 390 | module.exports = Actor; -------------------------------------------------------------------------------- /lib/forward-message-handler.js: -------------------------------------------------------------------------------- 1 | /* 2 | In an hierarchy like 3 | 4 | root 5 | / \ 6 | actor1 actor2 7 | 8 | if actor1 wants to send a message to actor2 then it has to go through the root. 9 | actor1 send the message to the path root/actor2. 10 | The dispatcher of the system looks at the path and decides to wrap the original message in an envelop marked as forward. 11 | When an actor receives a message marked as forward it understands that it doesn't need to process it 12 | rather it needs to forward to an actor down in its hierarchy. 13 | */ 14 | 15 | module.exports = function (mail) { 16 | var id = mail.id.split("/").shift(); 17 | /* 18 | child:{ 19 | id : id, 20 | context: genarated context (can be null/undefined if child exists) 21 | } 22 | */ 23 | 24 | //mail.id can be a single id or a path consisting of 25 | //ids' separated by "/" 26 | 27 | 28 | var child = this.context.createIfNotExists(id, this._child_type); 29 | this.context.send(mail.id, mail.box, child.context); 30 | } -------------------------------------------------------------------------------- /lib/generator-mapper.js: -------------------------------------------------------------------------------- 1 | /*maps the genrators to keys 2 | so that each generator can be referenced 3 | by a name*/ 4 | 5 | 6 | /* 7 | To be implemented : 8 | generator to be injected externally 9 | just like models in loopback 10 | generator added to "generators" folder and mapping in a json 11 | :::: 12 | and adding generator path to system config if required 13 | */ 14 | var path = require('path'); 15 | var _GEN_PATH = "./generators/"; 16 | 17 | module.exports = function (name) { 18 | try { 19 | var genpath = path.join(__dirname, "/generators/" , name + '.js'); 20 | var gen = require(genpath); 21 | return gen; 22 | } catch (exp) { 23 | console.error('error ' + exp); 24 | } 25 | 26 | } -------------------------------------------------------------------------------- /lib/generators/account.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var logger = require('../../utilities/logger.js').systemLogger; 4 | //receive messages and move to states to perform an action 5 | //each action is considered as an unit of work 6 | 7 | module.exports = function* account(callback) { 8 | 9 | var actor = this; 10 | 11 | actor._child_type = "accountBalance"; 12 | 13 | //The allowed states this actor can go to 14 | //after receiving a valid message 15 | const _states = { 16 | "initiate": 0, 17 | "deposit": 1, 18 | "withdraw": 2, 19 | "deposited": 3, 20 | "withdrawn": 4, 21 | "complete": 5 22 | }; 23 | 24 | while (true) { 25 | try { 26 | var mail = yield; 27 | if (mail) { 28 | //get state from the message action 29 | var state = _states[mail.action]; 30 | var payload = mail.payload; 31 | 32 | //move to a particular state using state 33 | switch (state) { 34 | 35 | //Implementing states 36 | case 0: 37 | //fund_transfer 38 | //initiates the fund transfer request 39 | var msg = { 40 | action: "withdraw", 41 | payload: payload 42 | }; 43 | 44 | //send a message to self to initiate a withdrawl 45 | actor.context.send(actor.context.id, msg); 46 | break; 47 | case 1: 48 | //deposit 49 | //spawns child accountBalance if not present 50 | //sends increment message to the child 51 | var child = actor.context.createIfNotExists("AccBal_" + actor.context.id, actor._child_type); 52 | 53 | var msg = { 54 | action: "increment", 55 | payload: payload 56 | }; 57 | 58 | //send message to the child 59 | actor.context.send(child.id, msg, child.context); 60 | break; 61 | case 2: 62 | //withdraw 63 | //spawns child accountBalance if not present 64 | //sends decrement message to the child 65 | var child = actor.context.createIfNotExists("AccBal_" + actor.context.id, actor._child_type); 66 | 67 | var msg = { 68 | action: "decrement", 69 | payload: payload 70 | }; 71 | 72 | //send message to the child 73 | actor.context.send(child.id, msg, child.context); 74 | break; 75 | case 3: 76 | //deposited 77 | //after amount has been deposited to the actor 78 | var msg = { 79 | action: "complete", 80 | payload: payload 81 | } 82 | 83 | //send completed message to parent 84 | actor.context.send("system-root/" + payload.from, msg); 85 | 86 | break; 87 | case 4: 88 | //withdrawn 89 | //after amount has been withdrawn from the current actor 90 | var msg = { 91 | action: "deposit", 92 | payload: payload 93 | } 94 | 95 | //send a message to the receiving actor to deposit amount 96 | actor.context.send("system-root/" + payload.to, msg); 97 | 98 | break; 99 | case 5: 100 | //complete 101 | //send parent message that transfer has completed 102 | var msg = { 103 | action: "complete", 104 | payload: payload 105 | } 106 | 107 | actor.context.send(actor.context.parentSupervisorId, msg); 108 | 109 | break; 110 | default: 111 | //can send a response back stating invalid message received. 112 | break; 113 | } 114 | 115 | //callback for next reading next message in the mailbox 116 | callback(); 117 | 118 | } 119 | } catch (exp) { 120 | logger.log('exception ~ '+ exp); 121 | //actor.handleException(exp, mail); 122 | } 123 | } 124 | } -------------------------------------------------------------------------------- /lib/generators/accountBalance.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | //receive messages and move to states to perform an action 3 | //each action is considered as an unit of work 4 | 5 | var services = require("../../db/services"); 6 | var logger = require('../../utilities/logger.js').systemLogger; 7 | module.exports = function* account(callback) { 8 | 9 | //context stores all the metadata of the actor 10 | var context = this.context; 11 | var strategy = this.strategy; 12 | var actor = this; 13 | //The allowed states this actor can go to 14 | //after receiving a valid message 15 | const _states = { 16 | "increment": 0, 17 | "decrement": 1 18 | }; 19 | 20 | while (true) { 21 | try { 22 | var mail = yield; 23 | if (mail) { 24 | //get state from the message action 25 | var state = _states[mail.action]; 26 | var payload = mail.payload; 27 | //var sender = mail.sender; 28 | 29 | //move to a particular state using state 30 | switch (state) { 31 | 32 | //Implementing states 33 | case 0: 34 | //increment 35 | //call service to increase balance 36 | 37 | //encapsulate the service call to hold the 38 | //correct instance of payload to be sent when the async request returns 39 | (function (mail) { 40 | services.incrementBalance(actor.context.id, mail.payload.amount).then(function (res) { 41 | if (res) { 42 | //send a deposited message with payload 43 | var msg = { 44 | action: "deposited", 45 | payload: mail.payload 46 | } 47 | 48 | //send message to parent 49 | actor.context.send(actor.context.parentSupervisorId, msg); 50 | } 51 | }).catch(function (exp) { 52 | logger.log('err ~ ' + JSON.stringify(exp)); 53 | //actor.handleException(exp, mail); 54 | }) 55 | }(mail)); 56 | break; 57 | case 1: 58 | //decrement 59 | //call service to decrement balance 60 | 61 | //encapsulate the service call to hold the 62 | //correct instance of payload to be sent when the async request returns 63 | (function (mail) { 64 | services.decrementBalance(actor.context.id, mail.payload.amount).then(function (res) { 65 | if (res) { 66 | //send a withdrawn message with payload 67 | var msg = { 68 | action: "withdrawn", 69 | payload: mail.payload 70 | } 71 | 72 | //send message to the parent 73 | actor.context.send(actor.context.parentSupervisorId, msg); 74 | } 75 | }).catch(function (exp) { 76 | logger.log('err ~ ' + JSON.parse(exp)); 77 | //actor.handleException(exp, mail); 78 | }) 79 | }(mail)); 80 | break; 81 | 82 | default: 83 | //can send a response back stating invalid message received. 84 | break; 85 | } 86 | 87 | //callback for next reading next message in the mailbox 88 | callback(); 89 | } 90 | } catch (exp) { 91 | logger.log('exception ~ ' + exp); 92 | //actor.handleException(exp, mail); 93 | } 94 | } 95 | } -------------------------------------------------------------------------------- /lib/generators/system-root.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /*THE ROOT ACTOR*/ 3 | //root of the actor hierarchy 4 | //supervises the first level of actors - order actors in this case 5 | //it creates new actors based on demand and assigns the required message 6 | 7 | //creates new actors for each order 8 | // 9 | module.exports = function* root(callback) { 10 | 11 | var actor = this; 12 | 13 | actor._child_type = "account"; 14 | 15 | var _states = { 16 | "initiate": 0, 17 | "complete": 1 18 | } 19 | 20 | while (true) { 21 | //receive messages 22 | try { 23 | var mail = yield; 24 | if (mail) { 25 | //get the state from message action 26 | var state = _states[mail.action]; 27 | switch (state) { 28 | case 0: 29 | //if child doesn't exist create context 30 | var child = actor.context.createIfNotExists(mail.payload.id, actor._child_type); 31 | 32 | var msg = { 33 | action: "initiate", 34 | payload: mail.payload.message 35 | }; 36 | actor.context.send(child.id, msg, child.context); 37 | break; 38 | case 1: 39 | //tell the client that the request has completed successfully 40 | var payload = mail.payload; 41 | var sender = payload.sender_context; 42 | 43 | //get the child information from the parent 44 | //send out the response to the client 45 | break; 46 | default: 47 | //invalid message 48 | break; 49 | } 50 | 51 | //callback for next reading next message in the mailbox 52 | callback(); 53 | } 54 | } catch (exp) { 55 | //tell the client something went wrong 56 | } 57 | } 58 | 59 | } -------------------------------------------------------------------------------- /lib/receiver.js: -------------------------------------------------------------------------------- 1 | "use strict" 2 | 3 | //receiver is used by actor to decide on the received messages 4 | //there can be three category of messages: 5 | //system : denotes any system event such exception, restart, shutdown, etc. 6 | //forward : denotes any message that needs to be forwarded to an actor through the current actor 7 | //others: these are regular messages that are exchanged between actors for processing and performing tasks 8 | 9 | module.exports = function* () { 10 | //receiver generator which receives messages 11 | var _states = { 12 | "system": 0, 13 | "forward": 1 14 | }; 15 | 16 | while (true) { 17 | let msg = yield; 18 | if (msg) { 19 | var state = _states[msg.action]; 20 | switch (state) { 21 | case 0: 22 | setImmediate(function () { 23 | this.handleSystemMessages(msg); 24 | }.bind(this)); 25 | break; 26 | case 1: 27 | setImmediate(function () { 28 | this.handleForwardedMessages(msg); 29 | }.bind(this)); 30 | break; 31 | default: 32 | //push message to the mailbox 33 | this._mailbox.push(msg); 34 | 35 | //peek into the mailbox if actor is not running 36 | //and new message arivved for processing 37 | if (!this._actor_running) { 38 | this._peek(); 39 | } 40 | } 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /lib/system-message-handler.js: -------------------------------------------------------------------------------- 1 | //used by actor to handle received system messages 2 | 3 | module.exports = function (mail) { 4 | var _system_messages = { 5 | "exception": 0, 6 | "retry": 1, 7 | "directive": 2, 8 | "terminate": 3, 9 | "terminated": 4, 10 | "restart": 5, 11 | "recover": 6 12 | } 13 | 14 | switch (_system_messages[mail.subaction]) { 15 | case 0: 16 | //fetch strategy by registering the rogue mail id 17 | var derived_strategy = this.strategy.registerException(mail.payload, mail.rogueMailId); 18 | 19 | //the derived_strategy contains either a retry command or a directive 20 | /*if the message crosses the exception per duration limit 21 | as specified by the strategy 22 | then a directive is triggered 23 | to enforce the counter measures on the particular message*/ 24 | if (derived_strategy.retry) { 25 | 26 | //subaction retry sent to the sender of the exception 27 | var msg = { 28 | action: "system", 29 | subaction: "retry", 30 | rogueMailId: mail.rogueMailId 31 | } 32 | 33 | this.context.send(mail.sender.id, msg); 34 | } else { 35 | 36 | //subaction directive is sent to the sender of the exception 37 | var msg = { 38 | action: "system", 39 | subaction: "directive", 40 | rogueMailId: mail.rogueMailId, 41 | payload: derived_strategy.directive 42 | } 43 | 44 | this.context.send(mail.sender.id, msg); 45 | } 46 | 47 | break; 48 | case 1: 49 | //retry message 50 | this.retry(mail.rogueMailId); 51 | break; 52 | case 2: 53 | 54 | //apply the directive 55 | this.applyDirective(mail.directive, mail.rogueMailId); 56 | break; 57 | case 3: 58 | //upon receiving terminate messsage the actor initiates shutdown 59 | this.stop(); 60 | break; 61 | case 4: 62 | //terminated message is received by the actor from the child actor 63 | //register the termination of each child until all the children have been successfully terminated 64 | this.registerChildTermination(mail.sender.id); 65 | break; 66 | case 5: 67 | //restart message is received by the actor from the parent actor 68 | this.restart(); 69 | break; 70 | case 6: 71 | //restart message is received by the actor from the parent actor 72 | this.recover(mail.failureTime, mail.failedNode); 73 | break; 74 | default: 75 | //invalid subaction 76 | break; 77 | } 78 | } -------------------------------------------------------------------------------- /logs/failure.log: -------------------------------------------------------------------------------- 1 | fault -------------------------------------------------------------------------------- /logs/system.log: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pragyandas/jsactor/876d50d604a091e8a18f10b2db7d1f4dddf40722/logs/system.log -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "actor-pattern-shopping-cart-poc", 3 | "version": "1.0.0", 4 | "description": "While a subroutine is executed sequentially and straightly, a coroutine can suspend and resume its execution at distinct points in code. Thus, coroutines are good primitives for cooperative task handling, as coroutines are able to yield execution. The advantages of cooperative task handling especially in case of massive parallelism of asynchronous operations such as I/O are in plenty.", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "Pragyan", 10 | "license": "ISC", 11 | "dependencies": { 12 | "async": "^2.1.2", 13 | "body-parser": "^1.15.2", 14 | "express": "^4.14.0", 15 | "hashring": "^3.2.0", 16 | "mongoose": "^4.6.4", 17 | "node-uuid": "^1.4.7", 18 | "restler": "^3.4.0", 19 | "rx": "^4.1.0", 20 | "socket.io": "^1.5.1", 21 | "socket.io-client": "^1.5.1", 22 | "winston": "^2.2.0" 23 | }, 24 | "devDependencies": { 25 | "chai": "^3.5.0", 26 | "mocha": "^3.1.2", 27 | "sinon": "^1.17.6", 28 | "sinon-chai": "^2.8.0" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /routes/acto-dispatcher.js: -------------------------------------------------------------------------------- 1 | "use strict" 2 | 3 | var HashRing = require('hashring'); 4 | var nodeCount = 0; 5 | var express = require('express'); 6 | var bodyParser = require('body-parser'); 7 | var rest = require('restler'); 8 | var Context = require('../utilities/context.js'); 9 | var Journal = require('../utilities/journal.js'); 10 | var journal = new Journal(); 11 | var config = require('../config.json'); 12 | var logger = require('../utilities/logger.js').systemLogger; 13 | var port = config.dispatcherPort || 3000; 14 | var ring = new HashRing('http://localhost:3001'); //['http://localhost:3001', 'http://localhost:3002','http://localhost:3003'] 15 | var _map = {}; 16 | var nodes = []; 17 | var app = express(); 18 | app.use(bodyParser.json()); // for parsing application/json 19 | app.use(bodyParser.urlencoded({ 20 | extended: true 21 | })); // for parsing application/x-www-form-urlencoded 22 | var http = require('http').Server(app); 23 | var io = require('socket.io')(http); 24 | var matchIp = '((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))' 25 | 26 | journal.out.subscribe(function (message) { 27 | //var message = {msg:env?env:msg,actorId:forward} 28 | var actoNode = ring.get(message.actorId); 29 | rest.post(actoNode + '/Actor', { 30 | data: { 31 | id: message.actorId, 32 | msg: message.msg 33 | } 34 | }); 35 | }); 36 | 37 | /** 38 | * for external message, urlencoded "message" object should be like 39 | * {"id":,"type": "","message": }, 40 | **/ 41 | app.post('/create', function (req, res) { 42 | var data = null; 43 | if (typeof req.body.message === 'string') { 44 | data = JSON.parse(req.body.message); 45 | } else { 46 | data = req.body.message; 47 | } 48 | 49 | 50 | //POST to node for creating the actor and return the success/failure 51 | var msg = { 52 | action: 'initiate', 53 | payload: { 54 | "id": data.id, 55 | "type": data.type, 56 | "message": data.message 57 | } 58 | }; 59 | journal.in.onNext({ 60 | msg: msg, 61 | actorId: 'system-root' 62 | }); 63 | return res.send('transfer initiated'); 64 | }); 65 | 66 | 67 | /** 68 | * for internal actor to send message to a specific actor or to system-root using a path, 69 | * urlencoded "req.body" should be like 70 | * {"id":,"type": "message": }, 71 | * 72 | * "id": could be a specific actor ID or a path like system-root/1234/321 73 | **/ 74 | app.post('/send', function (req, res) { 75 | var forward = req.body.id; 76 | var msg = req.body.message; 77 | //wrapped in a box 78 | var env = null; 79 | //check if its new or forward 80 | var paths = forward.split('/'); 81 | if (paths.length > 1) { 82 | forward = paths.splice(0, 1); 83 | paths = paths.join('/'); 84 | env = {}; 85 | env.id = paths; 86 | env.action = 'forward'; 87 | env.box = msg; 88 | } 89 | //POST to node for creating the actor and return the success/failure 90 | journal.in.onNext({ 91 | msg: env ? env : msg, 92 | actorId: forward 93 | }); 94 | return res.end(); 95 | }); 96 | 97 | //handle socket disconnection, on disconnection remove server from node ring 98 | io.on('connection', function (socket) { 99 | console.log('NODE: A connection established ~ from ' + socket.request.connection.remoteAddress.toString().match(matchIp)[1] + ' port ' + socket.request._query.port); 100 | 101 | socket.on('disconnect', function (s) { 102 | var nodeData = nodes[socket.id]; 103 | nodeData.failureTime = Date.now(); 104 | io.sockets.emit('event', nodeData); 105 | var server = 'http://' + nodeData.node + ':' + nodeData.port; 106 | ring.remove(server); 107 | nodes.splice(nodes.indexOf(socket.id), 1); 108 | nodeCount--; 109 | console.log(server + ' got disconnected.'); 110 | }); 111 | }); 112 | 113 | //socket middleware to track connection from clients and add server of new clients to the node ring 114 | io.use(function (socket, next) { 115 | var ip = socket.request.connection.remoteAddress.toString().match(matchIp)[1]; 116 | ip = (ip == "127.0.0.1") ? 'localhost' : ip; 117 | var port = socket.request._query.port; 118 | var server = 'http://' + ip + ':' + port; 119 | 120 | if (!ring.has(server)) { // if server is not already added to ring 121 | console.log('new server added - ' + server) 122 | ring.add(server); //then add the server 123 | } 124 | //keep track of socket and respective server details 125 | nodes[socket.id] = { 126 | 'node': ip, 127 | 'port': port 128 | }; 129 | nodeCount++ 130 | //Hard coded for time being... root would be created as soon as 3001 port establish a connection 131 | if (server == config.rootNodeServer) { 132 | var actoNode = ring.get('system-root'); 133 | console.log('Creating root @ ' + actoNode); 134 | var rootContext = new Context('system-root', 'system-root', null); 135 | //this would create root actor 136 | var msg = { 137 | action: null, 138 | payload: null, 139 | sender_context: null, 140 | child_context: rootContext 141 | }; 142 | 143 | var root = { 144 | id: 'system-root', 145 | msg: msg 146 | }; 147 | 148 | rest.post(actoNode + '/Actor', { 149 | data: root 150 | }).on('complete', function (data, response) { 151 | console.log('root process initiated!'); 152 | }); 153 | } 154 | next(); 155 | }); 156 | 157 | http.listen(port, function () { 158 | console.log('listening on port 3000!'); 159 | }); -------------------------------------------------------------------------------- /routes/acto-node.js: -------------------------------------------------------------------------------- 1 | "use strict" 2 | 3 | var express = require('express'); 4 | var bodyParser = require('body-parser'); 5 | var mongoose = require("mongoose"); 6 | var port = process.env.PORT || 1337; 7 | var Context = require('../utilities/context.js'); 8 | var logger = require('../utilities/logger.js').systemLogger; 9 | var Actor = require('../lib/actor.js'); 10 | var genMap = require('../lib/generator-mapper.js'); 11 | var _map = {}; 12 | var actorMap = require('../utilities/actor-mapper.js'); 13 | var app = express(); 14 | var faultInfo = require('../utilities/fault-line.js'); 15 | var io = require('socket.io-client'), 16 | 17 | //connect to dispatcher, pass port as parameter (query is the predefined key to pass parameter in socket.io) 18 | socket = io.connect('http://localhost:3000',{'query': 'port=' + port}); 19 | 20 | //listen to any even if any node got disconnected from node ring 21 | socket.on('event', function(data){ 22 | //push to subject so that all actor subscribing this can know about any event on node ring 23 | faultInfo.onNext(data); 24 | }); 25 | 26 | app.use(bodyParser.json()); // for parsing application/json 27 | app.use(bodyParser.urlencoded({ extended: true })); // for parsing application/x-www-form-urlencoded 28 | 29 | //remove a actor in case an actor is terminated 30 | function removeActorFromMap(actorId){ 31 | delete _map[actorId]; 32 | } 33 | 34 | //yet to implement 35 | app.get('/Actor/:id', function(req,res){ 36 | res.send('TO DO: do something') 37 | return; 38 | }); 39 | 40 | /** 41 | * /Actor is express route to accept a message from dispatcher, no one else talk to acto-node 42 | * req.body comes as 43 | * {id:, msg: } 44 | */ 45 | app.post('/Actor', function(req, res){ 46 | var pkg = req.body; 47 | var msg = pkg.msg; 48 | var actorId = req.body.id; 49 | var parentId = msg && msg.child_context ? msg.child_context.parentSupervisorId : null; 50 | 51 | var aMap = {} 52 | if(parentId){ 53 | aMap.id = actorId; 54 | aMap.parentId = parentId; 55 | aMap.node = { 'node': req.get('host').split(":")[0], 'port': port } ; 56 | actorMap.onNext(aMap); 57 | } 58 | 59 | if(msg && msg.child_context){ // child_context present means the actor needs to be created 60 | //TO DO: need to check if actor exists and child_context came by mistake 61 | msg.child_context = new Context(msg.child_context.id, msg.child_context.type,parentId); 62 | var generator = genMap(msg.child_context.type); 63 | var actor = new Actor(generator,msg.child_context); 64 | _map[actorId] = actor; 65 | actor.receive(msg); 66 | }else{ //no child_context, hense actor already exists 67 | _map[actorId] && _map[actorId].receive(msg); 68 | } 69 | return res.end(); 70 | 71 | }); 72 | 73 | 74 | app.listen(port, function () { 75 | console.log('listening on port ' + port); 76 | }); -------------------------------------------------------------------------------- /test/actor.core.spec.js: -------------------------------------------------------------------------------- 1 | var chai = require('chai'); 2 | // var expect = require('chai').expect; 3 | var sinon = require('sinon'); 4 | var sinonChai = require('sinon-chai'); 5 | var Actor = require("../lib/actor.js"); 6 | var Context = require("../utilities/context.js"); 7 | 8 | chai.use(sinonChai); 9 | 10 | //create actor and perform actor tasks 11 | describe("create actor and perform basic actor behaviour", function () { 12 | 13 | var genMap = { 14 | 'foo': foo, 15 | 'bar': bar 16 | } 17 | 18 | var actorMap = {}; 19 | 20 | 21 | //stubbing the router used by the context to send message to other actors 22 | var router = { 23 | send: function (id, mail) { 24 | var _actor = actorMap[id]; 25 | //if actor doesn't exist create actor and add to actor map 26 | if (!_actor && mail.child_context) { 27 | _actor = new Actor(genMap[mail.child_context.type], mail.child_context); 28 | actorMap[id] = _actor; 29 | } 30 | _actor.receive(mail); 31 | // console.log("sent"); 32 | } 33 | } 34 | 35 | function* foo(callback) { 36 | 37 | var actor = this; 38 | 39 | const _states = { 40 | "start": 0, 41 | "end": 1 42 | }; 43 | 44 | while (true) { 45 | try { 46 | var mail = yield; 47 | if (mail) { 48 | //get state from the message action 49 | var state = _states[mail.action]; 50 | var payload = mail.payload; 51 | 52 | //move to a particular state using state 53 | switch (state) { 54 | //Implementing states 55 | case 0: 56 | var child = actor.context.createIfNotExists("4321", "bar"); 57 | var msg = { 58 | action: "eat" 59 | }; 60 | actor.context.send(child.id, msg, child.context); 61 | break; 62 | case 1: 63 | console.log("end"); 64 | break; 65 | default: 66 | 67 | } 68 | 69 | //callback for next reading next message in the mailbox 70 | callback(); 71 | 72 | } 73 | } catch (exp) { 74 | 75 | } 76 | } 77 | } 78 | 79 | function* bar(callback) { 80 | 81 | var actor = this; 82 | 83 | const _states = { 84 | "eat": 0, 85 | "sleep": 1 86 | }; 87 | 88 | while (true) { 89 | try { 90 | var mail = yield; 91 | if (mail) { 92 | //get state from the message action 93 | var state = _states[mail.action]; 94 | var payload = mail.payload; 95 | 96 | //move to a particular state using state 97 | switch (state) { 98 | //Implementing states 99 | case 0: 100 | console.log("eat"); 101 | break; 102 | case 1: 103 | console.log("sleep"); 104 | break; 105 | default: 106 | 107 | } 108 | 109 | //callback for next reading next message in the mailbox 110 | callback(); 111 | 112 | } 113 | } catch (exp) { 114 | 115 | } 116 | } 117 | } 118 | 119 | 120 | this.timeout(2000); 121 | 122 | 123 | before(function () { 124 | sinon.spy(console, 'log'); 125 | }); 126 | 127 | 128 | //create an actor 129 | it("should create an actor of type foo", function (done) { 130 | var actor_props = { 131 | id: '1234', 132 | type: "foo", 133 | parent: null 134 | } 135 | var context = new Context(actor_props.id, actor_props.type, actor_props.parent, router); 136 | var actor = new Actor(foo, context); 137 | 138 | actorMap[actor_props.id] = actor; 139 | 140 | chai.expect(actor.generator.name).to.equal("foo"); 141 | chai.expect(actor.context.id).to.equal('1234'); 142 | done(); 143 | }); 144 | 145 | it("should receive a message", function (done) { 146 | var message = { 147 | action: "end" 148 | } 149 | 150 | router.send("1234", message); 151 | setImmediate(function () { 152 | chai.expect(console.log).to.have.been.called; 153 | chai.expect(console.log).to.have.been.calledWith("end"); 154 | done(); 155 | }); 156 | 157 | }); 158 | 159 | it("should be able to create child actor", function (done) { 160 | var message = { 161 | action: "start" 162 | } 163 | 164 | router.send("1234", message); 165 | setImmediate(function () { 166 | chai.expect(actorMap["4321"]).to.be.an.instanceof(Actor); 167 | done(); 168 | }); 169 | }); 170 | 171 | it("should be able send a message", function (done) { 172 | var message = { 173 | action: "start" 174 | } 175 | 176 | router.send("1234", message); 177 | 178 | setImmediate(function () { 179 | chai.expect(console.log).to.have.been.called; 180 | chai.expect(console.log).to.have.been.calledWith("eat"); 181 | done(); 182 | }); 183 | }); 184 | }) -------------------------------------------------------------------------------- /test/integration.test.js: -------------------------------------------------------------------------------- 1 | var Rx = require('rx'); 2 | var rest = require('restler'); 3 | var async = require('async'); 4 | var fs = require('fs'); 5 | var testData = JSON.parse(fs.readFileSync('./test_data/test-data-100.json', 'utf8')); 6 | 7 | console.time('Test'); 8 | async.eachSeries(testData, function (item, cb) { 9 | rest.post('http://localhost:3000/create', { 10 | data: { 11 | message: item 12 | } 13 | }).on('complete', function (data, response) { 14 | console.log('FT: ~ ' + JSON.stringify(item)); 15 | cb(); 16 | }); 17 | 18 | }, function () { 19 | console.timeEnd('Test'); 20 | }) 21 | /* 22 | var numbers = Rx.Observable.timer(0, 100); 23 | numbers.subscribe(function (x) { 24 | var msg = { 25 | from: x, 26 | to: x+1, 27 | amount: 100 28 | } 29 | rest.post('http://localhost:3000/create',{data:{id:x, type:'account',message:msg}}).on('complete',function(data, response){ 30 | console.log(x); 31 | }); 32 | }); 33 | */ -------------------------------------------------------------------------------- /test/test_data/test-data-100.json: -------------------------------------------------------------------------------- 1 | [ 2 | {"id":1111,"type": "account","message": { "from":1111, "to":2222, "amount":100 } }, 3 | {"id":2222,"type": "account","message": { "from":2222, "to":3333, "amount":100 } }, 4 | {"id":1111,"type": "account","message": { "from":1111, "to":3333, "amount":100 } }, 5 | {"id":3333,"type": "account","message": { "from":3333, "to":2222, "amount":100 } }, 6 | {"id":2222,"type": "account","message": { "from":2222, "to":1111, "amount":100 } }, 7 | {"id":3333,"type": "account","message": { "from":3333, "to":4444, "amount":100 } }, 8 | {"id":4444,"type": "account","message": { "from":4444, "to":2222, "amount":100 } }, 9 | {"id":1111,"type": "account","message": { "from":1111, "to":4444, "amount":100 } }, 10 | {"id":4444,"type": "account","message": { "from":4444, "to":1111, "amount":100 } }, 11 | {"id":2222,"type": "account","message": { "from":2222, "to":3333, "amount":100 } }, 12 | {"id":1111,"type": "account","message": { "from":1111, "to":3333, "amount":100 } }, 13 | {"id":3333,"type": "account","message": { "from":3333, "to":2222, "amount":100 } }, 14 | {"id":2222,"type": "account","message": { "from":2222, "to":1111, "amount":100 } }, 15 | {"id":3333,"type": "account","message": { "from":3333, "to":4444, "amount":100 } }, 16 | {"id":4444,"type": "account","message": { "from":4444, "to":2222, "amount":100 } }, 17 | {"id":1111,"type": "account","message": { "from":1111, "to":4444, "amount":100 } }, 18 | {"id":4444,"type": "account","message": { "from":4444, "to":1111, "amount":100 } }, 19 | {"id":2222,"type": "account","message": { "from":2222, "to":3333, "amount":100 } }, 20 | {"id":1111,"type": "account","message": { "from":1111, "to":4444, "amount":100 } }, 21 | {"id":4444,"type": "account","message": { "from":4444, "to":1111, "amount":100 } }, 22 | {"id":2222,"type": "account","message": { "from":2222, "to":3333, "amount":100 } }, 23 | {"id":1111,"type": "account","message": { "from":1111, "to":3333, "amount":100 } }, 24 | {"id":3333,"type": "account","message": { "from":3333, "to":2222, "amount":100 } }, 25 | {"id":3333,"type": "account","message": { "from":3333, "to":4444, "amount":100 } }, 26 | {"id":4444,"type": "account","message": { "from":4444, "to":2222, "amount":100 } }, 27 | {"id":1111,"type": "account","message": { "from":1111, "to":4444, "amount":100 } }, 28 | {"id":4444,"type": "account","message": { "from":4444, "to":1111, "amount":100 } }, 29 | {"id":2222,"type": "account","message": { "from":2222, "to":3333, "amount":100 } }, 30 | {"id":1111,"type": "account","message": { "from":1111, "to":4444, "amount":100 } }, 31 | {"id":4444,"type": "account","message": { "from":4444, "to":1111, "amount":100 } }, 32 | {"id":4444,"type": "account","message": { "from":4444, "to":2222, "amount":100 } }, 33 | {"id":1111,"type": "account","message": { "from":1111, "to":4444, "amount":100 } }, 34 | {"id":4444,"type": "account","message": { "from":4444, "to":1111, "amount":100 } }, 35 | {"id":2222,"type": "account","message": { "from":2222, "to":3333, "amount":100 } }, 36 | {"id":1111,"type": "account","message": { "from":1111, "to":3333, "amount":100 } }, 37 | {"id":3333,"type": "account","message": { "from":3333, "to":2222, "amount":100 } }, 38 | {"id":2222,"type": "account","message": { "from":2222, "to":1111, "amount":100 } }, 39 | {"id":3333,"type": "account","message": { "from":3333, "to":4444, "amount":100 } }, 40 | {"id":4444,"type": "account","message": { "from":4444, "to":2222, "amount":100 } }, 41 | {"id":1111,"type": "account","message": { "from":1111, "to":4444, "amount":100 } }, 42 | {"id":4444,"type": "account","message": { "from":4444, "to":1111, "amount":100 } }, 43 | {"id":2222,"type": "account","message": { "from":2222, "to":3333, "amount":100 } }, 44 | {"id":1111,"type": "account","message": { "from":1111, "to":4444, "amount":100 } }, 45 | {"id":4444,"type": "account","message": { "from":4444, "to":1111, "amount":100 } }, 46 | {"id":2222,"type": "account","message": { "from":2222, "to":3333, "amount":100 } }, 47 | {"id":1111,"type": "account","message": { "from":1111, "to":3333, "amount":100 } }, 48 | {"id":3333,"type": "account","message": { "from":3333, "to":2222, "amount":100 } }, 49 | {"id":3333,"type": "account","message": { "from":3333, "to":4444, "amount":100 } }, 50 | {"id":3333,"type": "account","message": { "from":3333, "to":4444, "amount":100 } }, 51 | {"id":4444,"type": "account","message": { "from":4444, "to":2222, "amount":100 } }, 52 | {"id":1111,"type": "account","message": { "from":1111, "to":4444, "amount":100 } }, 53 | {"id":4444,"type": "account","message": { "from":4444, "to":1111, "amount":100 } }, 54 | {"id":2222,"type": "account","message": { "from":2222, "to":3333, "amount":100 } }, 55 | {"id":1111,"type": "account","message": { "from":1111, "to":3333, "amount":100 } }, 56 | {"id":3333,"type": "account","message": { "from":3333, "to":2222, "amount":100 } }, 57 | {"id":2222,"type": "account","message": { "from":2222, "to":1111, "amount":100 } }, 58 | {"id":3333,"type": "account","message": { "from":3333, "to":4444, "amount":100 } }, 59 | {"id":4444,"type": "account","message": { "from":4444, "to":2222, "amount":100 } }, 60 | {"id":1111,"type": "account","message": { "from":1111, "to":4444, "amount":100 } }, 61 | {"id":4444,"type": "account","message": { "from":4444, "to":1111, "amount":100 } }, 62 | {"id":3333,"type": "account","message": { "from":3333, "to":4444, "amount":100 } }, 63 | {"id":4444,"type": "account","message": { "from":4444, "to":2222, "amount":100 } }, 64 | {"id":1111,"type": "account","message": { "from":1111, "to":4444, "amount":100 } }, 65 | {"id":4444,"type": "account","message": { "from":4444, "to":1111, "amount":100 } }, 66 | {"id":2222,"type": "account","message": { "from":2222, "to":3333, "amount":100 } }, 67 | {"id":1111,"type": "account","message": { "from":1111, "to":3333, "amount":100 } }, 68 | {"id":3333,"type": "account","message": { "from":3333, "to":2222, "amount":100 } }, 69 | {"id":2222,"type": "account","message": { "from":2222, "to":1111, "amount":100 } }, 70 | {"id":3333,"type": "account","message": { "from":3333, "to":4444, "amount":100 } }, 71 | {"id":4444,"type": "account","message": { "from":4444, "to":2222, "amount":100 } }, 72 | {"id":1111,"type": "account","message": { "from":1111, "to":4444, "amount":100 } }, 73 | {"id":4444,"type": "account","message": { "from":4444, "to":1111, "amount":100 } }, 74 | {"id":2222,"type": "account","message": { "from":2222, "to":3333, "amount":100 } }, 75 | {"id":1111,"type": "account","message": { "from":1111, "to":4444, "amount":100 } }, 76 | {"id":4444,"type": "account","message": { "from":4444, "to":1111, "amount":100 } }, 77 | {"id":2222,"type": "account","message": { "from":2222, "to":3333, "amount":100 } }, 78 | {"id":1111,"type": "account","message": { "from":1111, "to":3333, "amount":100 } }, 79 | {"id":3333,"type": "account","message": { "from":3333, "to":2222, "amount":100 } }, 80 | {"id":3333,"type": "account","message": { "from":3333, "to":4444, "amount":100 } }, 81 | {"id":4444,"type": "account","message": { "from":4444, "to":2222, "amount":100 } }, 82 | {"id":1111,"type": "account","message": { "from":1111, "to":4444, "amount":100 } }, 83 | {"id":4444,"type": "account","message": { "from":4444, "to":1111, "amount":100 } }, 84 | {"id":2222,"type": "account","message": { "from":2222, "to":3333, "amount":100 } }, 85 | {"id":1111,"type": "account","message": { "from":1111, "to":4444, "amount":100 } }, 86 | {"id":4444,"type": "account","message": { "from":4444, "to":1111, "amount":100 } }, 87 | {"id":4444,"type": "account","message": { "from":4444, "to":2222, "amount":100 } }, 88 | {"id":1111,"type": "account","message": { "from":1111, "to":4444, "amount":100 } }, 89 | {"id":4444,"type": "account","message": { "from":4444, "to":1111, "amount":100 } }, 90 | {"id":2222,"type": "account","message": { "from":2222, "to":3333, "amount":100 } }, 91 | {"id":1111,"type": "account","message": { "from":1111, "to":3333, "amount":100 } }, 92 | {"id":3333,"type": "account","message": { "from":3333, "to":2222, "amount":100 } }, 93 | {"id":2222,"type": "account","message": { "from":2222, "to":1111, "amount":100 } }, 94 | {"id":3333,"type": "account","message": { "from":3333, "to":4444, "amount":100 } }, 95 | {"id":4444,"type": "account","message": { "from":4444, "to":2222, "amount":100 } }, 96 | {"id":1111,"type": "account","message": { "from":1111, "to":4444, "amount":100 } }, 97 | {"id":4444,"type": "account","message": { "from":4444, "to":1111, "amount":100 } }, 98 | {"id":2222,"type": "account","message": { "from":2222, "to":3333, "amount":100 } }, 99 | {"id":1111,"type": "account","message": { "from":1111, "to":4444, "amount":100 } }, 100 | {"id":4444,"type": "account","message": { "from":4444, "to":1111, "amount":100 } } 101 | ] -------------------------------------------------------------------------------- /test/test_data/test-data.json: -------------------------------------------------------------------------------- 1 | [ 2 | {"id":1111,"type": "account","message": { "from":1111, "to":2222, "amount":100 } }, 3 | {"id":2222,"type": "account","message": { "from":2222, "to":3333, "amount":100 } } 4 | ] -------------------------------------------------------------------------------- /utilities/actor-mapper.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var Rx = require('rx'); 3 | var Subject = Rx.Subject; 4 | var Observable = Rx.Observable; 5 | var services = require("../db/services"); 6 | var fail = require('./logger.js').failureLogger; 7 | var logger = require('./logger.js').systemLogger; 8 | var MAX_RETRIES = 3; 9 | 10 | 11 | var actor_mapper = new Subject(); 12 | 13 | 14 | actor_mapper.flatMap(get_observable_from_promise) 15 | .subscribe(function (d) { 16 | //log db save successful message 17 | }); 18 | 19 | 20 | //takes a value to perform db opration and returns an observable from promise 21 | //retry mechanism built_in to retry specified number of times 22 | //if retry fails then it it logged into a file 23 | function get_observable_from_promise(val) { 24 | var query = Rx.Observable.fromPromise(services.saveActorMap(val)); 25 | 26 | var retry_count = 0; 27 | 28 | return query.retryWhen(retryStrategy(retry_count)).catch(function (err) { 29 | //log errored out messages after it has been retried the specified number of times 30 | fail.log(err.payload); 31 | return Rx.Observable.empty(); 32 | }); 33 | } 34 | 35 | //retries the rejected or failed promise 36 | //errors out when retry_count > MAX_RETRIES 37 | function retryStrategy(retry_count) { 38 | return function (errors) { 39 | return errors.map(function (x) { 40 | if (retry_count > MAX_RETRIES) { 41 | throw new Error({message:"Cannot write to actor map", payload: x }); 42 | } 43 | retry_count++; 44 | return x; 45 | }); 46 | } 47 | } 48 | 49 | module.exports = actor_mapper; 50 | -------------------------------------------------------------------------------- /utilities/context.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | //context describe the meta-data for an actor 4 | //It maintains various information like 5 | //parent of the actor 6 | //children list of the actor 7 | //It listens to cluster events and make an actor aware of the same 8 | //It also exposes factory api to create new actor - which an actor can use to create children 9 | 10 | var uuid = require("node-uuid"); 11 | var config = require("../actor-config.json"); 12 | var defaultRouter = require('./router.js'); 13 | 14 | class Context { 15 | //life-cycle monitoring - listens to system events - To be implemented 16 | 17 | //parentSupervisor - the parent actor 18 | //supervisedChildren - the set of supervisedChildren 19 | constructor(id, type, parentSupervisorId, router) { 20 | //actor id 21 | this.id = id; 22 | 23 | //the type denotes the generator 24 | //to be used as the message processing core of the actor 25 | this.type = type; 26 | 27 | //id of the parent 28 | this.parentSupervisorId = parentSupervisorId; 29 | 30 | //set router if not exist 31 | this.router = router || defaultRouter; 32 | 33 | //set of children ids 34 | this.supervisedChildren = new Set(); 35 | 36 | //status of the children 37 | this.supervisedChildrenStatus = {}; 38 | } 39 | 40 | 41 | //factory method to create child actor for a particular actor 42 | /** 43 | * [create child for the actor] 44 | * @param {[string]} id [the id with which child need be created] 45 | * @param {[string]} type [type of the generator function] 46 | * @return {[object]} context [the generated context] 47 | */ 48 | createChildContext(id, type) { 49 | 50 | //add to supervisedChildren set 51 | this.supervisedChildren.add(id); 52 | 53 | //create context object with id and this.id 54 | //new Context(id,this.id,pathFromRoot); 55 | return new Context(id, type, this.id); 56 | } 57 | 58 | //create child context if child doesn't exist 59 | createIfNotExists(id, type) { 60 | //check if the child exists with id 61 | //if child exists pass the childId 62 | //else pass both the childId and the child_context 63 | 64 | //populate only the id 65 | var child = { 66 | id: id 67 | }; 68 | 69 | //if id doesn't exist then create and populate context 70 | if (!this.ifExists(id)) { 71 | child.context = this.createChildContext(id, type); 72 | this.supervisedChildrenStatus[id] = "created"; 73 | } 74 | 75 | return child; 76 | } 77 | 78 | //check if child exists 79 | ifExists(id) { 80 | //check if the actor is present in the set 81 | return this.supervisedChildren.has(id); 82 | } 83 | 84 | /** 85 | * [used to send message to different actor in the system] 86 | * @param {[id]} id [actor identifier] 87 | * @param {[object]} msg [message object to be sent] 88 | * @param {[object]} child_context [child context object - only used when creating new actor] 89 | * @return {[undefined]} [NA] 90 | */ 91 | send(id, message, child_context) { 92 | 93 | //wrapping essentials in an object 94 | var mail = Object.assign(message, { 95 | sender: this, 96 | child_context: child_context 97 | }); 98 | //send message to router which is responsible for discovering 99 | //actor location and sending the message to the actor 100 | this.router.send(id, mail); 101 | } 102 | } 103 | 104 | module.exports = Context; -------------------------------------------------------------------------------- /utilities/fault-line.js: -------------------------------------------------------------------------------- 1 | var Rx = require('rx'); 2 | var faultInfo = new Rx.Subject(); 3 | 4 | 5 | module.exports = faultInfo; -------------------------------------------------------------------------------- /utilities/journal.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | //stream the messages through the journal function which 3 | //does all the journalizing while passing the messages downstream to the required actor. 4 | 5 | 6 | //fires and forgets promises 7 | //so that whenever a message is sent around the system it gets 8 | //implicitly stored without adding any delay to the overall communication cycle. 9 | 10 | var Rx = require('rx'); 11 | var Subject = Rx.Subject; 12 | var Observable = Rx.Observable; 13 | var uuid = require("node-uuid"); 14 | var services = require("../db/services"); 15 | 16 | var MAX_RETRIES = 3; 17 | 18 | class Journal { 19 | constructor() { 20 | /*subject is both an Observable as well as an Observer 21 | so the messages when arriving at the actor are passed thorugh 22 | this subject so that it can just perform a few actions `` 23 | and then return to the world subscibed from.*/ 24 | 25 | //the start and end of the pipe 26 | //data can be pushed through one end 27 | //data can be subscribed through the other end 28 | //journalizing happens in the pipe without any delay 29 | this.in = new Subject(); 30 | this.out = this.in.map(generateId).do(journalize); 31 | } 32 | } 33 | 34 | /* 35 | message, 36 | actorId, 37 | msgId, 38 | timestamp 39 | */ 40 | 41 | //time-based id to identify each message with a unique number 42 | function generateId(mail) { 43 | 44 | mail.msg = Object.assign(mail.msg, { 45 | msgId: uuid.v4() 46 | }); 47 | 48 | mail.timestamp = Date.now(); 49 | 50 | return mail; 51 | } 52 | 53 | //for each value pumped into the subject(this.in) 54 | //journalize will create an observable from Promise(wrapper of database call) 55 | 56 | //Observable sequence has retryStrategy 57 | //built-in to handle the failure of Promise 58 | 59 | //when the promise resolves the message can be logged 60 | /*when the promise rejects the : 61 | 1 - value can be put into memory to be periodically retried 62 | 2 - can be logged with a retry count of 1 63 | periodic retries will increase the retry count 64 | for each in-memory store entry on repeated failure */ 65 | function journalize(mail) { 66 | var retry_count = 0; 67 | 68 | Observable 69 | .fromPromise(services.saveJournal(mail)) 70 | .retryWhen(retryStrategy(retry_count)) 71 | .subscribe(function (x) { 72 | //TODO: log 73 | }, function (err) { 74 | //write to in_memory store and log 75 | }); 76 | } 77 | 78 | 79 | //retries the rejected or failed promise 80 | //errors out when retry_count > MAX_RETRIES 81 | function retryStrategy(retry_count) { 82 | return function (errors) { 83 | return errors.map(function (x) { 84 | if (retry_count > MAX_RETRIES) { 85 | throw new Error("Cannot write to journal"); 86 | } 87 | retry_count++; 88 | return x; 89 | }); 90 | } 91 | } 92 | 93 | module.exports = Journal; -------------------------------------------------------------------------------- /utilities/logger.js: -------------------------------------------------------------------------------- 1 | var winston = require('winston'); 2 | 3 | var failureLogger = new (winston.Logger)({ 4 | transports: [ 5 | new (winston.transports.File)({ filename: '../logs/failure.log' }) 6 | ] 7 | }); 8 | 9 | var systemLogger = new (winston.Logger)({ 10 | transports: [ 11 | new (winston.transports.File)({ filename: '../logs/system.log' }) 12 | ] 13 | }); 14 | 15 | module.exports = { 16 | failureLogger: failureLogger, 17 | systemLogger: systemLogger 18 | } -------------------------------------------------------------------------------- /utilities/router.js: -------------------------------------------------------------------------------- 1 | var rest = require('restler'); 2 | var config = require('../config.json'); 3 | var dispatherUrl = config.dispatcherURL + ':'+ config.dispatcherPort + '/send'; // dispatcher's "send" route listens to internal message 4 | 5 | /** 6 | * helper function, always pass the message to dispather using target actor id 7 | * id: target actor id 8 | * mail: message to be passed 9 | */ 10 | var send = function(id, mail){ 11 | rest.post(dispatherUrl, {data:{id:id,message:mail}}); 12 | } 13 | 14 | module.exports = { 15 | send : send 16 | } -------------------------------------------------------------------------------- /utilities/supervision/supervision-directive.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | //Supervision directives 3 | //depending on exception from the child actor the parent actor can use any 4 | //of the mentioned directives to counter the failure. 5 | var Directive = { 6 | //Restart - restart the child default directive - most commonly used 7 | "Restart": function () { 8 | let msg = { 9 | action: "system", 10 | payload: "restart" 11 | } 12 | }, 13 | 14 | //Escalate - parent doesn't know what to do - stops everything and asks its parent to take control 15 | "Escalate": function () { 16 | let msg = { 17 | action: "system", 18 | payload: "escalate" 19 | } 20 | }, 21 | 22 | //Stop - terminates the child actor 23 | "Stop": function () { 24 | let msg = { 25 | action: "system", 26 | payload: "stop" 27 | } 28 | }, 29 | 30 | //Resume - ignores the error - least used strategy 31 | "Resume": function () { 32 | let msg = { 33 | action: "system", 34 | payload: "resume" 35 | } 36 | } 37 | } 38 | 39 | module.exports = Directive; -------------------------------------------------------------------------------- /utilities/supervision/supervision-strategy.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | //Supervision strategy is used by each actor to supervise the child actors 4 | //It is used to countering failures of the child actors 5 | //This helps in containment of the error 6 | 7 | /* 8 | Supervision strategies: 9 | One-For-One - default - affects the direct children 10 | All-For-One - affects the whole hierarchy of the parent 11 | */ 12 | 13 | var Directive = require("./supervision-directive.js"); 14 | 15 | class SupervisorStrategy { 16 | constructor(maxRetries, duration) { 17 | //if the number of retries is greater than maxRetries 18 | //then call the decider with the exception 19 | //and the decider will decide the directive it needs to dispatch 20 | //to deal with the upset child 21 | this._maxRetries = maxRetries; 22 | this._duration = duration; 23 | 24 | //to record the retry count for each child 25 | this._retries = {}; 26 | 27 | //maintain timer for each child 28 | this._timers = {}; 29 | } 30 | 31 | //getter and setter for each property 32 | get duration() { 33 | return this._duration; 34 | } 35 | 36 | set duration(duration) { 37 | this._duration = duration; 38 | } 39 | 40 | get maxRetries() { 41 | return this._maxRetries; 42 | } 43 | 44 | set maxRetries(maxRetries) { 45 | this._maxRetries = maxRetries; 46 | } 47 | 48 | get timers() { 49 | return this._timers; 50 | } 51 | 52 | get retries() { 53 | return this._retries; 54 | } 55 | 56 | //returns a directive based on types of exception 57 | //if exception doesn't match any of the exception types 58 | //"Restart directive is dispatched" 59 | decider(exp) { 60 | 61 | // Maybe ArithmeticException is not application critical 62 | // so we just ignore the error and keep going. 63 | // Error that we have no idea what to do with 64 | // Error that we can't recover from, stop the failing child 65 | // otherwise restart the failing child 66 | 67 | //example directiveExceptionMap 68 | // { 69 | // "ArithmeticException": Directive.Resume, 70 | // "InsanelyBadException:/": Directive.Escalete, 71 | // "NotSupportedException:(": Directive.Stop, 72 | // } 73 | 74 | return this._directiveExceptionMap[exp.type || typeof exp] || Directive.Restart; 75 | 76 | } 77 | 78 | //actor calls register exception to receive an acknowledgement 79 | //to retry or use directive to apply on the child 80 | registerException(exp, key) { 81 | //increase retry count on each retry 82 | var retryCount = this.retries[key] = (this.retries[key] && ++this.retries[key]) || 1; 83 | if (retryCount === 1) { 84 | 85 | //reset retry count after the duration 86 | this.timers[key] = setTimeout(function () { 87 | this.retries[key] = 0; 88 | }.bind(this), this.duration); 89 | return { 90 | retry: true, 91 | directive: null 92 | }; 93 | 94 | } else if (retryCount > this.maxRetries) { 95 | 96 | //if retries is more than maxRetries 97 | //clear timeout and return the appropriate directive 98 | //using decider 99 | clearTimeout(this.timers[key]); 100 | this.retries[key] = 0; 101 | return { 102 | retry: false, 103 | directive: this.decider(exp) 104 | }; 105 | } else { 106 | return { 107 | retry: true, 108 | directive: null 109 | }; 110 | } 111 | } 112 | 113 | } 114 | 115 | 116 | class OneForOneStrategy extends SupervisorStrategy { 117 | /** 118 | * [initialize instance] 119 | * @param {[number]} maxRetries [in integer] 120 | * @param {[number]} duration [in milliseconds] 121 | * @param {[object]} directiveExceptionMap [:<["Resume","Escalate","Stop","Restart"]>] 122 | * @return {[OneForOneStrategy]} [instance] 123 | */ 124 | constructor(maxRetries, duration, directiveExceptionMap) { 125 | super(maxRetries, duration); 126 | this._directiveExceptionMap = directiveExceptionMap || { 127 | //To be implemented 128 | MinorRecoverableException: "Restart", 129 | Exception: "Stop" 130 | } 131 | } 132 | } 133 | 134 | class AllForOneStrategy extends SupervisorStrategy { 135 | /** 136 | * [initialize instance] 137 | * @param {[number]} maxRetries [in integer] 138 | * @param {[number]} duration [in milliseconds] 139 | * @param {[object]} directiveExceptionMap [:<["Resume","Escalate","Stop","Restart"]>] 140 | * @return {[AllForOneStrategy]} [instance] 141 | */ 142 | constructor(maxRetries, duration, directiveExceptionMap) { 143 | super(maxRetries, duration); 144 | this._directiveExceptionMap = directiveExceptionMap || { 145 | //to be implemented 146 | MajorUnRecoverableException: "Stop", 147 | Exception: "Escalate" 148 | } 149 | } 150 | } 151 | 152 | //export modules 153 | module.exports = { 154 | OneForOneStrategy: OneForOneStrategy, 155 | AllForOneStrategy: AllForOneStrategy 156 | } --------------------------------------------------------------------------------