Useful tips for a new FDK development

Image result for fn project logo

Welcome back! This particular guideline is a part of Fn’s contribution, development, and usage series.

Previous posts:

In this post I’d like to cover:

  • Communication between Fn and function (both transport and a request framing)
  • Purpose of the development kits

How does Fn communicate with functions?

Your function runs inside a container and uses the container’s STDIN to read serialized requests, STDOUT to return a serialized response and STDERR to write all logs. In order to establish communication with functions, the Fn server utilizes native Docker APIs to work with container IO streams. Fn writes serialized requests on the function container’s STDIN and listens for a serialized response from the function process inside of the container’s STDOUT. Fn uses the function container’s STDERR as a source for the function’s logs.

An FDK provides a convenient facade, a higher level abstraction and shields the function developer from having to interact directly with these underlying low-level constructs.

What is the communication format?

The function’s communication format defines an interaction protocol between Fn and the function. Fn dictates request and response schemas for a function that is responsible for composing valid response objects in terms of the format.

Fn defines 4 different formats: default, HTTP, JSON, CloudEvents.

Default

A function receives the body of the request as is on STDIN. In addition, the function has access to the application and function configuration parameters. Similar to request, the function’s response body will be sent as is on STDOUT. There are no hard rules that the request and response body need to adhere to.

HTTP

A function receives an HTTP 1.1 serialized request object. A function must return a valid HTTP 1.1 serialized response object.

JSON

A function receives a serialized JSON object. Take a look at the following example:

{
"call_id": "123",
"content_type": "application/json",
"deadline":"2018-01-30T16:52:39.786Z",
"body": "{\"some\":\"input\"}",
"protocol": {
"type": "http",
"method": "POST",
"request_url": "http://localhost:8080/r/myapp/myfunc?q=hi",
"headers": {
"Content-Type": ["application/json"],
"Other-Header": ["something"]
}
}
}

A function must respond with the following data:

{
"body": "{\"some\":\"output\"}",
"content_type": "application/json",
"protocol": {
"status_code": 200,
"headers": {
"Other-Header": ["something"]
}
}
}

where the bodyis a JSON string, i.e., serialized JSON response content. The rest of the fields were derived from HTTP 1.1 request headers. With the JSON protocol, HTTP requests should be transformed into JSON objects for function consumption.

CloudEvents

The CloudEvents format is a part of the CNCF community effort to create a common event data format to aid in the portability of functions between cloud providers. More information on this particular format can be found here. Fn recently added support for CloudEvents. From Fn’s perspective, it’s similar to JSON protocol.

Development issues

As you may have noticed, each protocol requires the function developer to handle parsing of requests. Each request has two parts:

  • request data (basically, represents caller request data)
  • request context (request-specific attributes like headers, etc.)

Having function developers write code to deal with Fn formats is not the most efficient use of their time. Enter FDKs.

On the Fn team, we care about developers. Formats were not designed to be a problem, so we invested a significant amount of time to develop a set of libraries for the various programming languages that we use on a regular bases (we started with Java, Go, Python, Ruby). These libraries were named as Functions Development Kits - aka FDK.

What is an FDK?

An FDK, or a Functions Development Kit, is a set of libraries included in Fn to simplify function development. An FDK shields developers from:

  1. having to deal with STDIN, STDOUT, and STDERR directly
  2. the complexities of Fn formats — parsing request and assembling response that complies with Fn formats, and
  3. writing logic to keep your functions hot

Fn provides FDKs with rich support for various programming languages — Java, Go, Python, Ruby, Node, etc. The idea behind FDKs is to hide any protocol-specific work from developers and lets them focus on the business logic of their function code” or “on the function business logic.

Design

The main idea behind FDKs is to let developers focus on their function and make the FDK handle protocol framing. Technically, FDK runs three blocks inside an infinite loop that consists of the following parts:

  • a formatted request deserialization into a request context and request data
  • a function execution with request context and request data
  • a function’s response being rendered into a formatted response

These three tasks run in an infinite loop in the following order, allowing Fn to process more requests for a single function. This schema is how Fn must process continuous requests:

incoming request(s) --> 
infinite loop:
- parse request
- call a function
- write response back
---> outgoing response

Parameters

In order to provide function developers a consistent user experience, we decided our FDKs should look similar regardless of the language used, with the exception of some language-specific features and/or when a language offers a more idiomatic approach (e.g. the Java FDK does not have the notion of handlers mentioned below):

  • a handler that accepts a callable object: fdk.handle({callable_object})
  • a callable object with the following signature: (context, data)

So, a function signature and its parameters will look similar no matter which programming language you chose to write your serverless function code in.

Request context

Let’s take a look at what the request context is. The request context is a placeholder for request-specific data like headers, deadline, call Id, etc. Below are the attributes of a request context object available to function developers in their code:

 Context
|
|________ AppName (current function's application)
|
|________ Route (HTTP trigger URI)
|
|________ CallID (unique ID assigned to the request)
|
|________ Config (current application + function config)
|
|________ Headers (request headers, may be an HTTP request headers)
|
|________ Arguments (may be an HTTP query parameters map)
|
|________ Format (current function's format)
|
|________ Deadline (how soon function will be aborted with the
| timeout)
|
|________ ExecutionType (wether function is async or sync)
|
|________ RequestContentType (request data content type, may be
| derived from an HTTP headers
|
|________ RequestURL (in case of an HTTP triggers would be complete
request URL that was used to trigger current
function)

These are the attributes (or methods without parameters) that you can request from the context object.

Request data

Request data is pulled out from the request used to trigger a function. In the case of an HTTP invocation path, data is an HTTP request body.

FDK implementation details

An FDK must be designed and implemented to hide the underlying protocol details and should provide a consistent user experience. Regardless of the format (http/JSON/CloudEvents/default) and execution mode (sync/async) specified, the programming mode as well as a user experience should remain consistent across different FDKs.

Go-like serialized HTTP headers

Fn is written in Golang. One of the features of the Golang HTTP framework is that it has its own native HTTP headers data type, where each key is mapped to a list of strings: map[string]string. This data type may not be native to other programming languages. In case you are creating a new FDK please make sure your FDK can safely [de]serialize Golang HTTP headers while using JSON or CloudEvent format.

Function’s deadline

Each Fn function has two timeout values that can be configured through the Fn public API:

  • (ordinary) timeout: a hard limit for a function call execution. After this time the function must die with HTTP 502 timeout in case it was invoked using an HTTP trigger
  • idle timeout: is a time between two requests to the same function. With idle timeout, Fn keeps a function container provisioned but idling which helps to save resources.

As part of JSON or CloudEvent format, Fn provides the deadline which indicates to the functions developer the time remaining before the current call execution will be terminated by Fn. The deadline is stored in HTTP headers for every hot format. This means that an FDK writer must implement a background handler or a timeout-based wrapper to call a developer’s function. Before the timeout is reached, an FDK must emit an entire protocol frame, otherwise the function will be timed out.

Default content type

In order to make an FDK compliant to Fn’s CLI testing framework, the default content type in a response frame must be set to application/json.

Fn CLI testing compliance

Fn CLI has a feature to generate boilerplate code. When you run fn init, fn generates a simple “Hello World!” function for one of the supported languages. The generated boilerplate also has a test.json file that contains a set of inputs and corresponding outputs for black-box testing. If a function passes these tests, it is expected to work well against a remote Fn API.

If you have thoughts on how to improve function testing, please let us know!.

Function testing with an FDK fixtures

A couple of our officially maintained FDKs — Java and Kotlin — go beyond this and use a popular Java unit test framework, i.e. JUnit. Ideally, every FDK must provide similar unit test capabilities to detect regression with any major or minor FDK release.

Programming language-specific features in FDK

Most programming languages have unique features that are not available in other languages. FDKs should expose such features to functions developers. For instance, FDK Python supports native coroutines (starting Python 3.5 or greater):

import asyncio
import fdk

async def handler(context, data=None, loop=None):
return data

if __name__ == "__main__":
loop = asyncio.get_event_loop()
fdk.handle(handler, loop=loop)

or promises in FDK Node:

var fdk=require('@fnproject/fdk');

fdk.handle(function(input, ctx){
return new Promise((resolve,reject)=>{
setTimeout(()=>resolve("Hello"),1000);
});
})

Here you’d find a recap from this post, the exact key takeaways we’d like you to keep in mind while developing a new FDK, so:

  • An FDK allows a developer to focus on function development rather than on low level concerns such as transports, protocols, etc.
  • An FDK is what you need when a function needs to go hot (switching from the default to any of JSON, HTTP, CloudEvent formats).
  • Developer’s UX is what we care about, which is why the programming model remains the same across an FDKs Fn Project officially supports.
  • An FDK may have programming language-specific features support like async/await in FDK-Python, or promises in FDK-Node.
  • We have an existing programming model, since an FDK concept was proposed as part of Fn Project, but it doesn’t mean that it will always remain the same. Fn evolves, meaning an FDK interface and programming model may evolve as well. If you think the existing programming model does not really fit your needs, do not hesitate to let us know! We are an open community, and feedback and contributions are always both welcome and highly appreciated!

You may find the following links useful as a general follow-up on this post:

Finally, join the Fn Army on Slack!