Skip to content

Commit 87e1dd6

Browse files
committed
test: allow invoking sandobxed function multiple times
1 parent 6e83413 commit 87e1dd6

File tree

2 files changed

+150
-35
lines changed

2 files changed

+150
-35
lines changed

tests/utils/fixture.ts

+131-34
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,12 @@ import { env } from 'node:process'
1414
import { fileURLToPath } from 'node:url'
1515
import { v4 } from 'uuid'
1616
import { LocalServer } from './local-server.js'
17-
import { loadFunction, type FunctionInvocationOptions } from './lambda-helpers.mjs'
17+
import {
18+
type InvokeFunctionResult,
19+
loadFunction,
20+
type LoadFunctionOptions,
21+
type FunctionInvocationOptions,
22+
} from './lambda-helpers.mjs'
1823

1924
import { glob } from 'fast-glob'
2025
import {
@@ -405,48 +410,140 @@ export async function invokeEdgeFunction(
405410
})
406411
}
407412

408-
export async function invokeSandboxedFunction(
413+
/**
414+
* Load function in child process and allow for multiple invocations
415+
*/
416+
export async function loadSandboxedFunction(
409417
ctx: FixtureTestContext,
410-
options: Parameters<typeof invokeFunction>[1] = {},
418+
options: LoadFunctionOptions = {},
411419
) {
412-
return new Promise<ReturnType<typeof invokeFunction>>((resolve, reject) => {
413-
const childProcess = spawn(process.execPath, [import.meta.dirname + '/sandbox-child.mjs'], {
414-
stdio: ['pipe', 'pipe', 'pipe', 'ipc'],
415-
cwd: process.cwd(),
416-
})
420+
const childProcess = spawn(process.execPath, [import.meta.dirname + '/sandbox-child.mjs'], {
421+
stdio: ['pipe', 'pipe', 'pipe', 'ipc'],
422+
cwd: join(ctx.functionDist, SERVER_HANDLER_NAME),
423+
env: {
424+
...process.env,
425+
...(options.env || {}),
426+
},
427+
})
417428

418-
childProcess.stdout?.on('data', (data) => {
419-
console.log(data.toString())
420-
})
429+
let isRunning = true
430+
let operationCounter = 1
421431

422-
childProcess.stderr?.on('data', (data) => {
423-
console.error(data.toString())
424-
})
432+
childProcess.stdout?.on('data', (data) => {
433+
console.log(data.toString())
434+
})
425435

426-
childProcess.on('message', (msg: any) => {
427-
if (msg?.action === 'invokeFunctionResult') {
428-
resolve(msg.result)
429-
childProcess.send({ action: 'exit' })
430-
}
431-
})
436+
childProcess.stderr?.on('data', (data) => {
437+
console.error(data.toString())
438+
})
439+
440+
const onGoingOperationsMap = new Map<
441+
number,
442+
{
443+
resolve: (value?: any) => void
444+
reject: (reason?: any) => void
445+
}
446+
>()
447+
448+
function createOperation<T>() {
449+
const operationId = operationCounter
450+
operationCounter += 1
432451

433-
childProcess.on('exit', () => {
434-
reject(new Error('worker exited before returning result'))
452+
let promiseResolve, promiseReject
453+
const promise = new Promise<T>((innerResolve, innerReject) => {
454+
promiseResolve = innerResolve
455+
promiseReject = innerReject
435456
})
436457

458+
function resolve(value: T) {
459+
onGoingOperationsMap.delete(operationId)
460+
promiseResolve?.(value)
461+
}
462+
function reject(reason) {
463+
onGoingOperationsMap.delete(operationId)
464+
promiseReject?.(reason)
465+
}
466+
467+
onGoingOperationsMap.set(operationId, { resolve, reject })
468+
return { operationId, promise, resolve, reject }
469+
}
470+
471+
childProcess.on('exit', () => {
472+
isRunning = false
473+
474+
const error = new Error('worker exited before returning result')
475+
476+
for (const { reject } of onGoingOperationsMap.values()) {
477+
reject(error)
478+
}
479+
})
480+
481+
function exit() {
482+
if (isRunning) {
483+
childProcess.send({ action: 'exit' })
484+
}
485+
}
486+
487+
// make sure to exit the child process when the test is done just in case
488+
ctx.cleanup?.push(async () => exit())
489+
490+
const { promise: loadPromise, resolve: loadResolve } = createOperation<void>()
491+
492+
childProcess.on('message', (msg: any) => {
493+
if (msg?.action === 'invokeFunctionResult') {
494+
onGoingOperationsMap.get(msg.operationId)?.resolve(msg.result)
495+
} else if (msg?.action === 'loadedFunction') {
496+
loadResolve()
497+
}
498+
})
499+
500+
// context object is not serializable so we create serializable object
501+
// containing required properties to invoke lambda
502+
const serializableCtx = {
503+
functionDist: ctx.functionDist,
504+
blobStoreHost: ctx.blobStoreHost,
505+
siteID: ctx.siteID,
506+
deployID: ctx.deployID,
507+
}
508+
509+
childProcess.send({
510+
action: 'loadFunction',
511+
args: [serializableCtx],
512+
})
513+
514+
await loadPromise
515+
516+
function invokeFunction(options: FunctionInvocationOptions): InvokeFunctionResult {
517+
if (!isRunning) {
518+
throw new Error('worker is not running anymore')
519+
}
520+
521+
const { operationId, promise } = createOperation<Awaited<InvokeFunctionResult>>()
522+
437523
childProcess.send({
438524
action: 'invokeFunction',
439-
args: [
440-
// context object is not serializable so we create serializable object
441-
// containing required properties to invoke lambda
442-
{
443-
functionDist: ctx.functionDist,
444-
blobStoreHost: ctx.blobStoreHost,
445-
siteID: ctx.siteID,
446-
deployID: ctx.deployID,
447-
},
448-
options,
449-
],
525+
operationId,
526+
args: [serializableCtx, options],
450527
})
451-
})
528+
529+
return promise
530+
}
531+
532+
return {
533+
invokeFunction,
534+
exit,
535+
}
536+
}
537+
538+
/**
539+
* Load function in child process and execute single invocation
540+
*/
541+
export async function invokeSandboxedFunction(
542+
ctx: FixtureTestContext,
543+
options: FunctionInvocationOptions = {},
544+
) {
545+
const { invokeFunction, exit } = await loadSandboxedFunction(ctx, options)
546+
const result = await invokeFunction(options)
547+
exit()
548+
return result
452549
}

tests/utils/sandbox-child.mjs

+19-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ import { loadFunction } from './lambda-helpers.mjs'
55

66
getLogger().level = 'alert'
77

8+
/**
9+
* @type {import('./lambda-helpers.mjs').InvokeFunction | undefined}
10+
*/
11+
let invokeFunctionImpl
12+
813
process.on(
914
'message',
1015
/**
@@ -13,16 +18,29 @@ process.on(
1318
async (msg) => {
1419
if (msg?.action === 'exit') {
1520
process.exit(0)
21+
} else if (msg?.action === 'loadFunction') {
22+
const [ctx, options] = msg.args
23+
24+
invokeFunctionImpl = await loadFunction(ctx, options)
25+
26+
if (process.send) {
27+
process.send({
28+
action: 'loadedFunction',
29+
})
30+
}
1631
} else if (msg?.action === 'invokeFunction') {
1732
try {
1833
const [ctx, options] = msg.args
1934

20-
const invokeFunctionImpl = await loadFunction(ctx, options)
35+
if (!invokeFunctionImpl) {
36+
throw new Error('Function not loaded')
37+
}
2138
const result = await invokeFunctionImpl(options)
2239

2340
if (process.send) {
2441
process.send({
2542
action: 'invokeFunctionResult',
43+
operationId: msg.operationId,
2644
result,
2745
})
2846
}

0 commit comments

Comments
 (0)