Add tests for per-request execution timeout.
This commit is contained in:
parent
bd324f3225
commit
1c34147357
|
@ -21,6 +21,10 @@ export function cleanup() {
|
||||||
intervals = []
|
intervals = []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class ExecutionTimeoutError extends Error {
|
||||||
|
public readonly name = "ExecutionTimeoutError"
|
||||||
|
}
|
||||||
|
|
||||||
export class ExecutionTimeTracker {
|
export class ExecutionTimeTracker {
|
||||||
static withLimit(limitMs: number) {
|
static withLimit(limitMs: number) {
|
||||||
return new ExecutionTimeTracker(limitMs)
|
return new ExecutionTimeTracker(limitMs)
|
||||||
|
@ -31,6 +35,7 @@ export class ExecutionTimeTracker {
|
||||||
private totalTimeMs = 0
|
private totalTimeMs = 0
|
||||||
|
|
||||||
track<T>(f: () => T): T {
|
track<T>(f: () => T): T {
|
||||||
|
this.checkLimit()
|
||||||
const [startSeconds, startNanoseconds] = process.hrtime()
|
const [startSeconds, startNanoseconds] = process.hrtime()
|
||||||
const startMs = startSeconds * 1000 + startNanoseconds / 1e6
|
const startMs = startSeconds * 1000 + startNanoseconds / 1e6
|
||||||
try {
|
try {
|
||||||
|
@ -38,15 +43,14 @@ export class ExecutionTimeTracker {
|
||||||
} finally {
|
} finally {
|
||||||
const [endSeconds, endNanoseconds] = process.hrtime()
|
const [endSeconds, endNanoseconds] = process.hrtime()
|
||||||
const endMs = endSeconds * 1000 + endNanoseconds / 1e6
|
const endMs = endSeconds * 1000 + endNanoseconds / 1e6
|
||||||
this.increment(endMs - startMs)
|
this.totalTimeMs += endMs - startMs
|
||||||
|
this.checkLimit()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private increment(byMs: number) {
|
private checkLimit() {
|
||||||
this.totalTimeMs += byMs
|
|
||||||
|
|
||||||
if (this.totalTimeMs > this.limitMs) {
|
if (this.totalTimeMs > this.limitMs) {
|
||||||
throw new Error(
|
throw new ExecutionTimeoutError(
|
||||||
`Execution time limit of ${this.limitMs}ms exceeded: ${this.totalTimeMs}ms`
|
`Execution time limit of ${this.limitMs}ms exceeded: ${this.totalTimeMs}ms`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -2086,4 +2086,108 @@ describe.each([
|
||||||
expect(row.formula).toBe(relatedRow.name)
|
expect(row.formula).toBe(relatedRow.name)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("Formula JS protection", () => {
|
||||||
|
it("should time out JS execution if a single cell takes too long", async () => {
|
||||||
|
await config.withEnv(
|
||||||
|
{ JS_PER_EXECUTION_TIME_LIMIT_MS: 100 },
|
||||||
|
async () => {
|
||||||
|
const js = Buffer.from(
|
||||||
|
`
|
||||||
|
let i = 0;
|
||||||
|
while (true) {
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
return i;
|
||||||
|
`
|
||||||
|
).toString("base64")
|
||||||
|
|
||||||
|
const table = await config.createTable({
|
||||||
|
name: "table",
|
||||||
|
type: "table",
|
||||||
|
schema: {
|
||||||
|
text: {
|
||||||
|
name: "text",
|
||||||
|
type: FieldType.STRING,
|
||||||
|
},
|
||||||
|
formula: {
|
||||||
|
name: "formula",
|
||||||
|
type: FieldType.FORMULA,
|
||||||
|
formula: `{{ js "${js}"}}`,
|
||||||
|
formulaType: FormulaTypes.DYNAMIC,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
await config.api.row.save(table._id!, { text: "foo" })
|
||||||
|
const { rows } = await config.api.row.search(table._id!)
|
||||||
|
expect(rows).toHaveLength(1)
|
||||||
|
const row = rows[0]
|
||||||
|
expect(row.text).toBe("foo")
|
||||||
|
expect(row.formula).toBe("Timed out while executing JS")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should time out JS execution if a multiple cells take too long", async () => {
|
||||||
|
await config.withEnv(
|
||||||
|
{
|
||||||
|
JS_PER_EXECUTION_TIME_LIMIT_MS: 100,
|
||||||
|
JS_PER_REQUEST_TIME_LIMIT_MS: 200,
|
||||||
|
},
|
||||||
|
async () => {
|
||||||
|
const js = Buffer.from(
|
||||||
|
`
|
||||||
|
let i = 0;
|
||||||
|
while (true) {
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
return i;
|
||||||
|
`
|
||||||
|
).toString("base64")
|
||||||
|
|
||||||
|
const table = await config.createTable({
|
||||||
|
name: "table",
|
||||||
|
type: "table",
|
||||||
|
schema: {
|
||||||
|
text: {
|
||||||
|
name: "text",
|
||||||
|
type: FieldType.STRING,
|
||||||
|
},
|
||||||
|
formula: {
|
||||||
|
name: "formula",
|
||||||
|
type: FieldType.FORMULA,
|
||||||
|
formula: `{{ js "${js}"}}`,
|
||||||
|
formulaType: FormulaTypes.DYNAMIC,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
for (let i = 0; i < 10; i++) {
|
||||||
|
await config.api.row.save(table._id!, { text: "foo" })
|
||||||
|
}
|
||||||
|
|
||||||
|
const { rows } = await config.api.row.search(table._id!)
|
||||||
|
expect(rows).toHaveLength(10)
|
||||||
|
|
||||||
|
let i = 0
|
||||||
|
for (; i < 10; i++) {
|
||||||
|
const row = rows[i]
|
||||||
|
if (row.formula !== "Timed out while executing JS") {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(i).toBeGreaterThan(0)
|
||||||
|
expect(i).toBeLessThan(5)
|
||||||
|
|
||||||
|
for (; i < 10; i++) {
|
||||||
|
const row = rows[i]
|
||||||
|
expect(row.text).toBe("foo")
|
||||||
|
expect(row.formula).toBe("Request JS execution limit hit")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -8,12 +8,16 @@ type TrackerFn = <T>(f: () => T) => T
|
||||||
export function init() {
|
export function init() {
|
||||||
setJSRunner((js: string, ctx: vm.Context) => {
|
setJSRunner((js: string, ctx: vm.Context) => {
|
||||||
const perRequestLimit = env.JS_PER_REQUEST_TIME_LIMIT_MS
|
const perRequestLimit = env.JS_PER_REQUEST_TIME_LIMIT_MS
|
||||||
const bbCtx = context.getCurrentContext()
|
|
||||||
let track: TrackerFn = f => f()
|
let track: TrackerFn = f => f()
|
||||||
if (perRequestLimit && bbCtx && !bbCtx.jsExecutionTracker) {
|
if (perRequestLimit) {
|
||||||
bbCtx.jsExecutionTracker =
|
const bbCtx = context.getCurrentContext()
|
||||||
timers.ExecutionTimeTracker.withLimit(perRequestLimit)
|
if (bbCtx) {
|
||||||
track = bbCtx.jsExecutionTracker.track
|
if (!bbCtx.jsExecutionTracker) {
|
||||||
|
bbCtx.jsExecutionTracker =
|
||||||
|
timers.ExecutionTimeTracker.withLimit(perRequestLimit)
|
||||||
|
}
|
||||||
|
track = bbCtx.jsExecutionTracker.track.bind(bbCtx.jsExecutionTracker)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx = {
|
ctx = {
|
||||||
|
|
|
@ -56,10 +56,12 @@ module.exports.processJS = (handlebars, context) => {
|
||||||
const res = { data: runJS(js, sandboxContext) }
|
const res = { data: runJS(js, sandboxContext) }
|
||||||
return `{{${LITERAL_MARKER} js_result-${JSON.stringify(res)}}}`
|
return `{{${LITERAL_MARKER} js_result-${JSON.stringify(res)}}}`
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(`JS error: ${typeof error} ${JSON.stringify(error)}`)
|
|
||||||
if (error.code === "ERR_SCRIPT_EXECUTION_TIMEOUT") {
|
if (error.code === "ERR_SCRIPT_EXECUTION_TIMEOUT") {
|
||||||
return "Timed out while executing JS"
|
return "Timed out while executing JS"
|
||||||
}
|
}
|
||||||
|
if (error.name === "ExecutionTimeoutError") {
|
||||||
|
return "Request JS execution limit hit"
|
||||||
|
}
|
||||||
return "Error while executing JS"
|
return "Error while executing JS"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue