AWS Compute Blog

Everything Depends on Context or, The Fine Art of nodejs Coding in AWS Lambda

Tim Wagner Tim Wagner, AWS Lambda General Manager

Quick, what’s wrong with the Lambda code sketch below?

exports.handler = function(event, context) {
    anyAsyncCall(args, function(err, result) {
        if (err) console.log('problem');
        else /* do something with result */;
    });
    context.succeed();
};

If you said the placement of context.succeed, you’re correct – it belongs inside the callback. In general, when you get this wrong your code exits prematurely, after the incorrectly placed context.succeed line, without allowing the callback to run. The same thing happens if you make calls in a loop, often leading to race conditions where some callbacks get “dropped”; the lack of a barrier synchronization forces a too-early exit.

If you test outside of Lambda, these patterns work fine in nodejs, because the default node runtime waits for all tasks to complete before exiting. Context.succeed, context.done, and context.fail however, are more than just bookkeeping – they cause the request to return after the current task completes, even if other tasks remain in the queue. Generally that’s not what you want if those tasks represent incomplete callbacks.

Placement Patterns

Fixing the code in the single callback case is trivial; the code above becomes

exports.handler = function(event, context) {
    anyAsyncCall(args, function(err, result) {
        if (err) console.log('problem');
        else /* do something with result */;
        context.succeed();
    });
};

Dealing with a loop that has an unbounded number of async calls inside it takes more work; here’s one pattern (the asyncAll function) used in Lambda’s test harness blueprint to run a test a given number of iterations:

/**
 * Provides a simple framework for conducting various tests of your Lambda
 * functions. Make sure to include permissions for `lambda:InvokeFunction`
 * and `dynamodb:PutItem` in your execution role!
 */
var AWS = require('aws-sdk');
var doc = require('dynamodb-doc');

var lambda = new AWS.Lambda({ apiVersion: '2015-03-31' });
var dynamo = new doc.DynamoDB();


// Runs a given function X times
var asyncAll = function(opts) {
    var i = -1;
    var next = function() {
        i++;
        if (i === opts.times) {
            opts.done();
            return;
        }
        opts.fn(next, i);
    };
    next();
};


/**
 * Will invoke the given function and write its result to the DynamoDB table
 * `event.resultsTable`. This table must have a hash key string of "testId"
 * and range key number of "iteration". Specify a unique `event.testId` to
 * differentiate each unit test run.
 */
var unit = function(event, context) {
    var lambdaParams = {
        FunctionName: event.function,
        Payload: JSON.stringify(event.event)
    };
    lambda.invoke(lambdaParams, function(err, data) {
        if (err) {
            context.fail(err);
        }
        // Write result to Dynamo
        var dynamoParams = {
            TableName: event.resultsTable,
            Item: {
                testId: event.testId,
                iteration: event.iteration || 0,
                result: data.Payload,
                passed: !JSON.parse(data.Payload).hasOwnProperty('errorMessage')
            }
        };
        dynamo.putItem(dynamoParams, context.done);
    });
};

/**
 * Will invoke the given function asynchronously `event.iterations` times.
 */
var load = function(event, context) {
    var payload = event.event;
    asyncAll({
        times: event.iterations,
        fn: function(next, i) {
            payload.iteration = i;
            var lambdaParams = {
                FunctionName: event.function,
                InvocationType: 'Event',
                Payload: JSON.stringify(payload)
            };
            lambda.invoke(lambdaParams, function(err, data) {
                next();
            });
        },
        done: function() {
            context.succeed('Load test complete');
        }
    });
};


var ops = {
    unit: unit,
    load: load
};

/**
 * Pass the test type (currently either "unit" or "load") as `event.operation`,
 * the name of the Lambda function to test as `event.function`, and the event
 * to invoke this function with as `event.event`.
 *
 * See the individual test methods above for more information about each
 * test type.
 */
exports.handler = function(event, context) {
    if (ops.hasOwnProperty(event.operation)) {
        ops[event.operation](event, context);
    } else {
        context.fail('Unrecognized operation "' + event.operation + '"');
    }
};

The approach above serializes the loop; there are many other approaches, and you can use async or other libraries to help.

Does this matter for Java or other jvm-based languages in AWS Lambda?

The specific issue discussed here – the “side effect” of the placement of a call like context.success on outstanding callbacks – is unique to nodejs. In other languages, such as Java, returning from the thread of control that represents the request ends the request, which is a little easier to reason about and generally matches developer expectations. Any other threads or processes running at the time that request returns get frozen until the next request (assuming the container gets reused; i.e., possibly never), so if you want them to wrap up, you would need to include explicit barrier synchronization before returning, just as you normally would for a server-side request implemented with multiple threads/processes.

In all languages, context also offers useful “environmental” information (like the request id) and methods (like the amount of time remaining).

Why not just let nodejs exit, if its default behavior is fine?

That would require every request to “boot” the runtime…potentially ok in a one-off functional test, but latency would suffer and it would keep high request rates from being cost effective. Check out the post on container reuse for more on this topic.

Can you make this easier?

You bet! We were trying hard to balance simplicity, control, and a self-imposed “don’t hack node” rule when we launched back in November. Fortunately, newer versions of nodejs offer more control over “exiting” behavior, and we’re looking hard at how to make future releases of nodejs within Lambda offer easier to understand semantics without losing the latency and cost benefits of container reuse. Stay tuned!

Until next time, happy Lambda (and contextually successful nodejs) coding!

-Tim
Follow Tim’s Lambda adventures on Twitter