Skip to content

Commit d270820

Browse files
committed
test: allow invoking sandobxed function multiple times
1 parent dc949b1 commit d270820

File tree

2 files changed

+151
-35
lines changed

2 files changed

+151
-35
lines changed

tests/utils/fixture.ts

+132-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 {
@@ -24,6 +29,7 @@ import {
2429
} from '../../src/build/plugin-context.js'
2530
import { BLOB_TOKEN } from './constants.mjs'
2631
import { type FixtureTestContext } from './contexts.js'
32+
// import { createBlobContext } from './helpers.js'
2733
import { setNextVersionInFixture } from './next-version-helpers.mjs'
2834

2935
const bootstrapURL = 'https://edge.netlify.com/bootstrap/index-combined.ts'
@@ -405,48 +411,140 @@ export async function invokeEdgeFunction(
405411
})
406412
}
407413

408-
export async function invokeSandboxedFunction(
414+
/**
415+
* Load function in child process and allow for multiple invocations
416+
*/
417+
export async function loadSandboxedFunction(
409418
ctx: FixtureTestContext,
410-
options: Parameters<typeof invokeFunction>[1] = {},
419+
options: LoadFunctionOptions = {},
411420
) {
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-
})
421+
const childProcess = spawn(process.execPath, [import.meta.dirname + '/sandbox-child.mjs'], {
422+
stdio: ['pipe', 'pipe', 'pipe', 'ipc'],
423+
cwd: join(ctx.functionDist, SERVER_HANDLER_NAME),
424+
env: {
425+
...process.env,
426+
...(options.env || {}),
427+
},
428+
})
417429

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

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

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

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

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

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)