Skip to content

Commit

Permalink
Disable interpreter caching
Browse files Browse the repository at this point in the history
  • Loading branch information
achamayou committed Oct 17, 2023
1 parent 33e4506 commit f73360e
Show file tree
Hide file tree
Showing 24 changed files with 33 additions and 584 deletions.
58 changes: 1 addition & 57 deletions doc/build_apps/js_app_bundle.rst
Original file line number Diff line number Diff line change
Expand Up @@ -339,60 +339,4 @@ If CCF is updated and introduces a newer JavaScript engine version, then any pre
]
}
.. note:: The operator RPC :http:GET:`/node/js_metrics` returns the size of the bytecode and whether it is used. If it is not used, then either no bytecode is stored or it needs to be re-compiled due to a CCF update.

Reusing interpreters
~~~~~~~~~~~~~~~~~~~~

By default, every request executes in a freshly-constructed JS interpreter. This provides extremely strict sandboxing - the only interaction with other requests is transactionally via the KV - and so forbids the sharing of any global state. For some applications, this may lead to unnecessarily duplicated work.

For instance, if your application needs to construct a large, immutable singleton object to process a request, that construction cost will be paid in each and every request. Requests could execute significantly faster if they were able to access and reuse a previously-constructed object, rather than constructing their own. JS libraries designed for other runtimes (such as Node) may benefit from this, as they expect to have a persistent global state.

CCF supports this pattern with `interpreter reuse`. Applications may opt-in to persisting an interpreter, and all of its global state, to be reused by multiple requests. This means that expensive initialisation work can be done once, and the resulting objects stashed in the global state where future requests will reuse them.

Note that this removes the sandboxing protections described above. If the contents of the global state change the result of a request's execution, then the execution will no longer be reproducible from the state recorded in the ledger, since the state of the interpreter cache will not be recorded. This should be avoided - reuse should only be used to make a handler `faster`, not to `change its behaviour`.

This behaviour is controlled in ``app.json``, with the ``"interpreter_reuse"`` property on each endpoint. The default behaviour, taken when the field is omitted, is to avoid any interpreter reuse, providing strict sandboxing safety. To reuse an interpreter, set ``"interpreter_reuse"`` to an object of the form ``{"key": "foo"}``, where ``foo`` is an arbitrary, app-defined string. Interpreters will be shared between endpoints where this string matches. For instance:

.. code-block:: json
{
"endpoints": {
"/admin/modify": {
"post": {
"js_module": ...,
"interpreter_reuse": {"key": "admin_interp"}
}
},
"/admin/admins": {
"get": {
"js_module": ...,
"interpreter_reuse": {"key": "admin_interp"}
},
"post": {
"js_module": ...,
"interpreter_reuse": {"key": "admin_interp"}
}
},
"/sum/{a}/{b}": {
"get": {
"js_module": ...,
"interpreter_reuse": {"key": "sum"}
}
},
"/fast/and/small": {
"get": {
"js_module": ...
// No "interpreter_reuse" field
}
}
}
}
In this example, each CCF node will store up-to 2 interpreters, and divides the endpoints into 3 classes:

- Requests to ``POST /admin/modify``, ``GET /admin/admins``, and ``POST /admin/admins`` will reuse the same interpreter (keyed by the string ``"admin_interp"``).
- Requests to ``GET /sum/{a}/{b}`` will use a separate interpreter (keyed by the string ``"sum"``).
- Requests to ``GET /fast/and/small`` will `not reuse any interpreters`, instead getting a fresh interpreter for each incoming request.

Note that ``"interpreter_reuse"`` describes when interpreters `may` be reused, but does not ensure that an interpreter `is` reused. A CCF node may decide to evict interpreters to limit memory use, or for parallelisation. Additionally, interpreters are node-local, are evicted for semantic safety whenever the JS application is modified, and only constructed on-demand for an incoming request (so the first request will see no performance benefit, since it includes the initialisation cost that later requests can skip). In short, this reuse should be seen as a best-effort optimisation - when it takes effect it will make many request patterns significantly faster, but it should not be relied upon for correctness.
.. note:: The operator RPC :http:GET:`/node/js_metrics` returns the size of the bytecode and whether it is used. If it is not used, then either no bytecode is stored or it needs to be re-compiled due to a CCF update.
23 changes: 1 addition & 22 deletions doc/schemas/gov_openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -165,9 +165,6 @@
"forwarding_required": {
"$ref": "#/components/schemas/ForwardingRequired"
},
"interpreter_reuse": {
"$ref": "#/components/schemas/InterpreterReusePolicy"
},
"js_function": {
"$ref": "#/components/schemas/string"
},
Expand Down Expand Up @@ -310,29 +307,11 @@
"HttpMethod": {
"type": "string"
},
"InterpreterReusePolicy": {
"oneOf": [
{
"properties": {
"key": {
"type": "string"
}
},
"required": [
"key"
],
"type": "object"
}
]
},
"JSRuntimeOptions": {
"properties": {
"log_exception_details": {
"$ref": "#/components/schemas/boolean"
},
"max_cached_interpreters": {
"$ref": "#/components/schemas/uint64"
},
"max_execution_time_ms": {
"$ref": "#/components/schemas/uint64"
},
Expand Down Expand Up @@ -1291,7 +1270,7 @@
"info": {
"description": "This API is used to submit and query proposals which affect CCF's public governance tables.",
"title": "CCF Governance API",
"version": "4.1.2"
"version": "4.1.3"
},
"openapi": "3.0.0",
"paths": {
Expand Down
8 changes: 2 additions & 6 deletions doc/schemas/node_openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -479,9 +479,6 @@
"bytecode_used": {
"$ref": "#/components/schemas/boolean"
},
"max_cached_interpreters": {
"$ref": "#/components/schemas/uint64"
},
"max_execution_time": {
"$ref": "#/components/schemas/uint64"
},
Expand All @@ -497,8 +494,7 @@
"bytecode_used",
"max_heap_size",
"max_stack_size",
"max_execution_time",
"max_cached_interpreters"
"max_execution_time"
],
"type": "object"
},
Expand Down Expand Up @@ -912,7 +908,7 @@
"info": {
"description": "This API provides public, uncredentialed access to service and node state.",
"title": "CCF Public Node API",
"version": "4.6.0"
"version": "4.6.1"
},
"openapi": "3.0.0",
"paths": {
Expand Down
12 changes: 1 addition & 11 deletions include/ccf/endpoint.h
Original file line number Diff line number Diff line change
Expand Up @@ -116,23 +116,13 @@ namespace ccf::endpoints
std::string js_module;
/// JavaScript function name
std::string js_function;
/// Determines how JS interpreters may be reused between multiple calls,
/// sharing global state in potentially unsafe ways. The default empty value
/// means no reuse is permitted.
std::optional<InterpreterReusePolicy> interpreter_reuse = std::nullopt;
};

DECLARE_JSON_TYPE_WITH_OPTIONAL_FIELDS(EndpointProperties);
DECLARE_JSON_REQUIRED_FIELDS(
EndpointProperties, forwarding_required, authn_policies);
DECLARE_JSON_OPTIONAL_FIELDS(
EndpointProperties,
openapi,
openapi_hidden,
mode,
js_module,
js_function,
interpreter_reuse);
EndpointProperties, openapi, openapi_hidden, mode, js_module, js_function);

struct EndpointDefinition
{
Expand Down
7 changes: 1 addition & 6 deletions include/ccf/service/tables/jsengine.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,18 +23,13 @@ namespace ccf
/// NOTE: this is a security risk as it may leak sensitive information,
/// albeit to the caller only.
bool return_exception_details = false;
/// @brief how many interpreters may be cached in-memory for future reuse
size_t max_cached_interpreters = 10;
};

DECLARE_JSON_TYPE_WITH_OPTIONAL_FIELDS(JSRuntimeOptions)
DECLARE_JSON_REQUIRED_FIELDS(
JSRuntimeOptions, max_heap_bytes, max_stack_bytes, max_execution_time_ms)
DECLARE_JSON_OPTIONAL_FIELDS(
JSRuntimeOptions,
log_exception_details,
return_exception_details,
max_cached_interpreters);
JSRuntimeOptions, log_exception_details, return_exception_details);

using JSEngine = ServiceValue<JSRuntimeOptions>;

Expand Down
Loading

0 comments on commit f73360e

Please sign in to comment.