-
Notifications
You must be signed in to change notification settings - Fork 4
Getting Started
First, install the package engine.js with NPM
$ npm install engine.js
Now we need to start intake, cylinder, and exhaust
components. These three components make up the "server-side" of an
Engine.js application.
Create a new file app.js and place the following code in
the new file.
var engine = require("engine.js").engine,
intake = engine.intake.create(),
cylinder = engine.cylinder.create(),
exhaust = engine.exhaust.create();Note: For the sake of simplicity we're starting the server components from within the application. Normally these components are started in a separate process(es).
Now we need to create a client. In your app.js file:
var client = engine.client.create();The client will create task objects and send them through the
server components (intake, cylinder, exhaust). Results from the
task will then be sent back to the client.
A unit of work in an Engine.js application is a task. A task
contains all the information it needs to execute.
In your app.js file add the following:
var task = client.createTask();
task.setCode("1+1");By default, a task's code is executed in an empty Javascript
environment. The code only has access to
built-in Javascript objects. Later we will
see how to add custom objects to execution environment.
A task is run asynchronously. When the task has completed the
task will emit an eval event passing:
-
err:nullunless there was a problem before a task was executed. -
response: An instance of a TaskResponse object.
In your app.js file add the following:
task.on('eval', function(err, response){
console.log("task evaluation:", response.getEvaluation());
});
task.run();Your app.js file should look like this:
var engine = require("engine.js").engine,
intake = engine.intake.create(),
cylinder = engine.cylinder.create(),
exhaust = engine.exhaust.create();
var client = engine.client.create();
var task = client.createTask();
task.setCode("1+1");
task.on("eval", function(err, response){
console.log("task evaluation:",response.getEvaluation());
});
task.run();If you run node app.js you should see task evaluation: 2
You should also notice that the process does not immediately
exit. This is because the client and server components are still
connected. To fix this, close the client and server components within
the "eval" event's callback:
task.on("eval", function(err, response){
console.log("task evaluation:", response.getEvaluation());
client.close();
intake.close();
cylinder.close();
exhaust.close();
});Note: Normally the server components are started in separate process(es) so only closing the
clientis usually necessary.
In the last chapter we showed how to execute code in an empty Javascript environment. In this chapter will see show how to add your own functions and runtime data to the execution environment.
An Engine.js Task should include all the information it needs to execute. This includes three things:
- An Execution Context (
Context) - Local Variable Bindings (
Locals) - User Code (
Code)
Context: A task's Context explicitly defines the global object
in which a task's Code is run. A task's Code does not have access
to anything except the defined global object and
built-in Javascript objects.
A Context is a Javascript function value (an anonymous function
wrapped in parens) that, when invoked, returns an object-literal. This
process is known as "Rendering the Context". The Context takes one
argument (locals) which binds when the Context is rendered.
For example:
(function(locals){
return {
foo: "bar",
hello: locals.world,
qux: function() {
return "qal";
}
};
})In this example the global object will be populated with a foo
object whose value is "bar", a hello object whose value is derived
from the locals variable hash (see below), and a qux object which
is a function. The task's Code will only have access to these three
objects (in addition to the built-in objects like Date, Array, etc.).
A Context is set on a Task object using the setContext method. It expects a String.
Local Variable Bindings: There are times when your task will
depend on runtime information. The locals argument of the Context
function allows you to pass-in an object literal of key/value pairs
which will be injected into your rendered Context.
In the example above, our Context references locals.world. If we
set the task's locals property to:
{ world: "Mundo" }then the locals.world object in the rendered Context would be "Mundo"
Locals are set on a Task object using the setLocals method. It expects an object-literal.
Code: The core of the task is the Code to be executed. Code
is just Plain Old Javascript (tm). It is executed in the context of
the task's corresponding rendered Context. It is set on the task
object as a String.
Code is set on a Task object using the setCode method. It expects a String.
Your app.js file from before should look like this:
var engine = require("engine.js").engine,
intake = engine.intake.create(),
cylinder = engine.cylinder.create(),
exhaust = engine.exhaust.create();
var client = engine.client.create();
var task = client.createTask();
task.setCode("1+1");
task.on("eval", function(err, response){
console.log("task evaluation:",response.getEvaluation());
client.close();
intake.close();
cylinder.close();
exhaust.close();
});
task.run();Lets define an increment function that will increment a value by 1. The definition of the function looks like this:
function(value){
return value + 1;
}Recall that a Context must be a function that returns an object-literal. An empty Context looks like this:
(function(locals){
return {
}
})To add the increment function to our Context we must add an
increment key to the returned object-literal. Use the previously
mentioned function definition as the increment key's value.
(function(locals){
return {
increment: function(value){
return value + 1;
}
}
})Put the above in a new file called context.js. Like a task's code, a
task's context is set on the task object as a string. Keeping the code
and context in seperate files allows us to easily read them as strings
while keeping their contents managable in a text editor.
To set the Context on the task, read in the contents of the
context.js file and set the Context on the task:
var fs = require("fs");
var context = fs.readFileSync(__dirname + "/context.js","utf-8");
task.setContext(context);Now you may call your new increment function from your Code:
task.setCode("increment(2)");Your app.js file should look like this now:
var engine = require("engine.js").engine,
intake = engine.intake.create(),
cylinder = engine.cylinder.create(),
exhaust = engine.exhaust.create();
var client = engine.client.create();
var task = client.createTask();
task.setCode("increment(2)");
var fs = require("fs");
var context = fs.readFileSync(__dirname + "/context.js","utf-8");
task.setContext(context);
task.on("eval", function(err, response){
console.log("task evaluation:",response.getEvaluation());
client.close();
intake.close();
cylinder.close();
exhaust.close();
});
task.run();Run node app.js and you should see task evaluation: 3
Sometimes the execution of a task will depend on information supplied
at runtime. This is what Locals are intended for.
Lets modify our existing increment function. Rather than increment
the passed-in value by 1, we will determine the value to increment by
at runtime.
Modify your context.js file to look like this:
(function(locals){
return {
increment: function(value){
return locals.increment_by + value;
}
}
})The value of the locals argument is set on the task object with
the setLocals method. Unlike the values accepted by setCode and
setContext, the setLocals method accepts an object-literal of
key/value pairs. Since the increment function in our context
references locals.increment_by, our Locals must have an
increment_by key.
Set the Locals for your task in app.js:
var engine = require("engine.js").engine,
intake = engine.intake.create(),
cylinder = engine.cylinder.create(),
exhaust = engine.exhaust.create();
var client = engine.client.create();
var task = client.createTask();
task.setCode("increment(2)");
var fs = require("fs");
var context = fs.readFileSync(__dirname + "/context.js","utf-8");
task.setContext(context);
task.setLocals({
increment_by: 3
});
task.on("eval", function(err, response){
console.log("task evaluation:",response.getEvaluation());
client.close();
intake.close();
cylinder.close();
exhaust.close();
});
task.run();Run node app.js and you should see task evaluation: 5
Locals cannot be directly accessed by Code. Properties on the
locals argument can only be accessed my functions in the Context.
In the last chapter we saw how to run a Task with Engine.js. In this chapter we're going to look at how Engine.js is architected and how it can be configured.
As you might have guessed, Engine.js uses the metaphor of a combustion engine to name its internal components.
-
Client: The
Clientis responsible for create newTaskobjects and sending them theIntake. It is also responsible for receivingTaskResponseobjects from theExhaust -
Intake: The
Intakeis responsible for accepting incomingTaskmessages from many clients and forwarding them to manyCylinders -
Cylinder: When a
Cylinderis created it starts its ownPiston(in a separate child process). When aCylinderreceives aTaskit starts an execution timer and sends theTaskto itsPistonfor execution. When thePistonreturns a result theCylinderforwards it to theExhaust- If the execution timer finishes before a response from the
Pistonis received, thePistonprocess is killed and an error is sent back to theClient(via theExhaust).
- If the execution timer finishes before a response from the
-
Piston: The
Pistonis responsible for creating a code sandbox from the receivedTaskmessages's context and run-time variable bindings. ThePistonthen runs the user-code against the sandbox and returns the results to its parent Cylinder. -
Exhaust: The
ExhaustreceivesTaskresult messages from manyCylindersand forwards then back to the appropriateClient.
All components use ZeroMQ to communicate with each other. This allows
us to start the components (Intake, Cylinder, and Exhaust) in
individual processes, even on different machines (as long as they are
in the same network). In the previous chapter we started all of the
components in the same process, but this was only for convenience. The
ZeroMQ endpoints are
configurable for most components.
+------------+ +------------+
-----| | | |
+------+ | | Cylinder |<---->| Piston |
| Task +-+ +------------+ +------------+ | | | | |
+------+ | | | | | | +------------+ +------------+
(n) +---+ Client +----+ +--->----| Intake |-->-+ | (n)
+------+ | | | | | | | | | +------------+ +------------+
| Task +-+ +------------+ | | +------------+ | | | | | |
+------+ (n) | | (1) +-->---+ | Cylinder |<---->+ Piston |
(n) +-----+ | | | | |
| | +--<---+ +------------+ +------------+
+------------+ | | +------------+ | | (n)
+------+ | | | | | | | | +------------+ +------------+
| Task +-----+ Client +----+ +---<----| Exhaust +-<--+ | | | | |
+------+ | | | | | | Cylinder |<---->+ Piston |
(n) +------------+ +------------+ | | | | |
(n) (1) | +------------+ +------------+
| (n)
| +------------+ +------------+
| | | | |
+----| Cylinder |<---->| Piston |
| | | |
+------------+ +------------+
(n)
- Start multiple
Cylinders - ZeroMQ round-robin balancing
- Standard Logger
- Custom Logger
TaskResponse.getDebug()
There are several measures taken to secure Engine.js
- Child process execution
- V8 Context membrane
- Execution timeouts
-
Function.toString()removal
- Asynchronous Functions
- Using 3rd Party Modules