Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for Node v14.x #25

Open
wants to merge 15 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions v14.x/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
layer.zip
layer
test
15 changes: 15 additions & 0 deletions v14.x/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
FROM lambci/lambda-base:build

COPY bootstrap.c bootstrap.js package.json /opt/

ARG NODE_VERSION

RUN curl -sSL https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-x64.tar.xz | \
tar -xJ -C /opt --strip-components 1 -- node-v${NODE_VERSION}-linux-x64/bin/node && \
strip /opt/bin/node

RUN cd /opt && \
export NODE_MAJOR=$(echo $NODE_VERSION | awk -F. '{print "\""$1"\""}') && \
clang -Wall -Werror -s -O2 -D NODE_MAJOR="$NODE_MAJOR" -o bootstrap bootstrap.c && \
rm bootstrap.c && \
zip -yr /tmp/layer.zip .
42 changes: 42 additions & 0 deletions v14.x/bootstrap.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <unistd.h>

#ifndef NODE_MAJOR
#error Must pass NODE_MAJOR to the compiler (eg "10")
#define NODE_MAJOR ""
#endif

#define AWS_EXECUTION_ENV "AWS_Lambda_nodejs" NODE_MAJOR "_lambci"
#define NODE_PATH "/opt/nodejs/node" NODE_MAJOR "/node_modules:" \
"/opt/nodejs/node_modules:" \
"/var/runtime/node_modules:" \
"/var/runtime:" \
"/var/task"
#define MIN_MEM_SIZE 128
#define ARG_BUF_SIZE 32

int main(void) {
setenv("AWS_EXECUTION_ENV", AWS_EXECUTION_ENV, true);
setenv("NODE_PATH", NODE_PATH, true);

const char *mem_size_str = getenv("AWS_LAMBDA_FUNCTION_MEMORY_SIZE");
int mem_size = mem_size_str != NULL ? atoi(mem_size_str) : MIN_MEM_SIZE;

char max_semi_space_size[ARG_BUF_SIZE];
snprintf(max_semi_space_size, ARG_BUF_SIZE, "--max-semi-space-size=%d", mem_size * 5 / 100);

char max_old_space_size[ARG_BUF_SIZE];
snprintf(max_old_space_size, ARG_BUF_SIZE, "--max-old-space-size=%d", mem_size * 90 / 100);

execv("/opt/bin/node", (char *[]){
"node",
"--expose-gc",
max_semi_space_size,
max_old_space_size,
"/opt/bootstrap.js",
NULL});
perror("Could not execv");
return EXIT_FAILURE;
}
208 changes: 208 additions & 0 deletions v14.x/bootstrap.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
import http from 'http'

const RUNTIME_PATH = '/2018-06-01/runtime'

const CALLBACK_USED = Symbol('CALLBACK_USED')

const {
AWS_LAMBDA_FUNCTION_NAME,
AWS_LAMBDA_FUNCTION_VERSION,
AWS_LAMBDA_FUNCTION_MEMORY_SIZE,
AWS_LAMBDA_LOG_GROUP_NAME,
AWS_LAMBDA_LOG_STREAM_NAME,
LAMBDA_TASK_ROOT,
_HANDLER,
AWS_LAMBDA_RUNTIME_API,
} = process.env

const [HOST, PORT] = AWS_LAMBDA_RUNTIME_API.split(':')

start()

async function start() {
let handler
try {
handler = await getHandler()
} catch (e) {
await initError(e)
return process.exit(1)
}
tryProcessEvents(handler)
}

async function tryProcessEvents(handler) {
try {
await processEvents(handler)
} catch (e) {
console.error(e)
return process.exit(1)
}
}

async function processEvents(handler) {
while (true) {
const { event, context } = await nextInvocation()

let result
try {
result = await handler(event, context)
} catch (e) {
await invokeError(e, context)
continue
}
const callbackUsed = context[CALLBACK_USED]

await invokeResponse(result, context)

if (callbackUsed && context.callbackWaitsForEmptyEventLoop) {
return process.prependOnceListener('beforeExit', () => tryProcessEvents(handler))
}
}
}

function initError(err) {
return postError(`${RUNTIME_PATH}/init/error`, err)
}

async function nextInvocation() {
const res = await request({ path: `${RUNTIME_PATH}/invocation/next` })

if (res.statusCode !== 200) {
throw new Error(`Unexpected /invocation/next response: ${JSON.stringify(res)}`)
}

if (res.headers['lambda-runtime-trace-id']) {
process.env._X_AMZN_TRACE_ID = res.headers['lambda-runtime-trace-id']
} else {
delete process.env._X_AMZN_TRACE_ID
}

const deadlineMs = +res.headers['lambda-runtime-deadline-ms']

const context = {
awsRequestId: res.headers['lambda-runtime-aws-request-id'],
invokedFunctionArn: res.headers['lambda-runtime-invoked-function-arn'],
logGroupName: AWS_LAMBDA_LOG_GROUP_NAME,
logStreamName: AWS_LAMBDA_LOG_STREAM_NAME,
functionName: AWS_LAMBDA_FUNCTION_NAME,
functionVersion: AWS_LAMBDA_FUNCTION_VERSION,
memoryLimitInMB: AWS_LAMBDA_FUNCTION_MEMORY_SIZE,
getRemainingTimeInMillis: () => deadlineMs - Date.now(),
callbackWaitsForEmptyEventLoop: true,
}

if (res.headers['lambda-runtime-client-context']) {
context.clientContext = JSON.parse(res.headers['lambda-runtime-client-context'])
}

if (res.headers['lambda-runtime-cognito-identity']) {
context.identity = JSON.parse(res.headers['lambda-runtime-cognito-identity'])
}

const event = JSON.parse(res.body)

return { event, context }
}

async function invokeResponse(result, context) {
const res = await request({
method: 'POST',
path: `${RUNTIME_PATH}/invocation/${context.awsRequestId}/response`,
body: JSON.stringify(result === undefined ? null : result),
})
if (res.statusCode !== 202) {
throw new Error(`Unexpected /invocation/response response: ${JSON.stringify(res)}`)
}
}

function invokeError(err, context) {
return postError(`${RUNTIME_PATH}/invocation/${context.awsRequestId}/error`, err)
}

async function postError(path, err) {
const lambdaErr = toLambdaErr(err)
const res = await request({
method: 'POST',
path,
headers: {
'Content-Type': 'application/json',
'Lambda-Runtime-Function-Error-Type': lambdaErr.errorType,
},
body: JSON.stringify(lambdaErr),
})
if (res.statusCode !== 202) {
throw new Error(`Unexpected ${path} response: ${JSON.stringify(res)}`)
}
}

async function getHandler() {
const appParts = _HANDLER.split('.')

if (appParts.length !== 2) {
throw new Error(`Bad handler ${_HANDLER}`)
}

const [modulePath, handlerName] = appParts

// Let any errors here be thrown as-is to aid debugging
const importPath = `${LAMBDA_TASK_ROOT}/${modulePath}.js`
const app = await new Promise((res, rej) => import(importPath).then(res).catch(rej))
sodiray marked this conversation as resolved.
Show resolved Hide resolved

const userHandler = app[handlerName]

if (userHandler == null) {
throw new Error(`Handler '${handlerName}' missing on module '${modulePath}'`)
} else if (typeof userHandler !== 'function') {
throw new Error(`Handler '${handlerName}' from '${modulePath}' is not a function`)
}

return (event, context) => new Promise((resolve, reject) => {
context.succeed = resolve
context.fail = reject
context.done = (err, data) => err ? reject(err) : resolve(data)

const callback = (err, data) => {
context[CALLBACK_USED] = true
context.done(err, data)
}

let result
try {
result = userHandler(event, context, callback)
} catch (e) {
return reject(e)
}
if (result != null && typeof result.then === 'function') {
result.then(resolve, reject)
}
})
}

function request(options) {
options.host = HOST
options.port = PORT

return new Promise((resolve, reject) => {
const req = http.request(options, res => {
const bufs = []
res.on('data', data => bufs.push(data))
res.on('end', () => resolve({
statusCode: res.statusCode,
headers: res.headers,
body: Buffer.concat(bufs).toString(),
}))
res.on('error', reject)
})
req.on('error', reject)
req.end(options.body)
})
}

function toLambdaErr(err) {
const { name, message, stack } = err
return {
errorType: name || typeof err,
errorMessage: message || ('' + err),
stackTrace: (stack || '').split('\n').slice(1),
}
}
6 changes: 6 additions & 0 deletions v14.x/build.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#!/bin/sh

. ./config.sh

docker build --build-arg NODE_VERSION -t node-provided-lambda-v14.x .
docker run --rm -v "$PWD":/app node-provided-lambda-v14.x cp /tmp/layer.zip /app/
11 changes: 11 additions & 0 deletions v14.x/check.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#!/bin/bash

. ./config.sh

REGIONS="$(aws ssm get-parameters-by-path --path /aws/service/global-infrastructure/services/lambda/regions \
--query 'Parameters[].Value' --output text | tr '[:blank:]' '\n' | grep -v -e ^cn- -e ^us-gov- | sort -r)"

for region in $REGIONS; do
aws lambda list-layer-versions --region $region --layer-name $LAYER_NAME \
--query 'LayerVersions[*].[LayerVersionArn]' --output text
done
2 changes: 2 additions & 0 deletions v14.x/config.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export LAYER_NAME=nodejs14
export NODE_VERSION=14.3.0
sodiray marked this conversation as resolved.
Show resolved Hide resolved
Binary file added v14.x/layer.zip
Binary file not shown.
5 changes: 5 additions & 0 deletions v14.x/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"name": "node-custom-lambda-v14.x",
"version": "1.0.0",
"type": "module"
}
25 changes: 25 additions & 0 deletions v14.x/publish.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#!/bin/bash

. ./config.sh

DESCRIPTION="Node.js v${NODE_VERSION} custom runtime"
FILENAME=${LAYER_NAME}-${NODE_VERSION}.zip

REGIONS="$(aws ssm get-parameters-by-path --path /aws/service/global-infrastructure/services/lambda/regions \
--query 'Parameters[].Value' --output text | tr '[:blank:]' '\n' | grep -v -e ^cn- -e ^us-gov- -e ^ap-northeast-3 | sort -r)"

aws s3api put-object --bucket lambci --key layers/${FILENAME} --body layer.zip

for region in $REGIONS; do
aws s3api copy-object --region $region --copy-source lambci/layers/${FILENAME} \
--bucket lambci-${region} --key layers/${FILENAME} && \
aws lambda add-layer-version-permission --region $region --layer-name $LAYER_NAME \
--statement-id sid1 --action lambda:GetLayerVersion --principal '*' \
--version-number $(aws lambda publish-layer-version --region $region --layer-name $LAYER_NAME \
--content S3Bucket=lambci-${region},S3Key=layers/${FILENAME} \
--description "$DESCRIPTION" --query Version --output text) &
done

for job in $(jobs -p); do
wait $job
done
22 changes: 22 additions & 0 deletions v14.x/test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#!/bin/sh

rm -rf layer && unzip layer.zip -d layer

cd test

npm ci

# Create zipfile for uploading to Lambda – we don't use this here
rm -f lambda.zip && zip -qyr lambda.zip index.js node_modules

docker run --rm -v "$PWD":/var/task -v "$PWD"/../layer:/opt lambci/lambda:provided index.handler

docker run --rm -v "$PWD":/var/task -v "$PWD"/../layer:/opt lambci/lambda:provided index.handler2

docker run --rm -v "$PWD":/var/task -v "$PWD"/../layer:/opt lambci/lambda:provided index.handler3

docker run --rm -v "$PWD":/var/task -v "$PWD"/../layer:/opt lambci/lambda:provided index.handler4

docker run --rm -v "$PWD":/var/task -v "$PWD"/../layer:/opt lambci/lambda:provided index.handler5

docker run --rm -v "$PWD":/var/task -v "$PWD"/../layer:/opt lambci/lambda:provided index.handler6
45 changes: 45 additions & 0 deletions v14.x/test/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Test that global requires work
import aws4 from 'aws4'

const interval = setInterval(console.log, 100, 'ping')

const sleep = async (miliseconds) => await new Promise(res => setTimeout(res, miliseconds))
sodiray marked this conversation as resolved.
Show resolved Hide resolved

// Test top-level await works
await sleep(1000)

export const handler = async (event, context) => {
console.log(process.version)
console.log(process.execPath)
console.log(process.execArgv)
console.log(process.argv)
console.log(process.cwd())
console.log(process.env)
console.log(event)
console.log(context)
console.log(context.getRemainingTimeInMillis())
console.log(aws4)
return { some: 'obj!' }
}

export const handler2 = (event, context) => {
setTimeout(context.done, 100, null, { some: 'obj!' })
}

export const handler3 = (event, context) => {
setTimeout(context.succeed, 100, { some: 'obj!' })
}

export const handler4 = (event, context) => {
setTimeout(context.fail, 100, new Error('This error should be logged'))
}

export const handler5 = (event, context, cb) => {
setTimeout(cb, 100, null, { some: 'obj!' })
setTimeout(clearInterval, 100, interval)
}

export const handler6 = (event, context, cb) => {
context.callbackWaitsForEmptyEventLoop = false
setTimeout(cb, 100, null, { some: 'obj!' })
}
Loading