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.
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
andARGS
start with index0
not1
asjavascript
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 integer1
. -
false
: Coerced to the integer0
. -
null
: Null reply (nil). -
Buffer
: Coerced to string usingtoString
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
: Theutil
module. -
crypto
: Thecrypto
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).