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

Function handlers discussion #14

Closed
mthenw opened this issue Nov 28, 2016 · 18 comments
Closed

Function handlers discussion #14

mthenw opened this issue Nov 28, 2016 · 18 comments

Comments

@mthenw
Copy link
Contributor

mthenw commented Nov 28, 2016

Having a "standard" function handler for serverless function provides following benefits:

  • no need to change code when deploying function to different providers,
  • providing common logic (like e.g. decrypting secrets from KMS),
  • keeping things simple.

Research

AWS Lambda

One type of function handler:

exports.myHandler = function(event, context, callback) {
   ...
}
  • event – AWS Lambda uses this parameter to pass in event data to the handler.
  • context – AWS Lambda uses this parameter to provide your handler the runtime information of the Lambda function that is executing. For more information, see The Context Object (Node.js).
  • callback – You can use the optional callback to return information to the caller, otherwise return value is null. For more information, see Using the Callback Parameter.

In case of API Gateway event specified structure and specified structure is expected in callback.

http://docs.aws.amazon.com/lambda/latest/dg/nodejs-prog-model-handler.html

Google Cloud Functions

Two types of function handler, one for HTTP functions, another for background functions. HTTP function handler accepts express-like arguments (req & res).

exports.helloHttp = function helloHttp (req, res) {
  res.send(`Hello ${req.body.name || 'World'}!`);
};

https://cloud.google.com/functions/docs/writing/http

Background function handler accepts event and callback arguments.

exports.helloBackground = function helloBackground (event, callback) {
  callback(null, `Hello ${event.data.name || 'World'}!`);
};

https://cloud.google.com/functions/docs/writing/background

Azure Function

One type of function handler:

module.exports = function(context) {
    // function logic goes here :)
};

context object differes between regular functions and functions triggered by HTTP request.

In case of regular functions context object has a bindings property with myInput & myOutput which can be used for getting input args and setting result.

In case of HTTP func there are req & res objects under context.

https://docs.microsoft.com/en-us/azure/azure-functions/functions-reference-node

Proposition

We should provide two handlers. One generic handler and one for HTTP triggered functions.

Generic handler

In case of generic handler I would follow AWS Lambda definition. But as we want to abstract provider specific stuff, context object would be different that context object from AWS Lambda. Still, raw AWS Lambda context object should be accessible under e.g. AWSContext key in stdlib's context. As context object in most case it not needed in handler logic (and it doesn't occur in GCF) it's an optional argument.

handler(function(event, [context], callback){}) (or λ(...))

Arguments:

  • function(event, [context], callback) - hander function. Function accepts following arguments:
    • event - any type - event payload,
    • context (optional) - object - additional info
    • callback - function - callback function result callback(err, result).
const stdlib = require('@serverless/stdlib')();

exports.userFunc = stdlib.handler((event, context, callback) => {
  callback(new Error('not implemented'))  
});

// or

exports.userFunc = stdlib.λ((event, context, callback) => {
  callback(new Error('not implemented'))
});

HTTP handler

http is a handler for HTTP triggered functions. This handler takes function that accepts two arguments (express-like req & res)

http(function(event, [context], callback){})

Arguments:

Example:

const stdlib = require('@serverless/stdlib')();

exports.userFunc = stdlib.http((req, res) => {
  res.status(200).end();
});

cc @nikgraf @pmuens @ac360 @eahefnawy @DavidWells

@mthenw mthenw modified the milestones: 1.0, 0.1 Nov 28, 2016
@eahefnawy
Copy link
Member

eahefnawy commented Nov 28, 2016

Awesome! super clear write up @mthenw 🙌 ... Another benefit for us is that we'll have some access to user's code => awesome for tracking!

We should provide two handlers. One generic handler and one for HTTP triggered functions.

why do we treat http events differently than other event sources?

In case of generic handler I would follow AWS Lambda definition.

Any reason why you prefer lambda definition? it seems the most complex and maybe we can simplify things a bit more. For example Azure functions look very simple with that single context param. Maybe we should stick to that and just have one argument that has everything the user needs? doesn't even need to be called context.

...stdlib.λ(...

LOVE how short & simple that looks ... but don't you think it looks a bit "Lambda-ish" ... I mean sure AWS dont really have copy rights over the symbol, but in the mind of many developers, we'd be referring to all provider functions as "Lambda".

Also, I'm on the fence with stdlib as well, imo from branding perspective, the best way to position this abstraction is: Serverless Function: Single function signature on top of all provider functions:

// not sure if the keyword "function" is reserved and can't be used as key though
exports.userFunc = serverless.function((event, context, callback) => {
  callback(new Error('not implemented'))
});

// or
exports.userFunc = serverless.fn((event, context, callback) => {
  callback(new Error('not implemented'))
});

// or
exports.userFunc = sl.fn((event, context, callback) => {
  callback(new Error('not implemented'))
});

// or ... this is <3
exports.userFunc = sl.fn((e, ctx, cb) => cb(new Error('not implemented')));

How does this look?

Overall, no matter how we end up doing this, I think it'll be a great user experience! Awesome stuff! 💯

@eahefnawy
Copy link
Member

Two more notes:

  • The word handler is also AWS specific. But I guess it depends on how we call it in serverless.yml. But I'm really hoping we find a more generic name, and so far I'm liking Serverless Function
  • So far we were able to support all runtimes that AWS supports because we don't really touch the handler code. Any ideas how we'll be able to accomplish this abstraction with all supported runtimes in all providers? Also what if a certain runtime is supported in single provider, but not the others?

@mthenw
Copy link
Contributor Author

mthenw commented Nov 28, 2016

@eahefnawy

why do we treat http events differently than other event sources?

UX mainly. req/res pattern is well known and it just fits better this use case. It's much easier to use res for sending response than constructing object in a specified schema (as it's right now with Lambda + APIG)

Any reason why you prefer lambda definition? it seems the most complex and maybe we can simplify things a bit more. For example Azure functions look very simple with that single context param. Maybe we should stick to that and just have one argument that has everything the user needs? doesn't even need to be called context.

When you look at (event, callback) or (event, context, callback) you immediately understand how to use it. Having one context object doesn't provide lots of info and you need to check docs because everything is hidden in this object. Also it's a standard way to write function.

LOVE how short & simple that looks ... but don't you think it looks a bit "Lambda-ish" ... I mean sure AWS dont really have copy rights over the symbol,

"In mathematical logic and computer science, lambda is used to introduce anonymous functions expressed with the concepts of lambda calculus." https://en.wikipedia.org/wiki/Lambda

It's commonly used in programming languages to describe anonymous functions. BTW it's only a proposition for shortcut.

but in the mind of many developers, we'd be referring to all provider functions as "Lambda".

Hmm, I think that's fine. Because they are actually lambdas :)

// not sure if the keyword "function" is reserved and can't be used as key though

It is reserverd. Though maybe fn?

The word handler is also AWS specific.

I don't think so. It's just a word describing function that handles something. There is nothing AWS specific here. Though I'm not saying that this is the best name :) Maybe fn.

So far we were able to support all runtimes that AWS supports because we don't really touch the handler code. Any ideas how we'll be able to accomplish this abstraction with all supported runtimes in all providers? Also what if a certain runtime is supported in single provider, but not the others?

Hmm, not exactly understand the question. I guess in every language with first-class function we can achieve something like wrapping user function with our function. Would be great if you could clarify.

@pmuens
Copy link
Contributor

pmuens commented Nov 29, 2016

Thanks for the writeup @mthenw! 👍

I like the idea of having one function handler which covers all the different FaaS providers.

I'd love the drop context so that we only have event and callback (like Google does it).
Is there any chance that we can only provider one handler?

In the beginning I liked the way Google separated the handlers between httpish and non-httpish handlers. However the data for req and res can also be attached to event, so that we end up with this one handler:

const stdlib = require('@serverless/stdlib')();

exports.userFunc = stdlib.handler((event, callback) => {
  callback(new Error('not implemented'))  
});

This could be the most minimal and simple handler we can provide. The other upside is that the user has to learn only one handler concept. Not sure what the upsides are of having one for http and one for the rest...

Aside:

no need to change code when deploying function to different providers,

Unfortunately this is not true as the function code which executes the logic still needs to be adapted so that the corresponding Cloud provider understands it / can use their services.

@mthenw
Copy link
Contributor Author

mthenw commented Nov 29, 2016

@pmuens

I'd love the drop context so that we only have event and callback (like Google does it).

It's optional. But I see few use cases when that might be needed.

Regarding one handler. First of all, it's not mandatory, generic handler can also be uses for HTTP funcs. Also, there is few important aspects of having separate http handler:

  1. follows well known req/res pattern from express
  2. easier to migrate existing web apps
  3. UX is just better

@austencollins
Copy link
Member

@mthenw Great research/write-up. This is the right way to start the conversation.

From what I understand, you are proposing a single function handler and an optional req, res handler on top of that. I believe this is the right way to go about this.

Some thoughts about context:

  • A reason I'm a fan of having context (but not as an argument), is over time, we can add in our own universal data and methods to context. Cross-provider methods people can call to getProvider() or getMemoryLimit() or getCurrentDuration() would be helpful and is an opportunity for this standard handler. If this sounds good to everyone else, we should make a note of this somewhere in the original proposal, to start the conversation on what standard data/methods we can provide.

  • An optional context argument is not something I'm fond of. If you are writing truly portable functions, then wouldn't an optional argument require writing logic at the beginning of every function to check if context was passed in? If so, this creates work, and work that a standard handler should handle.

  • What if we define context globally and not pass it in as an argument? serverless.context? That way it would always exist, but not pollute/confuse the arguments.

Additionally, how will this proposed handler pattern work across languages? Any language-specific quirks that we need to be aware of?

In general, I'm a fan of everything being discussed here. Overall, a very good starting point IMO.

@dougmoscrop
Copy link

I would like to propose using a Promise as the unit-of-response in the "universal handler signature", not callbacks. It's going to be something that ends up having to be boilerplated on to every single thing.

@dougmoscrop
Copy link

Also, in terms of event vs. req-res, my library: https://github.com/dougmoscrop/serverless-http might be of use/interest which represents lambda events as req/res objects so that existing frameworks (like koa or express or connect) can be used as the http pipeline.

@nikgraf
Copy link
Contributor

nikgraf commented Nov 30, 2016

@mthenw This document is a really nice read!

In what sense is the HTTP handler better UX? Do I miss the obvious?

I'm pretty certain we can't return the express req/res objects. They contain a lot of functions and attributes which are not exposed in any of the function providers. Even Google provides just objects which looks like express, but a lot of stuff is missing. In addition they provide a list of body parsers by default (JSON body parser, Raw body parser, Text body parser, URL-encoded form body parser) while in express that's something you can customize. I'm not sure of some bodyparser setups supported by Google are possible with Lamba. The devil is in the details here.
A rough prototype might help us to explore this API design.

One thing that I would love to see is the raw event/context and so on directly attached to the context. React uses the same strategy with syntactic events. So when you do onClick you don't get the raw event, but rather something very similar that with only attributes that work across all browsers. Still it contains the raw event nested inside in case you need it. The benefit here is that you can always use our handler, but still access provider specific parameters.
@ac360 without a function based context we would miss out on that features. The philosophy I would love to follow is: Keep it simple, but allow to leverage complex features if needed.

@nikgraf
Copy link
Contributor

nikgraf commented Nov 30, 2016

@dougmoscrop I love your promise based proposal. @mthenw and I talked for quite a while about it. I think it would be awesome, but I'm not sure if the community is ready for it. Maybe we support two versions: a promise based one and a callback version? To me it's certainly nice to use, especially with async/await coming 🙌

@johncmckim
Copy link
Member

When I first looked at the Serverless framework, I thought this kind of abstraction would be a great thing. However, now I'm not so convinced. I'm not sure that function handler is the hard part of swapping between providers. Generally, my functions look like this.

module.exports = (event, context, cb) => {
  const foo = event.body.foo; // Get information
  service
  .doFoo(foo)  // Act upon request
  .then(result => cb(null, result)); // Optionally transform response 
  .catch(err => cb(err));
}

Migrating that between providers is not a difficult problem. The hard part about swapping between providers is the other services like DynamoDb, Kinesis, SNS ect.

Standardising the function abstracts functionality away from the user. It could be great for very simple use cases. But, I'm not sure this is a good thing. Of course, using this is completely optional (or should be).

Tracking is probably a separate issue. I do like how IOPipe provides a wrapper around the handler to monitor functions. But it doesn't abstract any of the handler from the user.

@nikgraf
Copy link
Contributor

nikgraf commented Dec 1, 2016

@johncmckim Totally agree on the vendor-lock in. I believe the idea behind it is: Learn once, write everywhere. (totally stolen from React). If you are familiar with Serverless AWS this would make it easier to get started with Cloud Functions as you need to read through less documentation. Probably still a lot to read up on anyway 😄

@mthenw
Copy link
Contributor Author

mthenw commented Dec 1, 2016

@ac360

Cross-provider methods people can call to getProvider() or getMemoryLimit() or getCurrentDuration() would be helpful and is an opportunity for this standard handler.

What if we define context globally and not pass it in as an argument? serverless.context? That way it would always exist, but not pollute/confuse the arguments.

Nice idea! I like that. Totally make sense. For now we can remove that. But the issue I see is request related metadata. There is no way to pass them. Getting them from global object doesn't seems right.

@austencollins
Copy link
Member

Hmm, couldn't request related metadata that is in context could be added in the handler itself via some middleware logic?

I already wrote all of this btw:

@mthenw
Copy link
Contributor Author

mthenw commented Dec 1, 2016

@johncmckim The goal of providing standard handler is to make it easier to write functions. For the most use cases the differences between handlers expected by providers are slight but they are. We just want to simplify that.

@mthenw
Copy link
Contributor Author

mthenw commented Dec 1, 2016

@ac360

Hmm, couldn't request related metadata that is in context could be added in the handler itself via some middleware logic?

Could you provide some example?

@mthenw mthenw changed the title Function handlers Function handlers discussion Dec 1, 2016
@mthenw mthenw removed this from the 0.1 milestone Dec 1, 2016
@mthenw
Copy link
Contributor Author

mthenw commented Dec 2, 2016

I've created separate issue for generic handler #15.

@rochdev
Copy link

rochdev commented Dec 17, 2016

@dougmoscrop @nikgraf Even though promises are appealing in theory, I think in practice having a (req, res) callback would be the best approach. Keeping it as close as possible to what is yielded from node's http module (like @dougmoscrop implemented in serverless-http) also provides the best compatibility with existing frameworks such as express and koa.

It would of course be possible to then add support for promises and generators and whatnot, but the most important is definitely the callback.

@mthenw mthenw closed this as completed Jan 31, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

8 participants