V8 Javascript API

Execution Environment

[Architecture]

Description

Commands are executed within sandboxed environments which prevent data sharing between individual commands. There are two forms of sandboxing in place, both of which cause different forms of data isolation. These are module sandboxing and isolate sandboxing.

Details on the isolation methods are provided below however there are two practical consequences of isolation:

  • The module system isolates commands within their own private scope. Therefore it is not possible for different commands to share data via global variables since each command will see a different set of globals.

  • It is not guaranteed that a command will always be executed within the same environment. Therefore it is not possible for an individual command to persist data reliably between executions using global variables.

If commands do need to share or persist data between executions then this data should be stored by using commands to create an Attribute_container, setting data on it's attributes then storing it in the RealityServer database.

Module sandboxing

Each module in RealityServer is wrapped in a JavaScript function before it is executed. This means that any global variables defined within a module are actually scoped to the module, not to the global object, making them private to the module. Therefore it is not possible to share data between commands by defining variables with the same name.

For example if we have the following two commands:

‎// set_share_data.js
var share_data;
module.exports.command = {
    name: 'set_share_data',
    description: 'Sets a global variable.',
    arguments: {
        input : {
            description: 'The value to set.',
            type: 'String',
            default: null
        }
    },
    execute: function(args) {
        if  (args.input !== null) {
            share_data = args.input
        }        
    }
};

‎// get_share_data.js
var share_data;
module.exports.command = {
    name: 'get_share_data',
    description: 'Gets a global variable.',
    execute: function(args) {
        if  (share_data) {
            return share_data;
        } else {
            throw new RS.Error(1,"no shared data defined.");
        }        
    }
};

Then we execute the following:

‎[
  { "id": 1, "method": "set_share_data", "params": { "input": "my value" } },
  { "id": 2, "method": "get_share_data", "params": {  } }
]

It would return:

‎[
  { "id": 1, "result": {}, "error": null },
  { "id": 2, "result": null, "error" : { "code": 1, "message": "no shared data defined"} }
]

since share_data in the get_share_data command does not refer to the same object as share_data in set_share_data.

Isolate sandboxing

All V8 command execution occurs within a V8 'isolate'. Isolates are the mechanism within V8 used to provide sandboxed environments. This means that commands executing within a given isolate do not have access to JavaScript data from another isolate. This is similar to how JavaScript executing in one tab of a web browser does not have access to functions or global variables from another tab running in the same browser.

RealityServer maintains a pool of isolates for executing V8 commands. Starting up an isolate is an expensive process so being able to reuse an isolate over multiple command requests is required to provide reasonable performance. Maintaining multiple isolates also allows multiple V8 command requests to be executed simultaneously. However all of this means that there is no guarantee that 'global' data stored by a command will be available the next time that same command is executed.

The first time a V8 command is encountered in a command request an isolate is checked out of the pool. This isolate is used to execute the command, and any subsequent V8 commands in that request. Imagine we have the following command:

‎// return_last_value.js
var last_value;
module.exports.command = {
    name: 'return_last_value',
    description: 'Returns the last value stored by this command. If input is provided then this is stored for returning on the next call.',
    arguments: {
        input : {
            description: 'If provided this value is stored to be returned next time.',
            type: 'String',
            default: null
        }
    },    
    execute: function(args) {
        var to_return = last_value;
        if  (args.input !== null) {
            last_value = args.input;
        }
        if (to_return !== undefined) {
            return to_return;
        }
    }
};

Then we execute the following:

‎[
  { "id":1, "method": "return_last_value", "params": { "input":"first" } },
  { "id":2, "method": "return_last_value", "params": { "input":"second" } },
  { "id":3, "method": "return_last_value", "params": { } },
  { "id":4, "method": "return_last_value", "params": { } }
]

It would return:

‎[
  { "id":1, "result": null, "error": null },
  { "id":2, "result": "first", "error": null },
  { "id":3, "result": "second", "error": null },
  { "id":4, "result": "second", "error": null },
]

This is because each command in the request is executed in the same isolate and so sees the same instance of last_value.

At the end of execution the isolate is checked back into the pool for later reuse. If we then execute the same sequence of commands we could get two different responses:

If the same isolate gets used as before we would get:

‎[
  { "id":1, "result": "second", "error": null },
  { "id":2, "result": "first", "error": null },
  { "id":3, "result": "second", "error": null },
  { "id":4, "result": "second", "error": null },
]

since last_value still contains second from the initial set of commands.

Of, if a new isolate was used we would get:

‎[
  { "id":1, "result": null, "error": null },
  { "id":2, "result": "first", "error": null },
  { "id":3, "result": "second", "error": null },
  { "id":4, "result": "second", "error": null },
]

since last_value isn't initialized yet. In fact, the result we get in command 1 is effectively random since we do not know what value a previous command may have set it to, nor do we know which isolate the command may be executed in.