Coder Social home page Coder Social logo

comedy's Introduction

Comedy

Build Status Windows Tests codecov

Logo

Comedy is a Node.js actor framework.

Actors are all about flexible scalability. After describing your application in terms of actors, you can scale arbitrary parts of the application to multiple cores on a single host (by spawning sub-processes) or even to multiple hosts in your network by simply modifying the configuration and without changing a single line of code.

Note: Breaking changes in version 2.0 are covered here.

Installation

Comedy is installed with NPM by running:

npm install comedy

After that you can use Comedy framework in your code by requiring comedy package.

var actors = require('comedy');

Quick Start

Running your first actor is as simple as follows:

var actors = require('comedy');

var actorSystem = actors(); // Create an actor system.

var myActorPromise = actorSystem
  .rootActor() // Get a root actor reference.
  .then(rootActor => {
    return rootActor.createChild({ // Create a child actor that says hello.
      sayHello: to => {
        console.log(`Hello, ${to}!`)
      }
    });
  });

myActorPromise.then(myActor => {
  // Our actor is ready, we can send messages to it.
  myActor.send('sayHello', 'world');
});

This will print

Hello, world!

along with some other log messages from a created actor system.

So, the steps required to create and run a minimal actor are the following:

  1. Create an actor system. You would normally do that in your main (startup) script. There is a bunch of options that you can pass when creating an actor system, and these options will be discussed in later sections. For now, we'll be just using the defaults.
  2. Get a reference to a Root actor. Actors can only be created by other actors, so you need an initial actor to start from. This actor is called a Root actor, and you can get it from actor system by using rootActor() method. The method returns not the actor itself, but a Promise of the Root actor. To get an actual reference, we use Promise.then() method. (Comedy uses Bluebird promise library. For more information about promise API, please refer to Bluebird documentation).
  3. Create your actor as a child of a Root actor by using createChild() method. This method takes an actor definition as a first argument. An actor definition describes a behaviour of an actor: it defines what messages an actor can accept and how does it respond (message handlers) as well as how an actor is initialized and destroyed (lifecycle hooks). Actor definition can be represented in several formats. In our example, we're using a plain object actor definition with a single message handler, that handles sayHello message. It awaits a single to argument, prints a message to console and does not respond anything.

Class-Defined Actors

In previous section we've used plain-object actor definition to create our hello world actor. Another way to define actor behaviour is to use a class:

var actors = require('comedy');

/**
 * Actor definition class.
 */
class MyActor {
  sayHello(to) {
    console.log(`Hello, ${to}!`);
  }
}

actors()
  .rootActor() // Get a root actor reference.
  .then(rootActor => rootActor.createChild(MyActor)) // Create a class-defined child actor.
  .then(myActor => {
    // Our actor is ready, we can send messages to it.
    myActor.send('sayHello', 'world');
  });

This example does exactly the same as previous one. The difference is that we have defined our actor behaviour using a JavaScript class. In this definition, each class method becomes a message handler. An instance of MyActor class is created together with an actor instance during actor creation.

The class definition option may be better for several reasons:

  • When using classes for defining actor behaviour, you take full advantage of the object-oriented programming and useful class properties such as inheritance and data encapsulation.
  • Your existing application is likely to be already described in terms of classes and their relations. Given that, it's easy to use any of your existing classes as an actor definition without probably modifying anything inside this class.

Module-Defined Actors

If your class is defined in a separate file, making a module (which is most likely the case), you can simply a specify a path to this module in createChild() method.

Let's say, our MyActor class from previous example is defined in a separate module called MyActor.js that resides in actors folder:

actors/MyActor.js:

/**
 * Actor definition class.
 */
class MyActor {
  sayHello(to) {
    console.log(`Hello, ${to}!`);
  }
}

module.exports = MyActor;

Then we can reference it in createChild() method by simply specifying a module path:

var actors = require('comedy');

actors()
  .rootActor() // Get a root actor reference.
  .then(rootActor => rootActor.createChild('/actors/MyActor')) // Create a module-defined child actor.
  .then(myActor => {
    // Our actor is ready, we can send messages to it.
    myActor.send('sayHello', 'world');
  });

This example would again print "Hello world!".

When we put a slash at the start of our module path, the module is looked-up relative to the project root (a folder where the package.json file is).

Important note about code transfer

Though module-defined actor may seem like a mere shortcut for specifying a direct class reference, it has a subtle difference in case of creating forked or remote actors (separate-process actors, see below), that you should be aware of. That is: when you create a forked/remote (separate-process) actor with class-defined behaviour, Comedy serializes the code of your class definition and passes it to a child actor process, where it is being compiled. This means that you cannot reference external variables (such as module imports) from your class, because these external variables won't be recognized by a child process and actor definition compilation will fail (you can import modules inside your class definition, however, and that will work).

When using module-defined actors, you have no such problem, because in this case Comedy simply passes a module path to a child process, where it is then imported using a regular Node.js module resolution process.

Given the above, module path is a preferred way of specifying actor definition to createChild() method. Class and plain-object definitions may still be a good option when a definition is simple and self-contained and you don't want to bother creating a separate file for it.

Scaling

The whole point of actors is the ability to scale on demand. You can turn any actor to a standalone process and let it utilize additional CPU core on either local or remote machine. This is done by just using a configuration property, which can be specified both programmaticaly and using a configuration file. Let's see the programmatic example first.

Programmatic configuration

The following example runs MyActor actor as a separate operating system process.

var actors = require('comedy');

/**
 * Actor definition class.
 */
class MyActor {
  sayHello(to) {
    // Reply with a message, containing self PID.
    return `Hello to ${to} from ${process.pid}!`;
  }
}

// Create an actor system.
var actorSystem = actors();

actorSystem
  // Get a root actor reference.
  .rootActor()
  // Create a class-defined child actor, that is run in a separate process (forked mode).
  .then(rootActor => rootActor.createChild(MyActor, { mode: 'forked' }))
  // Send a message to our forked actor with a self process PID.
  .then(myActor => myActor.sendAndReceive('sayHello', process.pid))
  .then(reply => {
    // Output result.
    console.log(`Actor replied: ${reply}`);
  })
  // Destroy the system, killing all actor processes.
  .finally(() => actorSystem.destroy());

In the example above we define MyActor with a sayHello message handler, which replies with a string containing the self process PID. Then, like in previous examples, we create an actor system, get a root actor, and create a child actor with MyActor definition. But here we specify an additional option: { mode: 'forked' }, that tells the actor system that this actor should be run in a separate process ("forked" mode). Then, once child actor is created, we send a message with sayHello topic and wait for response using sendAndReceive method. For a message body we, again, use self process PID. Once the response from child actor is received, we print it to console and destroy the actor system.

The output for this example should contain a string like:

Actor replied: Hello to 15327 from 15338!

As you see, the self PID that we send and the self PID that MyActor replies with are different, which means that they are run in separate processes. The process where MyActor is run will be a child of a process, where an actor system is created, and the messaging between actors turns from method invocation to an inter-process communication.

If you switch to in-memory mode by changing mode option value from "forked" to "in-memory" (which is a default and is equivalent to just omitting the options in createChild method), then both root actor and MyActor actor will be run in the same process, the messaging between actors will boil down to method invocation and the PIDs in the resulting message will be the same.

actorSystem
 .rootActor()
 // ...
 .then(rootActor => rootActor.createChild(MyActor, { mode: 'in-memory' }))
 // ...
 .finally(() => actorSystem.destroy());
Actor replied: Hello to 19585 from 19585!

Using configuration file

An alternative for using programmatic actor configuration is a configuration file. It is a JSON file with an actor name to options mapping, like the one below:

{
  "MyActor": {
    "mode": "in-memory"
  },
  "MyAnotherActor": {
    "mode": "forked"
  }
}

The above file states that actor with name MyActor should be run in in-memory mode, while actor named MyAnotherActor should be run in forked mode. If you name this file actors.json and place it at the root of your project (a directory where your package.json file is), Comedy will automatically pick this file and use the actor configuration from there.

You can also put your actor configuration file wherever you want and give it arbitrary name, but in this case you should explicitly specify a path to your actor configuration file when creating the actor system:

var actorSystem = actors({
  config: '/path/to/your/actor-configuration.json'
});

You can use both the default actors.json configuration file and your custom configuration file, in which case the configuration from the default actors.json file is extended with the custom configuration (what is missing in custom configuration is looked up in default).

Please note that for a given actor programmatic configuration takes precedence over file configuration: only those configuration properties that are missing in programmatic configuration are taken from file configuration. So, for example, if you have programmaticaly specified that the actor should run in in-memory mode, there is no way to override it using the file configuration.

Hot configuration change

Starting from Comedy 2.0, no application restart is needed when you modify actor configuration file. Comedy automatically detects changes and dynamically rebuilds actor hierarchy where needed.

Scaling to multiple instances

Besides forking just one single instance of your actor to a separate process, you can spawn multiple instances of your actor to multiple separate processes by simply using a configuration property. This configuration property is named clusterSize. Here is an example:

var actors = require('comedy');
var P = require('bluebird');

/**
 * Actor definition class.
 */
class MyActor {
  sayHello(to) {
    // Reply with a message, containing self PID.
    return `Hello to ${to} from ${process.pid}!`;
  }
}

// Create an actor system.
var actorSystem = actors();

actorSystem
  // Get a root actor reference.
  .rootActor()
  // Create a class-defined child actor.
  .then(rootActor => rootActor.createChild(MyActor, {
    mode: 'forked', // Spawn separate process.
    clusterSize: 3 // Spawn 3 instances of this actor to load-balance over.
  }))
  .then(myActor => {
    // Sequentially send 6 messages to our newly-created actor cluster.
    // The messages will be load-balanced between 3 forked actors using
    // the default balancing strategy (round-robin).
    return P.each([1, 2, 3, 4, 5, 6], number => {
      return myActor.sendAndReceive('sayHello', `${process.pid}-${number}`)
        .then(reply => {
          console.log(`Actor replied: ${reply}`);
        });
    });
  })
  .finally(() => actorSystem.destroy());

The output for this example will look something like this:

Actor replied: Hello to 15400-1 from 15410!
Actor replied: Hello to 15400-2 from 15416!
Actor replied: Hello to 15400-3 from 15422!
Actor replied: Hello to 15400-4 from 15410!
Actor replied: Hello to 15400-5 from 15416!
Actor replied: Hello to 15400-6 from 15422!

As you see, the root actor messages are being round-robin-balanced between 3 child instances of MyActor actor.

The clusterSize configuration property can be as well used in JSON configuration:

{
  "MyActor": {
    "mode": "forked",
    "clusterSize": 3
  }
}

While round-robin balancer is a default, you can also use random balancer by specifying "balancer": "random" in actor configuration:

{
  "MyActor": {
    "mode": "forked",
    "clusterSize": 3,
    "balancer": "random"
  }
}

Remote Actors

In the examples above we used "forked" mode to spawn child processes and utilize additional CPU cores on local machine. But Comedy won't be a full-fledged actor framework without remoting capability. Using "remote" mode, you can launch an actor in a separate process on the host of your choice.

Let's take our example with "forked" mode and just change the mode to "remote":

var actors = require('comedy');

/**
 * Actor definition class.
 */
class MyActor {
  sayHello(to) {
    // Reply with a message, containing self PID.
    return `Hello to ${to} from ${process.pid}!`;
  }
}

// Create an actor system.
var actorSystem = actors();

actorSystem
  // Get a root actor reference.
  .rootActor()
  // Create a class-defined child actor, that is run on a remote machine (remote mode).
  .then(rootActor => rootActor.createChild(MyActor, { mode: 'remote', host: '192.168.33.20' }))
  // Send a message to our remote actor with a self process PID.
  .then(myActor => myActor.sendAndReceive('sayHello', process.pid))
  .then(reply => {
    // Output result.
    console.log(`Actor replied: ${reply}`);
  })
  // Log errors.
  .catch(err => {
    console.error(err);
  })
  // Destroy the system, killing all actor processes.
  .finally(() => actorSystem.destroy());

We have only done one tiny modification: changed child actor mode from "forked" to "remote" and specified a host parameter, that is mandatory for remote mode. The remote mode and host parameters can also be specified using actors.json configuration file.

Now let's run our new example. What we get is:

Sun Jun 11 2017 22:00:43 GMT+0300 (MSK) - info: Didn't find (or couldn't load) default configuration file /home/weekens/workspace/comedy/actors.json.
Sun Jun 11 2017 22:00:43 GMT+0300 (MSK) - info: Resulting actor configuration: {}
{ Error: connect EHOSTUNREACH 192.168.33.20:6161
    at Object.exports._errnoException (util.js:1018:11)
    at exports._exceptionWithHostPort (util.js:1041:20)
    at TCPConnectWrap.afterConnect [as oncomplete] (net.js:1086:14)
  code: 'EHOSTUNREACH',
  errno: 'EHOSTUNREACH',
  syscall: 'connect',
  address: '192.168.33.20',
  port: 6161 }

The error was thrown, because we need one more thing to be able to launch remote actor on 192.168.33.20: we need to launch a special Comedy listening node on this machine. To do this, we run the following commands on the target machine:

$ npm install comedy
$ node_modules/.bin/comedy-node
Mon Jun 12 2017 16:56:07 GMT+0300 (MSK) - info: Listening on :::6161

The last message tells us that the listener node is successfully launched and listening on default Comedy port 6161.

After running example again we get:

Mon Jun 12 2017 16:56:14 GMT+0300 (MSK) - info: Didn't find (or couldn't load) default configuration file /home/weekens/workspace/comedy/actors.json.
Mon Jun 12 2017 16:56:14 GMT+0300 (MSK) - info: Resulting actor configuration: {}
Actor replied: Hello to 8378 from 8391!

This means our remote actor successfully replied to our local actor from remote machine.

Named clusters

Most of the time you may probably want to launch a remote actor not on a single machine, but on a cluster of several machines, one actor per host or more. Comedy allows you to configure a named cluster, which can be later referenced by "cluster" actor option instead of "host" option.

var actors = require('comedy');
var P = require('bluebird');

/**
 * Actor definition class.
 */
class MyActor {
  sayHello(to) {
    // Reply with a message, containing self PID.
    return `Hello to ${to} from ${process.pid}!`;
  }
}

// Create an actor system.
var actorSystem = actors({
  clusters: {
    // Configure "alpha" cluster, consisting of 3 hosts (each should have comedy-node started).
    alpha: ['192.168.33.10', '192.168.33.20', '192.168.33.30']
  }
});

actorSystem
  // Get a root actor reference.
  .rootActor()
  // Create a class-defined child actor on each host of "alpha" cluster.
  .then(rootActor => rootActor.createChild(MyActor, { mode: 'remote', cluster: 'alpha' }))
  // Send a message to each remote actor in a cluster (messages are round-robin balanced).
  .then(myActor => P.map([1, 2, 3], () => myActor.sendAndReceive('sayHello', process.pid)
    .then(reply => {
      // Output result. There should be replies from each actor in a cluster.
      console.log(`Actor replied: ${reply}`);
    })))
  // Log errors.
  .catch(err => {
    console.error(err);
  })
  // Destroy the system, killing all actor processes.
  .finally(() => actorSystem.destroy());

By default, Comedy creates 1 actor per host in a cluster. You can override this by specifying "clusterSize" actor parameter. Comedy will try to distribute the "clusterSize" amount of actor instances equally between the hosts in a cluster, but "clusterSize" is not divisible by the number of hosts, some hosts will have 1 actor less than others.

Multiple hosts

Specifying a cluster for remote actor requires prior actor system configuration (see section above). In case you need to just quickly launch an actor on multiple hosts, you may not bother configuring a cluster and simply specify a host array in "host" actor parameter.

rootActor.createChild(MyActor, { mode: 'remote', host: ['192.168.33.10', '192.168.33.20', '192.168.33.30'] })

This works the same way as "cluster" parameter. The only difference is that now the cluster is unnamed.

Threaded Actors

In NodeJS v10 worker threads were introduced. Worker threads are additional threads within the same NodeJS process, and they allow scaling the load between multiple CPU cores without forking sub-processes. This makes scaling more lightweight and fast, because worker thread is faster to spawn and consumes less resources than a separate OS process.

Despite the fact that NodeJS worker threads run within the same process boundary, they are completely isolated from each other and can only interact by means of messaging. This fits quite nicely into actor paradigm.

Comedy actors support a "threaded" mode. In this mode an actor is launched in a separate worker thread.

In the example above, to run MyActor as a worker thread, you simply specify the following configuration:

{
  "MyActor": {
    "mode": "threaded"
  }
}

And if you want to distribute the load between multiple worker threads (say, 3), you just add a "clusterSize" option, as usual:

{
  "MyActor": {
    "mode": "threaded",
    "clusterSize": 3
  }
}

In NodeJS v10 worker threads are an experimental feature, so they are not enabled by default. To enable them, you need to run your NodeJS executable with --experimental-worker option.

Actor Respawning (Supervision)

When an actor operates in "in-memory" mode, only one of two things may happen: either the actor is working or the whole process has failed. But when an actor is in "forked" or "remote" mode, it operates in a separate process and communicates with it's parent across the process boundary. In this case, a child process can fail while a parent process is alive, and thus the child actor may stop responding to messages from parent actor.

Comedy has a built-in capability of respawning a crashed child process and restoring the child actor. It creates a new process and a new actor, so, the in-memory state that was kept in a child actor at that point would be lost. But in all other respects the new child actor will be identical to the lost one, except for actor ID, which will be new for respawned actor.

To enable automatic child actor respawn, you just need to specify "onCrash": "respawn" actor parameter:

{
  "MyForkedActor": {
    "mode": "forked",
    "onCrash": "respawn"
  }
}

The configuration above enables automatic actor respawn for all actors with name "MyForkedActor".

There is some additional overhead that you get for enabling this option. Namely, when automatic respawning is enabled, the child actor with "onCrash": "respawn" configured and it's parent actor will exchange keep-alive ("ping-pong") messages every 5 seconds. If a child actor does not respond to ping within 5 seconds, it is considered dead and a new child process with new child actor is launched to replace the lost one.

Actor Lifecycle

Like plain objects, actors live and die. The difference is that an actor instance can be created in a separate process or even on a separate machine, which is why actor creation and destruction is asynchronous.

An actor lifecycle is represented by the diagram below:

Actor Lifecycle

As you can see from this diagram, an actor passes several states along it's life. These are:

  • Initializing
  • Working
  • Destroying
  • Destroyed

Some of the above state transitions can be handled by lifecycle hooks - special methods in actor definition, which are all optional.

These lifecycle hooks are covered in the following sections.

initialize() lifecycle hook

After an actor instance is created, an actor immediately enters Initializing state. At this point, Comedy first ensures an actor definition instance is created, and then attempts to call an initialize() method of an actor definition instance.

If an initialize() method is absent, an actor immediately enters Working state and is ready to handle incoming messages.

If an initialize() method is present in actor definition, Comedy calls this method passing a self actor instance reference as an input parameter, and looks at return value. If a return value is a Promise, an actor initialization is considered asynchronous and an actor enters Working state only when a returned promise is resolved. In other cases actor enters Working state immediately after initialize() returns.

If initialize() throws exception or a promise returned from initialize() is rejected, the actor initialization is considered failed, and an actor enters Destroying state, which basically starts actor destruction process (this will be covered later).

With initialize() lifecycle hook you can initialize all the things needed for you actor to work. Very often you will create child actors exactly in initialize():

class MyActor {
  initialize(selfActor) {
    // Create child actor.
    return selfActor.createChild(MyChildActor)
      .then(childActor => {
        // Save created child actor to instance field.
        this.childActor = childActor;
      });
  }
}

In the example above, MyActor will only start handling incoming messages once it's child actor is created and fully initialized.

destroy() lifecycle hook

There are several events that can remove actor from existence:

  • a destroy() method has been explicitly called on actor (this can be done by actor itself);
  • a parent actor is being destroyed;
  • an actor process is killed;
  • an actor initialization failed (covered above).

In normal cases an actor is destroyed gracefully, which means that it has a chance to do all necessary clean-up actions before final termination. These actions include destroying all immediate children and calling a destroy() lifecycle hook on actor definition instance.

destroy() lifecycle hook is similar to initialize() - it is passed in a self actor reference and is allowed to return promise, in which case a destruction is considered asynchronous and is only finished once a returned promise is resolved.

The algorithm of actor destruction is the following:

  1. Enter Destroying state. At this point actor no longer accepts incoming messages.
  2. Destroy immediate actor children. All errors generated by child destruction process are logged and ignored. Children are destroyed simultaneously.
  3. Call destroy() lifecycle hook on self actor definition instance.
  4. Once destroy() finishes, enter Destroyed state, notify parent and remove actor from memory.

Logging

Your actor system can quickly become complex enough to require logging facility to trace messages between actors as well as individual actor workflow.

Comedy comes with a built-in logging facility that lets you write log messages with various severity levels and distinguish log messages from different actors in a single log output.

A logging is done with a Logger instance, that can be retrieved for an actor system or for a particular actor using getLog() method.

var actors = require('comedy');

// Create an actor system.
var actorSystem = actors();

/**
 * Example actor definition.
 */
class MyActor {
  initialize(selfActor) {
    // Output actor-level log message.
    selfActor.getLog().info('MyActor initialized.');
  }
}

actorSystem
  .rootActor()
  .then(rootActor => {
    // Output system-level log message.
    actorSystem.getLog().info('Actor system initialized. Creating MyActor actor...');

    return rootActor.createChild(MyActor);
  });

This example will output something like:

Mon Jan 09 2017 17:44:16 GMT+0300 (MSK) - info: Actor system initialized. Creating MyActor actor...
Mon Jan 09 2017 17:44:16 GMT+0300 (MSK) - info: InMemoryActor(5873a1c0705ebd2a663c3eeb, MyActor): MyActor initialized.

The first, system-level message, is prefixed with a current date-time and a log level label. The second, actor-level message, additionally prefixed with a description of an actor that writes the message. This prefixing is done automatically.

An actor description has the form:

ActorInstanceClassName(actorId, actorName)

It is exactly what you get when calling and Actor.toString() method.

An actor instance class name reveals the underlying actor instance implementation, which depends on an actor mode, and can be one of InMemoryActor, ForkedActorParent or ForkedActorChild. An actor ID is generated automatically for a given actor instance and is unique across the system.

Setting the log level

In some cases you might not want Comedy to do logging at all. In others you may want extended debug-level logging. Also, you might want to enable verbose logging for a certain actor while keeping the level for the rest of actors.

The log level is configured using setLevel() method of Logger instance.

There are 5 log levels in Comedy logger (each one includes all previous):

  1. Silent. No log messages are written.
  2. Error. Error messages are written.
  3. Warn. Warning messages are written.
  4. Info. Information messages are written. These messages are typically interesting for system administrators.
  5. Debug. Debug messages are written. These are messages that are only interesting for application developers and include some information about Comedy internals.

Here is an example of how you would configure logging level for the whole actor system:

// Create an actor system.
var actorSystem = actors();

// Set Silent logging level - no messages from Comedy will be written.
actorSystem.getLog().setLevel(1);

You can also use log level constants:

// Create an actor system.
var actorSystem = actors();

// Get system-level logger.
var logger = actorSystem.getLog();

// Set Debug log level.
logger.setLevel(logger.levels().Debug);

In a similar way, you can configure log level for a particular actor:

class MyActor {
  initialize(selfActor) {
    var logger = selfActor.getLog();
    logger.setLevel(logger.levels().Debug);
  }

  // ...
}

Dynamic logger configuration

Programmatic logger configuration described above is usually not what you really want. Instead of hard-coding log levels for various actors, you would normally prefer configuring these log levels in a configuration file to be able to change them in runtime. Comedy allows you doing this with dynamic logger configuration capability.

To configure log levels dynamically, you need to:

  1. Create a file with logger configuration (usually named logger.json) in the directory of your choice.
  2. Specify a path to this file in loggerConfiguration parameter in actor system options.

The logger.json file has the following format:

{
  "categories": {
    "Default": "Info",
    "MyActor": "Error",
    "MyOtherActor": "Debug"
  }
}

Here we have a category mapping under categories key. This mapping object maps logging category name to a log level. Each category name is just an actor name. So, with the above category mapping you can configure log levels on per-actor basis. There is a special category name - "Default" - which configures the default log level.

In the exampe above: actor(s) with name "MyActor" will log messages with Error log level or higher; actor(s) with name "MyOtherActor" will log messages with Debug log level or higher; all other actors will log messages with Info level or higher.

To enable file-based logger configuration, you need to specify a path to your logger.json file (or whatever the name is) in actor system configuration:

var actors = require('comedy');

//...

actors({
  loggerConfiguration: 'conf/logger.json' // You can also specify the absolute path.
})

Comedy supports hot logging configuration change. This means that all changes you make to logger.json file are applied on-the-run without process restart.

You can also specify multiple configuration files in actor system configuration. In this case these configurations will be merged just like actors.json files do:

var actors = require('comedy');

//...

actors({
  // Files are specified in descending priority: first file has highest priority.
  loggerConfiguration: ['/etc/my-service/logger.json', 'conf/logger.json']
})

Resource Management

Actors are not always completely self-contained. It's not unusual for an actor to require some external re-usable resource to operate. A typical example of such resource is a connection to a database. Database connection (or connection pool) is a kind of a resource that you might want to be re-used by multiple actors within the same process, but also want it to be re-created for each forked process, spawned by a forked actor. Comedy lets you implement such behaviour with resources facility.

Here is an example, that uses MongoDB connection resource:

var actors = require('comedy');
var mongodb = require('mongodb');

/**
 * MongoDB connection resource definition.
 */
class MongoDbConnectionResource {
  /**
   * Resource initialization logic.
   *
   * @param system Actor system instance.
   * @returns {Promise} Initialization promise.
   */
  initialize(system) {
    this.log = system.getLog();
    this.log.info('Initializing MongoDB connection resource...');

    return mongodb.MongoClient.connect('mongodb://localhost:27017/test')
      .then(connection => {
        this.log.info('MongoDB connection resource successfully initialized.');

        this.connection = connection;
      })
  }

  /**
   * Resource destruction logic.
   *
   * @returns {Promise} Destruction promise.
   */
  destroy() {
    this.log.info('Destroying MongoDB connection resource...');

    return this.connection.close().then(() => {
      this.log.info('MongoDB connection resource successfully destroyed.');
    });
  }

  /**
   * This method returns the actual resource, that will be used by actors.
   *
   * @returns {*} MongoDB Database instance.
   */
  getResource() {
    return this.connection;
  }
}

/**
 * Test actor, that works with MongoDB connection resource.
 */
class TestActor {
  /**
   * @returns {[String]} Names of injected resources (taken from resource class name
   * or getName() method, if present).
   */
  static inject() {
    return ['MongoDbConnectionResource'];
  }

  /**
   * @param mongoDb MongoDB Database instance (injected by Comedy).
   */
  constructor(mongoDb) {
    this.mongoDb = mongoDb;
  }

  /**
   * Actor initialization logic.
   *
   * @param selfActor Self actor instance.
   */
  initialize(selfActor) {
    this.log = selfActor.getLog();
  }

  /**
   * Dumps a given collection to stdout.
   *
   * @param {String} name Collection name.
   * @returns {Promise} Operation promise.
   */
  dumpCollection(name) {
    return this.mongoDb.collection(name).find({}).toArray().then(result => {
      result.forEach((obj, idx) => {
        this.log.info(`Collection "${name}", item #${idx}: ${JSON.stringify(obj, null, 2)}`);
      });
    });
  }
}

// Create actor system with MongoDB connection resource defined.
var system = actors({
  resources: [MongoDbConnectionResource]
});

system
  .rootActor()
  // Create test actor.
  .then(rootActor => rootActor.createChild(TestActor))
  // Send a 'dumpCollection' message and wait for processing to finish.
  .then(testActor => testActor.sendAndReceive('dumpCollection', 'test'))
  // Destroy the system.
  .finally(() => system.destroy());

You can run this example on a machine with MongoDB installed. If you put some sample objects into a test database (collection test):

$ mongo test
> db.test.insertMany([{ name: 'Alice' }, { name: 'Bob' }, { name: 'Carol' }])

and run the above example, you will get the output that looks like this:

Tue Jan 24 2017 11:51:36 GMT+0300 (MSK) - info: Initializing MongoDB connection resource...
Tue Jan 24 2017 11:51:36 GMT+0300 (MSK) - info: MongoDB connection resource successfully initialized.
Tue Jan 24 2017 11:51:36 GMT+0300 (MSK) - info: InMemoryActor(58871598da402221604ed455, TestActor): Collection "test", item #0: {
  "_id": "58861b5072b7a3ff497763e4",
  "name": "Alice"
}
Tue Jan 24 2017 11:51:36 GMT+0300 (MSK) - info: InMemoryActor(58871598da402221604ed455, TestActor): Collection "test", item #1: {
  "_id": "58861b5072b7a3ff497763e5",
  "name": "Bob"
}
Tue Jan 24 2017 11:51:36 GMT+0300 (MSK) - info: InMemoryActor(58871598da402221604ed455, TestActor): Collection "test", item #2: {
  "_id": "58861b5072b7a3ff497763e6",
  "name": "Carol"
}
Tue Jan 24 2017 11:51:36 GMT+0300 (MSK) - info: Destroying MongoDB connection resource...
Tue Jan 24 2017 11:51:36 GMT+0300 (MSK) - info: MongoDB connection resource successfully destroyed.

You can see that MongoDB resource has been created before test actor runs it's logic and is then destroyed.

Like actors, resources have lifecycle. A resource is created and initialized once a first actor, that is dependent on this resource, is created. A resource dependency is declared within an actor definition by creating an inject() static method, that returns an array of names of resources, which actor requires. Each resource is then injected to an actor definition constructor parameter with corresponding index upon actor instance creation. A value to inject is taken from getResource() method of corresponding resource definition.

All created resources are destroyed during actor system destruction.

If none of the created actors needs a specific resource, it is never created. If we comment-out TestActor-related lines in our example, we will not see MongoDB resource creation and destruction messages - a connection to MongoDB won't be established.

// ...

system
  .rootActor()
  // Create test actor.
  // Commented-out: .then(rootActor => rootActor.createChild(TestActor))
  // Send a 'dumpCollection' message and wait for processing to finish.
  // Commented-out: .then(testActor => testActor.sendAndReceive('dumpCollection', 'test'))
  // Destroy the system.
  .finally(() => system.destroy());

No resource-related information should be present in log after running a modified example above.

On the other hand, if we created several actors requiring MongoDB resource within the same process, a resource instance will be created only once and will be re-used by all these actors. A modified version of our example with 2 test actors:

system
  .rootActor()
  // Create 2 test actors.
  .then(rootActor => Promise.all([rootActor.createChild(TestActor), rootActor.createChild(TestActor)]))
  // Send a 'dumpCollection' message and wait for processing to finish.
  .then(testActors => Promise.all([
    testActors[0].sendAndReceive('dumpCollection', 'test'),
    testActors[1].sendAndReceive('dumpCollection', 'test')
  ]))
  // Destroy the system.
  .finally(() => system.destroy());

will give the following output:

Tue Jan 24 2017 12:21:40 GMT+0300 (MSK) - info: Initializing MongoDB connection resource...
Tue Jan 24 2017 12:21:40 GMT+0300 (MSK) - info: MongoDB connection resource successfully initialized.
Tue Jan 24 2017 12:21:40 GMT+0300 (MSK) - info: InMemoryActor(58871ca4cd6d772a5a73ff39, TestActor): Collection "test", item #0: {
  "_id": "58861b5072b7a3ff497763e4",
  "name": "Alice"
}
Tue Jan 24 2017 12:21:40 GMT+0300 (MSK) - info: InMemoryActor(58871ca4cd6d772a5a73ff39, TestActor): Collection "test", item #1: {
  "_id": "58861b5072b7a3ff497763e5",
  "name": "Bob"
}
Tue Jan 24 2017 12:21:40 GMT+0300 (MSK) - info: InMemoryActor(58871ca4cd6d772a5a73ff39, TestActor): Collection "test", item #2: {
  "_id": "58861b5072b7a3ff497763e6",
  "name": "Carol"
}
Tue Jan 24 2017 12:21:40 GMT+0300 (MSK) - info: InMemoryActor(58871ca4cd6d772a5a73ff3a, TestActor): Collection "test", item #0: {
  "_id": "58861b5072b7a3ff497763e4",
  "name": "Alice"
}
Tue Jan 24 2017 12:21:40 GMT+0300 (MSK) - info: InMemoryActor(58871ca4cd6d772a5a73ff3a, TestActor): Collection "test", item #1: {
  "_id": "58861b5072b7a3ff497763e5",
  "name": "Bob"
}
Tue Jan 24 2017 12:21:40 GMT+0300 (MSK) - info: InMemoryActor(58871ca4cd6d772a5a73ff3a, TestActor): Collection "test", item #2: {
  "_id": "58861b5072b7a3ff497763e6",
  "name": "Carol"
}
Tue Jan 24 2017 12:21:40 GMT+0300 (MSK) - info: Destroying MongoDB connection resource...
Tue Jan 24 2017 12:21:40 GMT+0300 (MSK) - info: MongoDB connection resource successfully destroyed.

As you see, MongoDB connection resource was created only once.

One final experiment we will do is creating 2 forked actors. In this case, a new instance of resource will be created for each actor, because they run in separate processes. In the parent process, however, the MongoDB resource instance won't be created, because no actor in parent process needs it.

system
  .rootActor()
  // Create 2 forked test actors.
  .then(rootActor => rootActor.createChild(TestActor, { mode: 'forked', clusterSize: 2 }))
  // Send a 'dumpCollection' message and wait for processing to finish.
  .then(testActor => testActor.sendAndReceive('dumpCollection', 'test'))
  // Destroy the system.
  .finally(() => system.destroy());

For the above example we will need to slightly rework our MongoDbConnectionResource: because it is declared in the system by using class name, not resource path, it will be serialized and sent to forked process and then compiled there. Because MongoDbConnectionResource uses external variable, that is not serialized (mongodb), we will get compilation error. A recommended way to go here is to move resource definition to a separate file and the declare resource using a module path. But here we will modify our class to require mongodb package inside initialize() method:

class MongoDbConnectionResource {
  // ...
  
  initialize(system) {
    var mongodb = require('mongodb');

    this.log = system.getLog();
    this.log.info('Initializing MongoDB connection resource...');

    return mongodb.MongoClient.connect('mongodb://localhost:27017/test')
      .then(connection => {
        this.log.info('MongoDB connection resource successfully initialized.');

        this.connection = connection;
      })
  }
  
  // ...
}

Now, after running our modified example, we will get the following output:

Tue Jan 24 2017 12:29:28 GMT+0300 (MSK) - info: Initializing MongoDB connection resource...
Tue Jan 24 2017 12:29:28 GMT+0300 (MSK) - info: Initializing MongoDB connection resource...
Tue Jan 24 2017 12:29:28 GMT+0300 (MSK) - info: MongoDB connection resource successfully initialized.
Tue Jan 24 2017 12:29:28 GMT+0300 (MSK) - info: MongoDB connection resource successfully initialized.
Tue Jan 24 2017 12:29:28 GMT+0300 (MSK) - info: ForkedActorChild(58871e7872a8482cdc429be7, TestActor): Collection "test", item #0: {
  "_id": "58861b5072b7a3ff497763e4",
  "name": "Alice"
}
Tue Jan 24 2017 12:29:28 GMT+0300 (MSK) - info: ForkedActorChild(58871e7872a8482cdc429be7, TestActor): Collection "test", item #1: {
  "_id": "58861b5072b7a3ff497763e5",
  "name": "Bob"
}
Tue Jan 24 2017 12:29:28 GMT+0300 (MSK) - info: ForkedActorChild(58871e7872a8482cdc429be7, TestActor): Collection "test", item #2: {
  "_id": "58861b5072b7a3ff497763e6",
  "name": "Carol"
}
Tue Jan 24 2017 12:29:28 GMT+0300 (MSK) - info: Actor process exited, actor ForkedActorParent(58871e7872a8482cdc429be7, TestActor)
Tue Jan 24 2017 12:29:28 GMT+0300 (MSK) - info: Actor process exited, actor ForkedActorParent(58871e78a30d3b2ce25f3727, TestActor)

As you see, 2 instances of MongoDB connection resource are created, one for each forked actor. A database collection is dumped once by a first actor that receives the dumpCollection message (default round-robin balancing strategy).

Actor Metrics

When an actor is up and running, it can be configured to output a number of useful metrics for monitoring.

var actors = require('comedy');

/**
 * Sample actor.
 */
class MyActor {
  // ...Some useful code.

  metrics() {
    return {
      requestsPerSecond: Math.floor(Math.random() * 100) // Some real value should be here.
    };
  }
}

actors()
  .rootActor() // Get a root actor reference.
  .then(rootActor => rootActor.createChild(MyActor)) // Create a child actor.
  .then(myActor => myActor.metrics()) // Query actor metrics.
  .then(metrics => {
    console.log('Actor metrics:', metrics); // Output actor metrics.
  });

An example above will output something like:

Actor metrics: { requestPerSecond: 47 }

What we did in the example above is we've defined a metrics method in MyActor actor definition class and then called metrics method on MyActor actor instance. This method returns a promise with actor metrics object, containing the metrics we've returned from metrics method.

"Wait!" - you'd say - "Why are we using a special metrics method for getting these metrics? Why don't we just send a 'metrics' message? Won't the result be the same?"

In this case - yes, the result will be exactly the same. But a metrics method has one additional useful property, for which you'll definitely want to use it: it automatically collects metrics from all child actors recursively as well.

Consider another example:

var actors = require('comedy');

/**
 * Sample actor.
 */
class MyActor {
  initialize(selfActor) {
    return selfActor.createChild(MyChildActor); // Create a child actor.
  }

  // ...Some useful code.

  metrics() {
    return {
      requestsPerSecond: Math.floor(Math.random() * 100) // Some real value should be here.
    };
  }
}

/**
 * Sample child actor.
 */
class MyChildActor {
  // ...Some useful code.

  metrics() {
    return {
      ignoredMessages: 0
    };
  }
}

actors()
  .rootActor() // Get a root actor reference.
  .then(rootActor => rootActor.createChild(MyActor)) // Create a child actor.
  .then(myActor => myActor.metrics()) // Query actor metrics.
  .then(metrics => {
    console.log('Actor metrics:', metrics); // Output actor metrics.
  });

This example's output will be similar to:

Actor metrics: { requestsPerSecond: 68, MyChildActor: { ignoredMessages: 0 } }

We've received metrics for MyActor as well as it's child actor, though we didn't change our calling code. When using metrics method, metric aggregation happens automatically, so each actor only needs to output it's own metrics from metrics message handler.

Advanced Features

In this section we cover advanced Comedy features, that are not absolutely necessary to use the framework, but can be useful in some particular cases.

Dropping messages on overload

In high-load applications, a usual thing is when the amount of incoming messages is greater than the one your actor can handle. In this case the message queue grows, the event loop lag grows as well, and eventually your server process crashes with OutOfMemory error.

You may configure an actor to drop incoming messages if the system is overloaded, i.e. when an event loop lag exceeds a certain threshold. This is useful if you cannot predict the load in advance and thus cannot initially pick the right clusterSize for an actor.

A dropMessagesOnOverload actor parameter set to true (can be set both programmatically and via actors.json) enables an actor to drop messages when an event loop lag is greater than a busyLagLimit actor system option value (defaults to 3 seconds). When the event loop lag decreases and becomes lower than busyLagLimit, an actor resumes normal message handling.

You normally only want to enable dropMessagesOnOverload for actors that receive messages you may allow to loose. An example of such messages are monitoring messages, informational or statistical messages or messages that regularly repeat.

Custom balancers

In Scaling to multiple instances section we covered clustered actors with two balancing strategies: "round-robin" and "random". Are there any other balancing strategies possible? The answer is: yes!

You can define your own custom balancers for clustered actors. Here we explain how it is done.

Firstly, you need to implement your custom balancer, which is just a regular actor definition with additional contract: you need to define a "forward" message handler and, optionally, a "clusterChanged" message handler.

Below is a custom balancer example:

/**
 * Custom balancer with simplistic sharding.
 */
class CustomBalancer {
  clusterChanged(actors) {
    var _ = require('underscore');

    this.table = _.chain(actors).map(actor => actor.getId()).sortBy().value();
  }

  forward(topic, msg) {
    var tableIdx = msg.shard % this.table.length;

    return this.table[tableIdx];
  }
}

A "forward" message handler performs actual balancing. It receives a message topic as a first argument and a message body as a second argument, and should return an ID of a child actor, to which a message should be forwarded.

A "clusterChanged" message handler is called once before a first message is received by balancer and then every time there is a change in a cluster (one or more actors went down or up, actors are added or removed etc.). The handler receives an array of online actors, which are capable of receiving a forwarded message.

The CustomBalancer actor above does a simple (maybe, too simple) shard-based forwarding. In "clusterChanged" message handler, it saves actor IDs into a sorted array. Then, in "forward" message handler it picks the appropriate actor ID based on a "shard" field of a message. If "shard" field is absent in a message, the "forward" handler will return undefined, which will result in and error ("No actor to send message to.") returned to a sender of a message.

After we have defined our custom balancer, we should feed it to the actor system through initialization parameters:

var system = actors({ balancers: [CustomBalancer] });

We could also save the balancer to a separate file and specify a path to this file in actor system initialization parameters:

var system = actors({ balancers: ['/some-project-folder/balancers/custom-balancer'] });

After that, we just specify the balancer's name in the "balancer" parameter of our clustered actor, either programmatically or via actors.json file:

{
  "MyClusteredActor": {
    "mode": "forked",
    "clusterSize": 3,
    "balancer": "CustomBalancer"
  }
}

In the example above, messages to MyClusteredActor will be balanced with our CustomBalancer.

System bus

There might be cases when you need to broadcast a message to all actors in your system. For example, you may need to propagate system-wide configuration. For such cases, a system bus is implemented.

System bus lets you broadcast a message to every actor that has subscribed to a particular topic.

To receive a system-wide message, an actor needs to subscribe to it. The subscription example is below:

class MyActor {
  initialize(selfActor) {
    // Subscribe to 'system-configuration-changed' topic.
    selfActor.getBus().on('system-configuration-changed', newConfig => {
      console.log('New system configuration:', newConfig);

      // Do something useful with new configuration...
    });
  }
}

After having your actors subscribed, you can broadcast a system-wide message from any place of your code where ActorSystem is accessible and have it received by subscribed actors, even if they are in "forked" or "remote" mode:

system.getBus().emit('system-configuration-changed', { foo: 'bar' });

Note the following:

  1. The level of actor hierarchy, from which you emit messages into the bus, does not matter - the messages are being broadcast to all subscribed actors in all processes in the system.
  2. If you subscribe to system bus messages in a clustered actor, all instances of your clustered actor will receive a message, no balancing will occur.
  3. You can subscribe to system bus messages in actor resources as well (and also emit messages from resources).

Do not overuse system bus messages! This feature is intended only for rare, occasional messages that potentially impact a large part of your system. For normal workload you better use direct actor-to-actor messaging and hierarchical work distribution.

Upcoming Features

There is a number of features planned for future Comedy releases. The below list is not a roadmap, but rather a vision of what would be needed in nearest time.

Optimized message serialization

Currently Comedy serializes messages as plain JSON. This serialization method certainly won't give the best throughput. In future versions Comedy is likely to switch to a binary message format. Candidates are:

  • BSON
  • PSON
  • Smilie

There is also an option to try message compression like Snappy or mere GZip. These solutions will be tested on existing benchmarks. There will also be an option for pluggable user-defined serialization.

Hot code deployment

Currently, when you want to run an actor that has external module dependencies in remote mode, you need to ensure the dependent modules are installed on a remote machine.

It would be more convenient if Comedy deploys these modules automatically as a part of actor initialization process.

Automatic actor clustering according to load

You may not know your load in advance. Manually changing cluster size for clustered actors as load changes is is tedious and inconvenient. Comedy could instead automatically change cluster size according to, say, actor metrics (a function for calculating cluster size from metrics can be specified in parameters). Thus, when your load increases, Comedy could automatically spawn additional actors to handle load, and when load reduces - destroy unneeded actors.

Breaking Changes in v 2.0

Comedy 2.0 brings a breaking change to it's interface. This change is described below.

No external actor child creation

In Comedy 2.0 there is no longer a possibility to create a child for an actor from outside of an actor. The following code will no longer work:

async initialize(selfActor) {
  // OK to create immediate child with selfActor reference.
  let childActor = await selfActor.createChild(ChildActorDefinition);

  // Will NOT work! The state of childActor is private and cannot be manipulated from outside!
  let grandChildActor = await childActor.createChild(GrandChildActorDefinition);
}

In Comedy 2.0 you can only create child actors inside an actor with selfActor reference. The only exception is a root actor, which still has createChild() method publicly available:

let system = actors();
let rootActor = await system.rootActor();
let level1Child = await rootActor.createChild(SomeDef); // OK.
let level2ChildNok = await level1Child.createChild(SomeOtherDef); // Not OK!

await rootActor.createChild({
  initialize: async function(selfActor) {
    this.level2ChildOk = await selfActor.createChild(SomeOtherDef); // OK.
  }
});

About

Comedy is developed as a part of SAYMON project and is actively used in all SAYMON production installations.

Comedy was initially inspired by Drama framework.

comedy's People

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

comedy's Issues

Vulnerability updates: underscore and winston

Description

From the looks of this config

- npm i npm@latest -g

The two pr's #75 and #74 are blocked because a configuration to install the latest version of npm, and the latest npm doesn't run with the node 10 version in the CI checks

from pr 75's checks https://ci.appveyor.com/project/weekens/comedy/builds/43166798
ERROR: npm is known not to run on Node.js v10.24.1

The last time this seemed to pass was in PR #73 and it ran with npm version 7.20.5
Can I create a PR to change the line above to npm i [email protected] -g

Sending the message 0 produces undefined for forked and threaded actors.

// the referenced actor file:
export default class Printer {
    printNumber(n: number) {
        console.log(n);
    }
}

// main file:
    const root = await system.rootActor();

    async function doTest(mode: "in-memory" | "forked" | "threaded") {
        const testActor = await root.createChild("/dist/src/printer", {
            mode
        });
        console.log("Output for: " + mode);
        await testActor.sendAndReceive("printNumber", 0);
        await testActor.destroy();
    }

    await doTest("in-memory");
    await doTest("forked");
    await doTest("threaded");

This prints:
Output for: in-memory
0
Output for: forked
undefined
Output for: threaded
undefined

I'd expect it to print 0 for all 3 cases, it seems there must be some sort of if (message) ... in the code somewhere that causes this.

Kafka transport

For now Comedy uses simple point-to-point channels for message exchange. There should be an option to use Kafka message broker as an underlying transport for more advanced and robust messaging.

hot config reloads do not consider actor custom parameters

  1. Initialize a forked (maybe also in-memory?) actor with custom parameters. I primarily use this to pass in some other ActorRefs.
  2. The actor reads from getCustomParameters() in the initialize method, everything works.
  3. Change the configuration of that actor.
  4. The actor gets destroyed and recreated, but getCustomParameters() now is undefined. debug mode shows the custom parameters are just gone from the create-actor message used internally.

I guess the hot reloading code is just missing this behavior entirely?

Hot configuration change

Users should be able to change actor configuration in actors.json without application restart. The configuration should be applied on-the-fly.

TypeError: Cannot read property 'send' of undefined

When i use forked actors, i got this error (and don't know why and where)

Error occurring at /var/www/scripts/node_modules/comedy/lib/forked-actor.js:476:18

Full error:

TypeError: Cannot read property 'send' of undefined at /var/www/scripts/node_modules/comedy/lib/forked-actor.js:476:18 at Promise._execute (/var/www/scripts/node_modules/comedy/node_modules/bluebird/js/release/debuggability.js:299:9) at Promise._resolveFromExecutor (/var/www/scripts/node_modules/comedy/node_modules/bluebird/js/release/promise.js:481:18) at new Promise (/var/www/scripts/node_modules/comedy/node_modules/bluebird/js/release/promise.js:77:14) at ForkedActorParent._send0 (/var/www/scripts/node_modules/comedy/lib/forked-actor.js:421:12) at ForkedActorParent.transmitBusMessage (/var/www/scripts/node_modules/comedy/lib/forked-actor.js:108:10) at /var/www/scripts/node_modules/comedy/lib/system-bus.js:82:19 at Set.forEach (<anonymous>) at SystemBus._broadcastToForkedRecipients (/var/www/scripts/node_modules/comedy/lib/system-bus.js:80:21) at SystemBus.emitFromActor (/var/www/scripts/node_modules/comedy/lib/system-bus.js:44:10) at ChildProcess.<anonymous> (/var/www/scripts/node_modules/comedy/lib/forked-actor.js:274:25) at ChildProcess.emit (events.js:314:20) at emit (internal/child_process.js:906:12) at processTicksAndRejections (internal/process/task_queues.js:81:21)

My architicture:
Forked actor running children forked actors (4), and each child running own one threaded and one forked actors. I am using bus for messaging between them.

My solution:
If i change this chain Forked -> Forked -> Forked to this Forked -> Forked -> Threaded it will be ok.
But i need only Forked -> Forked -> Forked

Discussion: drop configuration loading

This thread is about the discussion around the topic so it should stay open for a while.

The idea is that the library shouldn't actually be doing the configuration because it violates the primary concerns of comedy.
As most advanced use cases would just use their own config loading.

However, hot-loading is very useful.

So I see a number of options:

1. make hot-loading configurable: by providing a custom handler and an array of files to watch:

const CONFIG_FILES = ['file1.json', 'file2.yaml']

comedy.setConfigLoader(CONFIG_FILES, (changedfile) => { 
    // custom parsing and handling
    // then return the updated config in the JSON format currently read directly from files
    return {};
})

2. rip out hot-loader functionality and just provide a hot-update function, maybe making another library

import hotloader from `@comedy/hotloader`

hotloader.watch('file1.json', () => {
    // callback fires on config changes
    // custom parsing and handling
    const config = {};
    // then return the updated config as a JSON object in the format currently read directly from files
    comedy.updateConfig(config);
}

hotloader.watch('file2.yaml', () => {
    // callback fires on config changes
    // custom parsing and handling
    const config = {};
    // then call update with updated config as a JSON object in the format currently read directly from files
    comedy.updateConfig(config);
}

there are likely other options, the biggest issue is backwards compatibility,
I lean towards option 2 because it keeps comedy focussed on what it should, which is the actor system, and you can
keep BC if you detect that the @comedy/hotloader module is installed and the config option is set as a path to a json file and not a json object (which it should support as well)

Ability to Address Actors by Id

Is there any support for being able to assign identifiers to actors upon creation such that they could be addressed generically? All the examples I've seen so far seem to rely on having an actual reference to the actor in memory which makes it harder to support messaging between different actors.

Remote actor deployment via SSH

When launching remote actor for testing purposes, it's often convenient to specify SSH credentials rather than deploying comedy-node on remote machine by hand. Comedy should support this.

Possibility of scaling to other machines rather than just processes

I just found out about this project, and I find the scaling capabilities particularly interesting.

Just out of curiosity, have you considered the possibility of scaling out to other machines rather than just other processes?

It would be really interesting it we were able to configure an entire cluster, allocating actors to different machines, but using the same interface to communicate between them.

What would be the challenges in making something like this happen?

State of an actor passed into a forked actor does not get updated.

  1. Create a local in-memory actor L
  2. Pass it to a forked actor F
  3. Destroy the local in-memory actor L
  4. The forked actor won't recognize the new state of actor L.

Example:

    const rt = await system.rootActor();
    // local actor L
    const localPrinter = await rt.createChild({
        pipe(data: any) {
            console.log("I got data piped!", data);
        }
    });

    // forked actor F
    const forkedActor = await rt.createChild("/dist/src/dbg", {
        mode: "forked"
    });
    /**
        giveRef(ref: ActorRef) {
        setInterval(() => {
            console.log("The actor state of my ref is: " + ref.getState());
            if (ref.getState() === "ready") {
                ref.send("pipe", Date.now());
            }
        }, 1000);
    }
     */
    forkedActor.send("giveRef", localPrinter);

    /**
     * Prints:
     * The actor state of my ref is: ready
        I got data piped! 1607799890757
        The actor state of my ref is: ready
        I got data piped! 1607799891754
        The actor state of my ref is: ready
        I got data piped! 1607799892754
        The actor state of my ref is: ready
        I got data piped! 1607799893755
        The actor state of my ref is: ready
     */
    await new Promise<void>(resolve => {
        setTimeout(() => {
            resolve();
        }, 5000);
    });

    // destroy local actor
    await localPrinter.destroy();

    // now the prints continue:
    /**
The actor state of my ref is: ready
Sat Dec 12 2020 20:04:55 GMT+0100 (Central European Standard Time) - warn: ForkedActor(5fd51451843499b4f6d3a668): No pending promise for "actor-response": 
{
  type: 'actor-response',
  id: 6,
  body: {
    error: 'destroy() has been called for this actor, no further interaction possible'
  },
  actorId: '5fd51451843499b4f6d3a668'
}
     */
    await new Promise<void>(resolve => {
        setTimeout(() => {
            resolve();
        }, 5000);
    });

It should probably not return "ready" for getState() on ref in the forked actor, after the referenced actor has been destroyed.
If I use sendAndReceive("pipe", ...) instead of just send, that one throws an exception saying the actor has been destroyed, but the state of the actor in the forked actor still reports "ready".
send on the other hand just fails silently and causes those warning prints to show up a lot.

Inter-children communication

Since the root actor creates all the children sequentially, I don't see how different kind of children can exchange messages without passing by root.

For example, I need my Player actor to send data to Croupier.
The two choices I see are:

  • create the two actors simultaneously and add a setter:
class Player {
    constructor() {
        this.currentPile = []
        this.master = null
    }

    start(pile) {
        this.currentPile = pile;
        this.currentPile.forEach((x) => this.master.send('add', x))
    }

    setMaster(master) {
        this.master = master
    }
}

class Croupier {
    constructor() {
        this.pile = []
    }

    add(value) {
        this.pile.push(value);
    }

    print(to) {
        console.log(this.pile);
    }
}

var actorSystem = actors();

actorSystem
  .rootActor() // Get a root actor reference.
  .then(function(rootActor) {
    Promise.all([rootActor.createChild(Player, {mode: 'forked', clusterSize: 3}), rootActor.createChild(Croupier)])
    .then(children => {
        var player = children[0];
        var croupier = children[1];
        player.send('setMaster', croupier);
        player.send('start', [1,2,3]);
        setInterval(() => croupier.send('print'), 1000)
      })
  })
  • create actor one after the other and set the master in the constructor of the first actor.

Anyhow, when running the previous code, there is an error (again only when forked):

Unhandled rejection TypeError: Converting circular structure to JSON
    at Object.stringify (native)
    at ChildProcess.target._send (internal/child_process.js:626:23)
    at ChildProcess.target.send (internal/child_process.js:538:19)
    at P (/Users/vallettea/Desktop/actors/node_modules/comedy/lib/forked-actor.js:322:16)
    at Promise._execute (/Users/vallettea/Desktop/actors/node_modules/bluebird/js/release/debuggability.js:299:9)
    at Promise._resolveFromExecutor (/Users/vallettea/Desktop/actors/node_modules/bluebird/js/release/promise.js:481:18)
    at new Promise (/Users/vallettea/Desktop/actors/node_modules/bluebird/js/release/promise.js:77:14)
    at ForkedActorParent._send0 (/Users/vallettea/Desktop/actors/node_modules/comedy/lib/forked-actor.js:298:12)
    at ForkedActorParent._sendActorMessage (/Users/vallettea/Desktop/actors/node_modules/comedy/lib/forked-actor.js:263:17)
    at ForkedActorParent.send0 (/Users/vallettea/Desktop/actors/node_modules/comedy/lib/forked-actor.js:183:17)
    at ForkedActorParent.send (/Users/vallettea/Desktop/actors/node_modules/comedy/lib/actor.js:161:19)
    at ForkedActorStub.send (/Users/vallettea/Desktop/actors/node_modules/comedy/lib/forked-actor-stub.js:70:23)
    at currentChildPromise.then.child (/Users/vallettea/Desktop/actors/node_modules/comedy/lib/standard/round-robin-balancer-actor.js:119:64)
    at tryCatcher (/Users/vallettea/Desktop/actors/node_modules/bluebird/js/release/util.js:16:23)
    at Promise._settlePromiseFromHandler (/Users/vallettea/Desktop/actors/node_modules/bluebird/js/release/promise.js:510:31)
    at Promise._settlePromise (/Users/vallettea/Desktop/actors/node_modules/bluebird/js/release/promise.js:567:18)

What I am doing wrong ?

Dependencies inside Resources in forked modules

Hello!

I'm starting a new project with TypeScript and currently experimenting with Comedy as a solution for scaling the application.

I ran into an issue with the way Resources are serialized and sent to the forked process which is causing a lot of headaches and seems like a big limitation (if I'm understanding this correctly).

The original Resource definition is as follows:

import { ActorSystem, ResourceDefinition } from 'comedy';
import { getResource } from '../helpers/test';
import { ComedyResource } from '../decorators/ComedyResource';

@ComedyResource('TestResource')
export default class TestResource implements ResourceDefinition<string> {
	destroy(): Promise<void> | void {
		// nothing to do here
		return undefined;
	}

	getResource(): string {
		// return from an imported function as a test
		return getResource();
	}

	initialize(system: ActorSystem): Promise<void> | void {
		// nothing to do here
		return undefined;
	}
}

But what is sent to the child process (in the create-actor message) is:

class TestResource {
    destroy() {
        // nothing to do here
        return undefined;
    }
    getResource() {
        // return from an imported function as a test
        return test_1.getResource();
    }
    initialize(system) {
        // nothing to do here
        return undefined;
    }
}; TestResource;

As you can see, the imports are all missing and there is no way this can work.

@Zephyrrus noticed that you can use require() inside the definition and to import things, but they are imported from the wrong working directory and therefore don't resolve properly.

As a workaround I considered creating dummy "shells" that would dynamically load the correct file (from disk) with require(), but that sounds very cumbersome to maintain.

Is there any solution to this? Am I missing something?

How to ensure message belonging to a single group is processed sequentially?

Here's what I'm trying to achieve:

  • A cluster of actors to process commands (as in CQRS commands)
  • There will be 1 command router (that's where singleton question came from).
  • All commands will go to command router actor.
  • Each command is then distributed to command handler actors (from cluster mentioned in step 1).

My requirement is: command belonging to a single aggregate must be handled sequentially, although command belonging to different aggregates can be running in parallel.

Is it possible to setup the cluster of actors in such a way that when a command of an aggregate is being handled, other commands belonging to same aggregate will be queued until previous command is completed; while if command belonging to other aggregate comes in then it will be handled?

I'm envisioning solution like this:

  • divide aggregate (using its Id) into N buckets, where N is number of actors in cluster. This way all commands belonging to an aggregate will be routed to only one actor, which will then hopefully be easy to process sequentially.

But this may have problem of single actor being more loaded if aggregateId belonging to bucket of that particular actor comes in frequently

Location Transparency?

In frameworks like Akka, the concept of Location Transparency is always stressed a lot. It allows moving actors to another node, which in turn allows for dynamically growing and shrinking a cluster. Also live software updating of framework or application would require this.

Does the comedy team envision implementing something like this? Or is comedy built on a different vision that does not require these concepts?

Suggestion: drop bluebird and leverage async/await

This is probably the least important issue but figured we can discuss it anyway.

All the current stable versions of node and browsers support promises and bluebird is not a dependency that libraries should ship with if it can be avoided.

the codebase itself could really benefit from cleanup and a switch to native promises and async/await.

I don't see this adding much value other than decreasing the likelihood of silly errors like #62 and general code legibility (promises with callbacks tend to be less legible in most circumstances than the equivalent async/await).

P.S. I am aware that not ALL promise usages should be transferred to async/await, especially for concurrency but most of the functions tend to be step by step inside anyway.

Random balancer for clustered actors

For now, Comedy only supports round-robin balancing. We should also add random balancer as an option (configured via balancer actor parameter).

Size of the mailbox

How can I know if an actor has message in its mailbox that should be processed ?

Backpressure

Implement an actor mailbox size limit capability. This way, a user will be able to avoid overloading actors with too many messages.

DEPENDENCY: Winston failing on node 14

when running the basic getting started example the following error appears:

(node:290995) Warning: Accessing non-existent property 'padLevels' of module exports inside circular dependency

followed by a whole trace if you add the --trace-warnings flag.
This was fixed in Winston 2.4.5 but they do recommend moving to Winston 3.

That said, shipping with a logger is crossing concerns...
Libraries shouldn't have baked in loggers, but they can have interface to configure your own logger. for example you can pass a log function handler

import comedy from 'comedy';

comedy.setLogger((category, level, ...msg) => { /*handle however I want*/ });
// or 
comedy.setLoggerFactory((category) => (level, ...msg) => { /* create a logger for each category*/ });

// then the rest of the usage stays exactly the same.

ReadOnly shared object between forked Actors

Hello,
thanks for this awesome library.
I would like to know how to share an object between forked actors.

Here is an example:

var pile  = []

class Player {
  count(to) {
    console.log(pile.length);
  }
}

class Croupier {
  add(to) {
    pile.push(9);
  }
}

var actorSystem = actors();

actorSystem
  .rootActor()
  .then(function(rootActor) {
  	Promise.all([rootActor.createChild(Player, {mode: 'forked', clusterSize: 3}), rootActor.createChild(Croupier)])
  	.then(children => {
  		var player = children[0];
  		var croupier = children[1];
  		setInterval(() => player.send('count'), 10);
  		setInterval(() => croupier.send('add'), 1000)
	  })
  })

This example works when Player actors are not forked but as soon as they are, they can't access the list pile anymore. Player should only be able to read the object, only Croupier car modify the object.

The only solution I see is that each Player gets a copy of pile in a message, but I'm scared it will have big memory footprint.

Any help appreciated.

ActorRef passing is somewhat limited.

Currently ActorRefs can be passed to Actor calls, but when using forked or threaded actors, this only is correctly handled for a few cases:

  • As a method parameter value
  • As a value in customParameters.

Nesting is not supported at all, so passing a method parameter of types {a: ActorRef, b: ActorRef} fails.
Additionally you can also not return ActorRefs from actor methods.
There are workaround, but they are cumbersome.
I think bus messages also cannot transport ActorRefs.

It would make some scenarios easier to handle to add handling in more cases.

Can we define root actor action

Since all messages are going through the root actor, he may as well achieve some work. Can I define messages and associated function for the root actor ?

Actor State Names

It seems the actor state names are not documented.
They appear to be "new", "ready" and maybe a few more such as "destroying" or "destroyed".
And "crashed"?

They should be documented.

Actor initialization with parameters

[I've been working a couple of days with your library and I wanted to thanks you because I really enjoy it.]

Coming back to my question, how can I initialize an actor with some parameters ?
When an actor is forked, it doesn't have access to the scope.

Promise.defer is deprecated warning

Problem:
While running an app based on comedy framework we're getting an error:

Warning: Promise.defer is deprecated and will be removed in a future version. Use new Promise instead.
  at P (/home/node/work/node_modules/comedy/lib/forked-actor.js:433:25)
  From previous event:
  at ForkedActorChild._send0 (/home/node/work/node_modules/comedy/lib/forked-actor.js:421:12)
  at ForkedActorChild._pingParent (/home/node/work/node_modules/comedy/lib/forked-actor-child.js:105:17)
  at Timeout.setInterval [as _onTimeout] (/home/node/work/node_modules/comedy/lib/forked-actor-child.js:44:16)
  at ontimeout (timers.js:436:11)
  at tryOnTimeout (timers.js:300:5)
  at listOnTimeout (timers.js:263:5)
  at Timer.processTimers (timers.js:223:10)

Source:
/lib/forked-actor.js, method _send0 (https://github.com/untu/comedy/blob/master/lib/forked-actor.js#L433)

I think that the main issue here is how do you use Bluebird library. According to https://github.com/petkaantonov/bluebird/wiki/Promise-anti-patterns#the-deferred-anti-pattern you should not use defer and pending methods on P object.

Expected behavior:
No warning in a console.

tsc compile error

When I run tsc, the compiler tells:

xxx/AppData/Roaming/npm/node_modules/typescript/lib/lib.es2015.iterable.d.ts:41:6 - error TS2300: Duplicate identifier 'IteratorResult'.

41 type IteratorResult<T, TReturn = any> = IteratorYieldResult | IteratorReturnResult;
~~~~~~~~~~~~~~

node_modules/@types/node/index.d.ts:165:11
165 interface IteratorResult { }
~~~~~~~~~~~~~~
'IteratorResult' was also declared here.

And my tsconfig.json is:

{
"exclude": [
"node_modules/@types/node/index.d.ts"
],
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"lib": ["es6"],
"strict": true,
"esModuleInterop": true
}
}

Typescript version is 3.7.2

The "exclude" in tsconfig.json makes no difference.

Switchable Logger

Is there an easy way to switch out the logger? The application I'm porting over is using bunyan and I'd prefer to not have to redo it through out.

Singletons

How can I ensure that only one actor is created in whole actor system cluster? Also somewhat relates to #17

"Error: externalFunction is not defined" in forked nodes

I'm getting an error when I try to reference external symbols in forked agents.
Am I doing something wrong?

My code:

const actors = require('comedy');

function externalFunction() {
    console.log("CIAO");
}

/**
 * Actor definition class.
 */
let MyActor = {
    sayHello: function () {
        externalFunction();
    }
};

async function bootstrap() {
    let actorSystem = actors();
    try {
        let root = await actorSystem.rootActor();
        console.log("== Creating in memory actor ==");
        await (await root.createChild(MyActor, {mode: 'in-memory'})).sendAndReceive(`sayHello`); // ok
        console.log("== Creating forked actor ==");
        await (await root.createChild(MyActor, {mode: 'forked'})).sendAndReceive(`sayHello`); // not ok
    } catch (e) {
        console.log("error: ", e);
        await actorSystem.destroy();
    }
}

bootstrap();

My expected result:

== Creating in memory actor ==
CIAO
== Creating forked actor ==
CIAO

My actual result:

Tue Jun 11 2019 09:42:40 GMT+0200 (GMT+02:00) - info: Didn't find (or couldn't load) default configuration file C:\comedy-sandbox/actors.json.
Tue Jun 11 2019 09:42:40 GMT+0200 (GMT+02:00) - info: Resulting actor configuration: {}
== Creating in memory actor ==
CIAO
== Creating forked actor ==
error:  Error: externalFunction is not defined
    at ChildProcess.bus.on (C:\comedy-sandbox\node_modules\comedy\lib\forked-actor.js:224:32)
    at ChildProcess.emit (events.js:189:13)
    at emit (internal/child_process.js:820:12)
    at process._tickCallback (internal/process/next_tick.js:63:19)
Tue Jun 11 2019 09:42:41 GMT+0200 (GMT+02:00) - info: Killing forked system process.
Tue Jun 11 2019 09:42:41 GMT+0200 (GMT+02:00) - info: ForkedActorParent(5cff5b70806f34235cfa76d1, Object): Actor process exited, actor ForkedActorParent(5cff5b70806f34235cfa76d1, Object)

My Environment:

OS: Windows 10
node --version: v10.15.3
npm --version: 6.9.0
npm info comedy version: 2.1.0

Test dependencies in package

Next dependencies better to move from "dependencies" to "devDependencies"

    "@types/chai": "4.1.4",
    "@types/mocha": "5.2.2",

As a result of "required" dependencies, it can create conflicts with other test frameworks used in the project who consumes "comedy".

Cluster message failover

When a message is sent through a balancer actor to a cluster actor that crashes, the balancer should attempt to re-route that message to another actor in the cluster. The number of failover attempts should be configurable.

How to run actor compiled in ES6 as forked mode?

Hi guys,

I'm trying to run an actor as forked mode and this actor class is compiled with babel, but when runs the actor I got the following error:

ReferenceError: _classCallCheck is not defined

What am I missing?
Thanks,

Automatic scaling

Implement automatic actor scaling up and down according to the load.

Remote actor log aggregation

For development and debugging purposes it may be useful to transfer logs from remote actor to a parent actor to see the full picture of an actor system in the root node log.

comedy in browser

Getting comedy to work in the browser would be an interesting development.
removing dependencies on node and also converting the project to use ES modules.

How to prevent it outputting to console the initial setup log

When my actor system using Comedy runs it says on the console.

Mon Sep 10 2018 19:28:17 GMT+0800 (China Standard Time) - info: Loaded default actor configuration file: C:\home\projects\ContractPen\contractpen_node_client/actors.json
Mon Sep 10 2018 19:28:17 GMT+0800 (China Standard Time) - info: Resulting actor configuration: {}

How to prevent it from saying this upon startup on console? I don't want any output to console to occur.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.