Isolate and context re-use.

This commit is contained in:
Sam Rose 2024-01-12 15:08:08 +00:00
parent 290dde125e
commit c508a435d6
No known key found for this signature in database
3 changed files with 31 additions and 103 deletions

View File

@ -1,5 +1,5 @@
import { IdentityContext } from "@budibase/types"
import { ExecutionTimeTracker } from "../timers"
import { Isolate, Context } from "isolated-vm"
// keep this out of Budibase types, don't want to expose context info
export type ContextMap = {
@ -10,5 +10,6 @@ export type ContextMap = {
isScim?: boolean
automationId?: string
isMigrating?: boolean
jsExecutionTracker?: ExecutionTimeTracker
jsIsolate: Isolate
jsContext: Context
}

View File

@ -20,41 +20,3 @@ export function cleanup() {
}
intervals = []
}
export class ExecutionTimeoutError extends Error {
public readonly name = "ExecutionTimeoutError"
}
export class ExecutionTimeTracker {
static withLimit(limitMs: number) {
return new ExecutionTimeTracker(limitMs)
}
constructor(readonly limitMs: number) {}
private totalTimeMs = 0
track<T>(f: () => T): T {
this.checkLimit()
const start = process.hrtime.bigint()
try {
return f()
} finally {
const end = process.hrtime.bigint()
this.totalTimeMs += Number(end - start) / 1e6
this.checkLimit()
}
}
get elapsedMS() {
return this.totalTimeMs
}
checkLimit() {
if (this.totalTimeMs > this.limitMs) {
throw new ExecutionTimeoutError(
`Execution time limit of ${this.limitMs}ms exceeded: ${this.totalTimeMs}ms`
)
}
}
}

View File

@ -1,76 +1,41 @@
import ivm from "isolated-vm"
import env from "./environment"
import { setJSRunner } from "@budibase/string-templates"
import { context, timers } from "@budibase/backend-core"
import { context } from "@budibase/backend-core"
import tracer from "dd-trace"
import { readFileSync } from "fs"
type TrackerFn = <T>(f: () => T) => T
const helpersSource = readFileSync(
"node_modules/@budibase/string-templates/dist/bundle.mjs",
"utf8"
)
export function init() {
setJSRunner((js: string, ctx: Record<string, any>) => {
return tracer.trace("runJS", {}, span => {
const perRequestLimit = env.JS_PER_REQUEST_TIME_LIMIT_MS
let track: TrackerFn = f => f()
if (perRequestLimit) {
const bbCtx = tracer.trace("runJS.getCurrentContext", {}, span =>
context.getCurrentContext()
)
if (bbCtx) {
if (!bbCtx.jsExecutionTracker) {
span?.addTags({
createdExecutionTracker: true,
})
bbCtx.jsExecutionTracker = tracer.trace(
"runJS.createExecutionTimeTracker",
{},
span => timers.ExecutionTimeTracker.withLimit(perRequestLimit)
)
}
span?.addTags({
js: {
limitMS: bbCtx.jsExecutionTracker.limitMs,
elapsedMS: bbCtx.jsExecutionTracker.elapsedMS,
},
})
// We call checkLimit() here to prevent paying the cost of creating
// a new VM context below when we don't need to.
tracer.trace("runJS.checkLimitAndBind", {}, span => {
bbCtx.jsExecutionTracker!.checkLimit()
track = bbCtx.jsExecutionTracker!.track.bind(
bbCtx.jsExecutionTracker
)
})
}
}
const isolate = new ivm.Isolate({ memoryLimit: 64 })
const vm = isolate.createContextSync()
const jail = vm.global
jail.setSync("global", jail.derefInto())
for (let key in ctx) {
let value
if (["alert", "setInterval", "setTimeout"].includes(key)) {
value = undefined
} else {
value = ctx[key]
}
if (typeof value === "function") {
js = value.toString() + "\n" + js
continue
} else {
value = new ivm.ExternalCopy(value).copyInto({ release: true })
}
jail.setSync(key, value)
}
const script = isolate.compileScriptSync(js)
return track(() => {
return script.runSync(vm, {
timeout: env.JS_PER_EXECUTION_TIME_LIMIT_MS,
const bbCtx = context.getCurrentContext()!
if (!bbCtx.jsIsolate) {
bbCtx.jsIsolate = new ivm.Isolate({ memoryLimit: 64 })
bbCtx.jsContext = bbCtx.jsIsolate.createContextSync()
const helpersModule = bbCtx.jsIsolate.compileModuleSync(helpersSource)
helpersModule.instantiateSync(bbCtx.jsContext, () => {
throw new Error("No imports allowed")
})
}
const perRequestLimit = env.JS_PER_REQUEST_TIME_LIMIT_MS
if (perRequestLimit) {
const cpuMs = Number(bbCtx.jsIsolate.cpuTime) / 1e6
if (cpuMs > perRequestLimit) {
throw new Error(
`CPU time limit exceeded (${cpuMs}ms > ${perRequestLimit}ms)`
)
}
}
const script = bbCtx.jsIsolate.compileScriptSync(js)
return script.runSync(bbCtx.jsContext, {
timeout: env.JS_PER_EXECUTION_TIME_LIMIT_MS,
})
})
})