Isolate and context re-use.
This commit is contained in:
parent
290dde125e
commit
c508a435d6
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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`
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
Loading…
Reference in New Issue