Skip to content

Commit

Permalink
feat: use aws-lambda-ric UserFunction.js (#1534)
Browse files Browse the repository at this point in the history
  • Loading branch information
dnalborczyk authored Aug 10, 2022
1 parent c9c3804 commit de92b9e
Show file tree
Hide file tree
Showing 34 changed files with 868 additions and 77 deletions.
2 changes: 2 additions & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
**/dist
**/node_modules

src/lambda/handler-runner/in-process-runner/aws-lambda-ric/UserFunction.js
34 changes: 17 additions & 17 deletions src/lambda/__tests__/LambdaFunction.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,52 +27,52 @@ describe('LambdaFunction', () => {
{
description: 'should return result when handler is context.done',
expected: 'foo',
handler: 'fixtures/lambdaFunction.fixture.contextDoneHandler',
handler: 'fixtures/lambdaFunction-fixture.contextDoneHandler',
},
{
description:
'should return result when handler is context.done which is deferred',
expected: 'foo',
handler: 'fixtures/lambdaFunction.fixture.contextDoneHandlerDeferred',
handler: 'fixtures/lambdaFunction-fixture.contextDoneHandlerDeferred',
},
{
description: 'should return result when handler is context.succeed',
expected: 'foo',
handler: 'fixtures/lambdaFunction.fixture.contextSucceedHandler',
handler: 'fixtures/lambdaFunction-fixture.contextSucceedHandler',
},
{
description:
'should return result when handler is context.succeed which is deferred',
expected: 'foo',
handler:
'fixtures/lambdaFunction.fixture.contextSucceedHandlerDeferred',
'fixtures/lambdaFunction-fixture.contextSucceedHandlerDeferred',
},
{
description: 'should return result when handler is a callback',
expected: 'foo',
handler: 'fixtures/lambdaFunction.fixture.callbackHandler',
handler: 'fixtures/lambdaFunction-fixture.callbackHandler',
},
{
description:
'should return result when handler is a callback which is deferred',
expected: 'foo',
handler: 'fixtures/lambdaFunction.fixture.callbackHandlerDeferred',
handler: 'fixtures/lambdaFunction-fixture.callbackHandlerDeferred',
},
{
description: 'should return result when handler returns a promise',
expected: 'foo',
handler: 'fixtures/lambdaFunction.fixture.promiseHandler',
handler: 'fixtures/lambdaFunction-fixture.promiseHandler',
},
{
description:
'should return result when handler returns a promise which is deferred',
expected: 'foo',
handler: 'fixtures/lambdaFunction.fixture.promiseHandlerDeferred',
handler: 'fixtures/lambdaFunction-fixture.promiseHandlerDeferred',
},
{
description: 'should return result when handler is an async function',
expected: 'foo',
handler: 'fixtures/lambdaFunction.fixture.asyncFunctionHandler',
handler: 'fixtures/lambdaFunction-fixture.asyncFunctionHandler',
},
// NOTE: mix and matching of callbacks and promises is not recommended,
// nonetheless, we test some of the behaviour to match AWS execution precedence
Expand All @@ -81,33 +81,33 @@ describe('LambdaFunction', () => {
'should return result when handler returns a callback but defines a callback parameter',
expected: 'Hello Promise!',
handler:
'fixtures/lambdaFunction.fixture.promiseWithDefinedCallbackHandler',
'fixtures/lambdaFunction-fixture.promiseWithDefinedCallbackHandler',
},
{
description:
'should return result when handler calls context.succeed and context.done',
expected: 'Hello Context.succeed!',
handler:
'fixtures/lambdaFunction.fixture.contextSucceedWithContextDoneHandler',
'fixtures/lambdaFunction-fixture.contextSucceedWithContextDoneHandler',
},
{
description:
'should return result when handler calls callback and context.done',
expected: 'Hello Callback!',
handler:
'fixtures/lambdaFunction.fixture.callbackWithContextDoneHandler',
'fixtures/lambdaFunction-fixture.callbackWithContextDoneHandler',
},
{
description:
'should return result when handler calls callback and returns Promise',
expected: 'Hello Callback!',
handler: 'fixtures/lambdaFunction.fixture.callbackWithPromiseHandler',
handler: 'fixtures/lambdaFunction-fixture.callbackWithPromiseHandler',
},
{
description:
'should return result when handler calls callback inside returned Promise',
expected: 'Hello Callback!',
handler: 'fixtures/lambdaFunction.fixture.callbackInsidePromiseHandler',
handler: 'fixtures/lambdaFunction-fixture.callbackInsidePromiseHandler',
},
].forEach(({ description, expected, handler }) => {
it(description, async () => {
Expand All @@ -132,7 +132,7 @@ describe('LambdaFunction', () => {

it('should pass remaining time to LambdaContext', async () => {
const functionDefinition = {
handler: 'fixtures/lambdaFunction.fixture.remainingExecutionTimeHandler',
handler: 'fixtures/lambdaFunction-fixture.remainingExecutionTimeHandler',
}
const options = {}
const lambdaFunction = new LambdaFunction(
Expand All @@ -152,7 +152,7 @@ describe('LambdaFunction', () => {

it.skip('should use default lambda timeout when timeout is not provided', async () => {
const functionDefinition = {
handler: 'fixtures/lambdaFunction.fixture.defaultTimeoutHandler',
handler: 'fixtures/lambdaFunction-fixture.defaultTimeoutHandler',
}
const options = {}
const lambdaFunction = new LambdaFunction(
Expand All @@ -175,7 +175,7 @@ describe('LambdaFunction', () => {
// // might run flaky (unreliable)
// test('executionTimeInMillis should return execution time', async () => {
// const functionDefinition = {
// handler: 'fixtures/lambdaFunction.fixture.executionTimeInMillisHandler',
// handler: 'fixtures/lambdaFunction-fixture.executionTimeInMillisHandler',
// }
// const options = {}
// const lambdaFunction = new LambdaFunction(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export default class LambdaFunctionThatReturnsJSONObject {
serverless = {
config: {
serverlessPath: '',
servicePath: resolve(__dirname),
servicePath: resolve(__dirname, '../..'),
},
service: {
provider: {
Expand All @@ -27,8 +27,7 @@ export default class LambdaFunctionThatReturnsJSONObject {

getByFunctionName(functionName) {
const functionDefinition = {
handler:
'../../fixtures/lambdaFunction.fixture.asyncFunctionHandlerObject',
handler: 'fixtures/lambdaFunction-fixture.asyncFunctionHandlerObject',
}

this.#lambdaFunction = new LambdaFunction(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export default class LambdaFunctionThatReturnsNativeString {
serverless = {
config: {
serverlessPath: '',
servicePath: resolve(__dirname),
servicePath: resolve(__dirname, '../..'),
},
service: {
provider: {
Expand All @@ -27,7 +27,7 @@ export default class LambdaFunctionThatReturnsNativeString {

getByFunctionName(functionName) {
const functionDefinition = {
handler: '../../fixtures/lambdaFunction.fixture.asyncFunctionHandler',
handler: 'fixtures/lambdaFunction-fixture.asyncFunctionHandler',
}

this.#lambdaFunction = new LambdaFunction(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import assert from 'node:assert'
import InvocationsController from '../../../routes/invocations/InvocationsController.js'
import LambdaFunctionThatReturnsJSONObject from '../../fixtures/Lambda/LambdaFunctionThatReturnsJSONObject.fixture.js'
import LambdaFunctionThatReturnsNativeString from '../../fixtures/Lambda/LambdaFunctionThatReturnsNativeString.fixture.js'
import LambdaFunctionThatReturnsJSONObject from '../../fixtures/Lambda/LambdaFunctionThatReturnsJSONObject-fixture.js'
import LambdaFunctionThatReturnsNativeString from '../../fixtures/Lambda/LambdaFunctionThatReturnsNativeString-fixture.js'

describe('InvocationController', () => {
const functionName = 'foo'
Expand Down
8 changes: 4 additions & 4 deletions src/lambda/handler-runner/HandlerRunner.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,10 @@ export default class HandlerRunner {
async #loadRunner() {
const { useChildProcesses, useDocker, useInProcess } = this.#options

const { functionKey, handlerName, handlerPath, runtime, timeout } =
const { functionKey, handler, runtime, servicePath, timeout } =
this.#funOptions

log.debug(`Loading handler... (${handlerPath})`)
log.debug(`Loading handler... (${handler})`)

if (useDocker) {
// https://github.com/lambci/docker-lambda/issues/329
Expand Down Expand Up @@ -73,10 +73,10 @@ export default class HandlerRunner {

return new InProcessRunner(
functionKey,
handlerPath,
handlerName,
this.#env,
timeout,
handler,
servicePath,
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,19 @@ export default class ChildProcessRunner {

#functionKey = null

#handlerName = null
#handler = null

#handlerPath = null
#servicePath = null

#timeout = null

constructor(funOptions, env) {
const { functionKey, handlerName, handlerPath, timeout } = funOptions
const { functionKey, handler, servicePath, timeout } = funOptions

this.#env = env
this.#functionKey = functionKey
this.#handlerName = handlerName
this.#handlerPath = handlerPath
this.#handler = handler
this.#servicePath = servicePath
this.#timeout = timeout
}

Expand All @@ -34,7 +34,7 @@ export default class ChildProcessRunner {
async run(event, context) {
const childProcess = execaNode(
childProcessHelperPath,
[this.#functionKey, this.#handlerName, this.#handlerPath],
[this.#functionKey, this.#handler, this.#servicePath],
{
env: this.#env,
stdio: 'inherit',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,18 @@ process.on('uncaughtException', (err) => {
})
})

const [, , functionKey, handlerName, handlerPath] = argv
const [, , functionKey, handler, servicePath] = argv

process.on('message', async (messageData) => {
const { context, event, timeout } = messageData

// TODO we could probably cache this in the module scope?
const inProcessRunner = new InProcessRunner(
functionKey,
handlerPath,
handlerName,
process.env,
timeout,
handler,
servicePath,
)

const result = await inProcessRunner.run(event, context)
Expand Down
41 changes: 7 additions & 34 deletions src/lambda/handler-runner/in-process-runner/InProcessRunner.js
Original file line number Diff line number Diff line change
@@ -1,29 +1,26 @@
import { createRequire } from 'node:module'
import { performance } from 'node:perf_hooks'
import process from 'node:process'
import { log } from '@serverless/utils/log.js'
import { load } from './aws-lambda-ric/UserFunction.js'

const { floor } = Math
const { assign } = Object

const require = createRequire(import.meta.url)

export default class InProcessRunner {
#env = null

#functionKey = null

#handlerName = null
#handler = null

#handlerPath = null
#servicePath = null

#timeout = null

constructor(functionKey, handlerPath, handlerName, env, timeout) {
constructor(functionKey, env, timeout, handler, servicePath) {
this.#env = env
this.#functionKey = functionKey
this.#handlerName = handlerName
this.#handlerPath = handlerPath
this.#handler = handler
this.#servicePath = servicePath
this.#timeout = timeout
}

Expand All @@ -32,37 +29,13 @@ export default class InProcessRunner {
cleanup() {}

async run(event, context) {
// check if the handler module path exists
if (!require.resolve(this.#handlerPath)) {
throw new Error(
`Could not find handler module '${this.#handlerPath}' for function '${
this.#functionKey
}'.`,
)
}

// process.env should be available in the handler module scope as well as in the handler function scope
// NOTE: Don't use Object spread (...) here!
// otherwise the values of the attached props are not coerced to a string
// e.g. process.env.foo = 1 should be coerced to '1' (string)
assign(process.env, this.#env)

let handler

try {
// eslint-disable-next-line import/no-dynamic-require
;({ [this.#handlerName]: handler } = require(this.#handlerPath))
} catch (err) {
log.error(err)
}

if (typeof handler !== 'function') {
throw new Error(
`offline: handler '${this.#handlerName}' in ${
this.#handlerPath
} is not a function`,
)
}
const handler = await load(this.#servicePath, this.#handler)

let callback

Expand Down
Loading

0 comments on commit de92b9e

Please sign in to comment.