jsr-script

1.0.6 • Public • Published

Table of Contents

Script Execution Module

Script execution.

Experimental library that enables running scripts with the same semantics as EVAL, note that currently this uses synchronous JSON message passing, a later version may pass RESP messages via stdin and stdout to the child process.

Component of the jsr library.

Requires node and npm.

Install

npm i jsr-script

Compatibility

Whilst the script execution engine aims to be as compatible as possible with the redis script support there are some important differences. Primarily, scripts are javascript not lua as we are already using a scripting language, the other idiosyncrasies are:

  • Accessing the passed KEYS and ARGS start with index 0 not 1 as javascript arrays are zero-based.
  • Data type conversion differs, see data types.

Data Types

Data type conversion between javascript types and the RESP responses:

  • String: Simple string reply.
  • string: Bulk string reply.
  • number: Integers are returned as integer types, floating point numbers are returned as strings.
  • true: Coerced to the integer 1.
  • false: Coerced to the integer 0.
  • null: Null reply (nil).
  • Buffer: Coerced to string using toString and default encoding (utf8).
  • Error: Simple string error reply.
  • Array: Multi bulk reply.

In addition objects with the following structure are handled:

  • {ok: 'OK'}: Simple string reply.
  • {err: 'ERR'}: Simple string error reply.

Returning undefined or an object that does not conform to the data types described above will result in an error.

It is possible to return an error to the client by throwing the error, the following are equivalent scripts:

throw new Error('ERR');
return new Error('ERR');
return {err: 'ERR'};
return redis.error_reply('ERR');

Sandbox

The script execution sandbox exposes the following functions:

  • redis.sha1hex: Get the sha1 hex checksum of a string.
  • redis.status_reply: Return a status reply to the client.
  • redis.error_reply: Return an error reply to the client.
  • redis.call: Execute a command with arguments.
  • redis.log: Log a message.

In addition the following globals are exposed:

  • Error: Error class.
  • JSON: JSON parse and stringify.
  • Buffer: Buffer class.
  • util: The util module.
  • crypto: The crypto module.

Log

The redis.log method will log a message and return the message, it behaves almost identically to the redis version:

return redis.log(redis.LOG_DEBUG, "debug message");
return redis.log(redis.LOG_VERBOSE, "verbose message");
return redis.log(redis.LOG_NOTICE, "notice message");
return redis.log(redis.LOG_WARNING, "warning message");

But you may also pass replacement parameters:

return redis.log(redis.LOG_DEBUG, "args: %s", ARGS);

Or the full command using EVAL:

eval 'return redis.log(redis.LOG_DEBUG, "args: %s", ARGS);' 0 foo bar

Challenge

Designing the script execution engine posed some interesting challenges, for example, how to cope with infinite loops and bad function calls:

while(true){}
process.exit(1)

The former infinite loop would block the server if it were executed in the main process and there would be no way to catch a script execution timeout which would make it impossible to implement the semantics of SCRIPT KILL.

The latter would just exit the process and cause all sorts of problems.

The solution decided upon is to spawn a child process for script execution and use the vm module to create a sandbox for the compiled scripts. Using a child process for script execution allows the parent process to continue serving clients while long-running scripts are executing and using a vm sandbox for the script makes calling process.exit impossible as well as preventing global variable leak.

Using a child process for script execution posed some more interesting design challenges as the parent and child process' need to communicate. While the messaging is actually synchronous it is not possible to return a value from a call to the parent process as the communication is based on events. This is abstracted away so that invoking:

return redis.call("get", "foo");

Will return a function which instructs the script execution to wait for the result of the command execution from the parent process.

Because the calls to the parent process are synchronous it is still possible to block the parent process by issuing commands that would result in a large amount of data being converted to JSON and returned to the script execution process:

return redis.call("keys", "*");

Developer

Test

Tests are not included in the package, clone the repository:

npm test

Documentation

To generate all documentation:

npm run docs

Readme

To build the readme file from the partial definitions (requires mdp):

npm run readme

License

Everything is MIT. Read the license if you feel inclined.

Generated by mdp(1).

Readme

Keywords

none

Package Sidebar

Install

npm i jsr-script

Weekly Downloads

4

Version

1.0.6

License

none

Last publish

Collaborators

  • muji
  • tmpfs