Skip to content

Commit

Permalink
Add configurable timeout to function execution in lang-js
Browse files Browse the repository at this point in the history
This commit adds timeouts to lang-js function execution as well as the
ability to configure them. By default, functions can run for a maximum
of 30 minutes. This is tunable by a command-line argument, which can be
used by cyclone instances (functionality not added in this commit).

A new unit test has been added that uses a 1 second timeout in the
lang-js test suite.

In addition to these changes, dead example code has been removed and
the docs for lang-js have been revitalized, and the test suite timeout
has been increased for lang-js unit tests.

Signed-off-by: Nick Gerace <nick@systeminit.com>
  • Loading branch information
nickgerace committed Aug 6, 2024
1 parent e487fbc commit fc82011
Show file tree
Hide file tree
Showing 18 changed files with 139 additions and 203 deletions.
62 changes: 32 additions & 30 deletions bin/lang-js/README.md
Original file line number Diff line number Diff line change
@@ -1,57 +1,59 @@
# Lang JS
# `lang-js`

This directory contains `lang-js`.
This directory contains `lang-js`, the primary executor for functions in SI.

## Testing Locally
## Local Testing Guide

Here is an example of testing `lang-js` locally:
This is a guide for testing `lang-js` locally by using functions in the [examples](exmaples) directory.

```bash
SI_LANG_JS_LOG=* buck2 run :lang-js -- ${CHECK_NAME} < examples/${FILE_NAME}
```
> [!NOTE]
> While [dal integration tests](../../lib/dal/tests/integration.rs) are useful for testing new functions and workflows that leverage `lang-js`, it can be helpful to run `lang-js` directly for an efficient developer feedback loop.
## Encoding the Code
### 1) Writing the Function

Here is an example of "encoding the code" locally:
Before writing a payload file, we will want to write a function to be executed.
We can do this anywhere, but for this guide, we will write the file to the [examples](examples) directory.

```bash
cat examples/commandRunFailCode.js | base64 | tr -d '\n'
```
> [!TIP]
> You can write the function in JavaScript (`.js`) or TypeScript (`.ts`).
> You can also write an `async` function or a regular, synchronous one.
## Example Test Workflow

While [dal integration tests](../../lib/dal/tests/integration.rs) are useful for testing new functions and workflows
that leverage `lang-js`, it can be helpful to run `lang-js` directly for an efficient
developer feedback loop.

First, let's author a function and save it to the [examples](./examples) directory.
Here is an example function:

```js
function fail() {
throw new Error("wheeeeeeeeeeeeeeee");
}
```

Now, let's base64 encode this function and save the result to our clipboard.
### 2) Encoding the Function

With our new function written, we need to "base64 encode" it for the payload file.
You can do that by executing the following:

```bash
cat examples/commandRunFailCode.js | base64 | tr -d '\n'
cat examples/<code-file>.<js-or-ts> | base64 | tr -d '\n'
```

Then, we can create a `json` file in the same directory that's in a format that `lang-js` expects.
You'll want to copy the result to your clipboard.
On macOS, you can execute the following to do it in one step:

```json
{
"executionId": "fail",
"handler": "fail",
"codeBase64": "ZnVuY3Rpb24gZmFpbCgpIHsKICAgIHRocm93IG5ldyBFcnJvcigid2hlZWVlZWVlZWVlZWVlZWVlIik7Cn0K"
}
```bash
cat examples/<code-file>.<js-or-ts> | base64 | tr -d '\n' | pbcopy
```

Finally, we can run our function in `lang-js` directly.
### 3) Preparing the Payload File


With the encoded function in our clipboard, we can create a payload file to send to `lang-js`.
The payload file will be a `json` file in the same directory.

At the time of writing the guide ([PR #4259](https://github.com/systeminit/si/pull/4259)), the shape the the `json` file has drifted from what it used to be, so developers will need to read the source code to learn its shape.

### 4) Running the Function via the Payload File

When we run our function in `lang-js`, let's set the debug flag to see what's going on!

```bash
cat examples/commandRunFail.json | SI_LANG_JS_LOG=* buck2 run :lang-js -- commandRun
cat examples/<payload-file>.json | SI_LANG_JS_LOG=* buck2 run :lang-js -- <function-kind>
```
9 changes: 9 additions & 0 deletions bin/lang-js/examples/TODO.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# TODO

At the time of this file's creation, the existing examples did not work.
The [README](../README.md) has been updated to be generic for any example in its guide.

We should create new examples for testing `lang-js` locally without the entire stack.
For now, use `dal` integration tests for `lang-js` testing or add new examples to this directory and delete this `TODO.md` file.

See [PR #4259](https://github.com/systeminit/si/pull/4259) for more information.
9 changes: 0 additions & 9 deletions bin/lang-js/examples/codeGenerationTest.json

This file was deleted.

11 changes: 0 additions & 11 deletions bin/lang-js/examples/codeGenerationTestCode.js

This file was deleted.

5 changes: 0 additions & 5 deletions bin/lang-js/examples/commandRunFail.json

This file was deleted.

3 changes: 0 additions & 3 deletions bin/lang-js/examples/commandRunFailCode.js

This file was deleted.

6 changes: 0 additions & 6 deletions bin/lang-js/examples/resolver.json

This file was deleted.

20 changes: 0 additions & 20 deletions bin/lang-js/examples/resolverFunctionTest.json

This file was deleted.

61 changes: 0 additions & 61 deletions bin/lang-js/examples/resolverFunctionTestCode.js

This file was deleted.

9 changes: 0 additions & 9 deletions bin/lang-js/examples/resourceSyncTest.json

This file was deleted.

11 changes: 0 additions & 11 deletions bin/lang-js/examples/resourceSyncTestCode.js

This file was deleted.

5 changes: 0 additions & 5 deletions bin/lang-js/examples/workflowResolveTest.json

This file was deleted.

10 changes: 0 additions & 10 deletions bin/lang-js/examples/workflowResolveTestCode.js

This file was deleted.

33 changes: 25 additions & 8 deletions bin/lang-js/src/function.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,12 +91,12 @@ export interface OutputLine {
message: string;
}

export async function executeFunction(kind: FunctionKind, request: Request) {
export async function executeFunction(kind: FunctionKind, request: Request, timeout: number) {
// Run Before Functions
const ctx = ctxFromRequest(request);

for (const beforeFunction of request.before || []) {
await executor(ctx, beforeFunction, FunctionKind.Before, before);
await executor(ctx, beforeFunction, FunctionKind.Before, timeout, before);
// Set process environment variables, set from requestStorage
{
const requestStorageEnv = rawStorageRequest().env();
Expand All @@ -110,7 +110,7 @@ export async function executeFunction(kind: FunctionKind, request: Request) {
let result;
switch (kind) {
case FunctionKind.ActionRun:
result = await executor(ctx, request as ActionRunFunc, kind, action_run);
result = await executor(ctx, request as ActionRunFunc, kind, timeout, action_run);

console.log(
JSON.stringify({
Expand All @@ -129,6 +129,7 @@ export async function executeFunction(kind: FunctionKind, request: Request) {
ctx,
request as ReconciliationFunc,
kind,
timeout,
reconciliation,
);
break;
Expand All @@ -137,6 +138,7 @@ export async function executeFunction(kind: FunctionKind, request: Request) {
ctx,
request as ResolverFunc,
kind,
timeout,
resolver_function,
);

Expand All @@ -152,13 +154,14 @@ export async function executeFunction(kind: FunctionKind, request: Request) {
);
break;
case FunctionKind.Validation:
result = await executor(ctx, request as JoiValidationFunc, kind, joi_validation);
result = await executor(ctx, request as JoiValidationFunc, kind, timeout, joi_validation);
break;
case FunctionKind.SchemaVariantDefinition:
result = await executor(
ctx,
request as SchemaVariantDefinitionFunc,
kind,
timeout,
schema_variant_definition,
);
break;
Expand All @@ -169,10 +172,18 @@ export async function executeFunction(kind: FunctionKind, request: Request) {
console.log(JSON.stringify(result));
}

function timer(seconds: number): Promise<never> {
const ms = seconds * 1000;
return new Promise((_, reject) => {
setTimeout(() => reject(new Error(`function timed out after ${seconds} seconds`)), ms);
});
}

export async function executor<F extends Func | ActionRunFunc, Result>(
ctx: RequestCtx,
func: F,
kind: FunctionKind,
timeout: number,
{
debug,
wrapCode,
Expand All @@ -199,8 +210,14 @@ export async function executor<F extends Func | ActionRunFunc, Result>(

const vm = createNodeVm(createSandbox(kind, ctx.executionId));

const result = await execute(vm, ctx, func, code);
debug({ result });

return result;
try {
const result = await Promise.race([
execute(vm, ctx, func, code),
timer(timeout),
]);
debug({ result });
return result;
} catch (error) {
throw new Error(`${error}`);
}
}
Loading

0 comments on commit fc82011

Please sign in to comment.