0.0.25 • Published 2 years ago

@k0r0pt/jsam v0.0.25

Weekly downloads
-
License
Apache-2.0
Repository
github
Last release
2 years ago

JsAM - The JavaScript Actor Model Framework

JsAM

Build Status Version Monthly Downloads

Wut?

JsAM is a powerful actor model framework.

Why?

As of the writing of this framework, while in the JVM world, we have the all powerful Akka Framework, we have limited options with limited features in the native Javascript sphere.

Of that lack, this framework was born.

Features

JsAM currently has these features:

  • Clustering - through Leader Elections
  • Stateful Actors
  • Singleton Actors
  • Monitoring endpoints
  • Actor Communications through gRPC
  • Actor Respawning - In case nodes go down
  • Cluster Rebalancing - In case nodes are added or removed
  • Kubernetes API support - So that we can deploy to Kubernetes Clusters
  • Caching - For when the same big message needs to be sent to many actors. This is helpful when number of nodes is far lesser than the number of actors the message needs to be sent to, thereby saving time spent on network transfers.
  • Ask Timeouts - Optional timeouts on ask operations can be specified with enviornment variable JSAM_ASK_TIMEOUT or as a parameter in the ask function.
  • Graceful shutdowns - When receiving SIGTERM or SIGINT, nodes that go down will tell the leader nodes (or the next in line if the leader itself is going down) about all the actors and their states that will go down with them. The leader (or the new leader) will then recreate these actors along with their states.

Planned features

How?

Install

Here's how to include this in your project:

npm install @k0r0pt/jsam

Importing the necessary classes

import { ActorSystem, ActorBehavior } from '@k0r0pt/jsam';
import nodeUtil from 'util';

or if you're on a non-ES6 project,

const { ActorSystem, ActorBehavior } = require('@k0r0pt/jsam');

Creating the Actor System

The first step is to create the Actor System. Here's how:

let actorSystem = await nodeUtil.promisify(ActorSystem.getActorSystem).bind(ActorSystem)('MyActorSystem', port, config);

The ActorSystem not only sets up the ActorSystem, it also sets up a handler for uncaughtExceptions, which logs the error and then calls process.exit(255). If you have your own process.on('uncaughtException', ... handler, we want the ActorSystem's handler to not exit the process and it should be the responsibility of the host program to do the exiting. In this case, we want the ActorySystem to know that it should not exit the process.

let actorSystem = await nodeUtil.promisify(ActorSystem.getActorSystem).bind(ActorSystem)('MyActorSystem', port, config, { doNotExitProcess: true });

The ActorSystem also has an option to not configure its log4js. This option will come in handy when you have your own log4js configuration.

let actorSystem = await nodeUtil.promisify(ActorSystem.getActorSystem).bind(ActorSystem)('MyActorSystem', port, config, { skipLog4jsConfiguration: true });

And it also has an option to pass Middlewares (Filters). An example with middlewares can be found in the Traceability With Middleware Example.

N.B. In these examples above, we're using node's util to promisify our callback function, but it can be called using the callback pattern too.

Creating the Root Actor

The Root Actor is the parent of all actors in an Actor System. This is the first thing that needs to be done as soon as the Actor System is created. Point to note, this is has a callback function which will be called once the Root Actor is created.

actorSystem.rootActor((err, rootActor) => {
  if (err) {
    console.log('Error while trying to create the root actor', err);
    return;
  }
  console.log('Root Actor created', rootActor);

  // Rest of your code
  .
  .
  .
});

N.B. In this example, we're using the callback pattern, but using node's util, we can do this in the promise pattern too.

Creating the Actual Actors

Once the root actor is created, we can ask it to spawn children which will have behavior defining their behaviors. The Behaviors need to be specified from the root of the project.

await ra.spawnChild('MyActor', './path/to/MyActorBehavior.mjs');

Examples

We have a few examples, which we used as case studies during the development of this framework. Those should give you a better idea of how to write applications using JsAM. The examples can be found in the jsam-examples repository.

A good example to start with is the classic Ping-Pong example, wherein a messge is passed among two actors.

Under the hood

Since we support Clustering, a number of instances (henceforth referred to as nodes) can be deployed, which must know of each other's locations (ip:port) during the startup. There can be many ways this is possible. Since Node.js runs on a single thread, ideally we'd spin up muliple processes containing our JsAM Nodes on the same machine to be able take advantage of all the CPU cores.

These are the various components in JsAM:

Actor System

The Actor System is the core component of a node. A cluster can have multiple nodes and each node will have its own Actor System. The Actor System has a bunch of sub components, which makes it all churn.

Receptionist

Each Actor System will have its own Receptionist, wherein actor references that this node's actors communicate with, are stored. The actor references can be looked up by an Actor's locator, or by their name. Multiple Actors can have the same name as long as their hierarchy is different. This is so that we can adhere to the singleton actor pattern. The Receptionist can be accessed with actorSystem.getReceptionist().

Actor

The Actors are responsible for doing stuff. Each actor can have its own Behavior. Actors support two kinds of operations:

  • tell - Where the actor can be told to do something. This operation is handy when we don't expect the actor to respond with a reply.
  • ask - Where the actor can be asked to do something. This operation is handy when we do expect the actor to respond with a reply when it's done what it was asked.

In JsAM, each actor has its own messge Queue. The actors will refer to their Queues and if any messages are present in the queue, they'll dequeue it and process it based on its defined behavior. Each message will have:

  • messageType
  • messageBody

In a single node deployment, all actors will be present on that node. However, in a multiple node deployment, the actual actor can only be present in one of the nodes. In this case, the other ndoes which have to communicate with an actor on a different node, will have a reference to that actor - ActorRef.

Actor Hierarchy

Actors are hierarchical. Actors can have parents, and they can have children. In cluster mode, the parents and children can be on different nodes. An actor's hierarchy is represented using its locator. For example, if we have a child actor with the name ChildActor and its parent is ParentActor, its locator will be -/ParentActor/ChildActor.

Root Actor

The Root Actor is the first actor that will be created in the Actor System. This is the parent actor of all actors and Actor Systems in each node will have their own Root Actor. The locator of a Root Actor is always -/.

Actor Behavior

The Actor Behavior will determine what an actor is going to do. As an application developer, this is what needs to be defined for processing. Actor Behavior will have function callbacks based on the type of message.

Note

All the development and case studies were run on a 12 Core 24 Thread AMD Ryzen 9 3900X processor with 50.8 GB of RAM. This is important to note in relation to the numbers (processing time and memory consumption) that follow.

The numbers will vary of course, based on the processing power and RAM capacity.

GRPC and Streamification

All Actor creations happen through gRPC streams. This makes the process less resource intensive (except for memory) and faster. However, the ask and tell operations run on top of plain old gRPC calls.

Why we didn't streamify ask and tell operations

We ran the processing speed with jsam case study for a million actors (1000 parent actors with 1000 child actors each), with each actor reference having a stream open to the node with the actual actor.

  • Memory usage of streaming vs not streaming was huge - 32 GB vs 12 GB.
  • Time of processing (processing-speed with-jsam case study) was only shaved off by a few seconds - 94 vs 88 seconds.

In conclusion, the time memory trade-off was not worth it. And the staggering number of open streams ended up using a ton of memory.

0.0.22

2 years ago

0.0.23

2 years ago

0.0.24

2 years ago

0.0.25

2 years ago

0.0.20

2 years ago

0.0.21

2 years ago

0.0.15

2 years ago

0.0.16

2 years ago

0.0.17

2 years ago

0.0.18

2 years ago

0.0.19

2 years ago

0.0.10

2 years ago

0.0.11

2 years ago

0.0.12

2 years ago

0.0.13

2 years ago

0.0.14

2 years ago

0.0.4-alpha

2 years ago

0.0.3-alpha

2 years ago

0.0.2-alpha

2 years ago

0.0.2

2 years ago

0.0.9

2 years ago

0.0.8

2 years ago

0.0.7

2 years ago

0.0.6

2 years ago

0.0.1-alpha

2 years ago

0.0.1-test-alpha

2 years ago

0.0.1

2 years ago