Skip to content

Instantly share code, notes, and snippets.

@djspiewak
Created August 7, 2021 13:36
Show Gist options
  • Star 10 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save djspiewak/37a4ea0d7a5237144ec8b56a76ed080d to your computer and use it in GitHub Desktop.
Save djspiewak/37a4ea0d7a5237144ec8b56a76ed080d to your computer and use it in GitHub Desktop.

Serverless Concept

This document describes the general outcome we want for users as well as a few ideas on how we should do this. It shouldn't be considered prescriptive or precise though; if we come up with better ideas along the way, we should do them instead!

Onboarding

Users should be able to run something like the following:

$ sbt new typelevel/serverless.g8 --branch aws/http

Then, once this is done, within the resulting project they should be able to run sbt deploy and immediately get a running, functioning HTTP lambda. Assume we have an AWS_API_KEY in an environment variable (or similar), presumably referenced in some way from the build.sbt.

We should provide similar templates for a whole variety of serverless options, including different forms of AWS Lambda, Google Cloud Functions, etc. Obviously these templates will need to be primarily maintained by automated processes, such as Scala Steward, to avoid bitrot.

Features

Every serverless miniframework should revolve around a type like IOLambda (name subject to bikeshedding; also should probably be function type-specific, so we don't have collisions between (e.g.) http and kinesis lambdas in the same project), inspired by IOApp. These traits should define the entrypoint function, as well as a general Resource management API. A quick example concept:

// imagine a bare TCP 
trait IOLambda {
  type Deps
  def init: Resource[IO, Deps] = Resource.pure(null.asInstanceOf[Deps])

  def run(deps: Deps): Pipe[IO, Byte, Byte]
}

With this kind of API, subtypes would need to define the run method. If they don't have any long-lived resources, then they would leave Deps alone and wouldn't need to override init. If they do have long-lived resources, then overriding init (and the poorly-named Deps) would allow them to declare those resources to the runtime.

Function entrypoints should be defined in a domain-specific way. The above example shows a simple TCP lambda. An HTTP lambda could be very similar:

trait IOLambda {
  type Deps
  def init: Resource[IO, Deps] = Resource.pure(null.asInstanceOf[Deps])

  def routes(deps: Deps): HttpRoutes[IO]
}

Thus, an HTTP lambda would be defined not by some Pipe, but rather by an Http4s routes value. Imagine similar things for other sorts of lambdas or serverless functions.

Runtime

All serverless functions should be compiled to JavaScript and deployed to the relevant runtime. There are a lot of reasons for this, cold-start being one of them, but more generally it's important to remember what the JVM is and is not good at. In particular, the JVM excels at long-lived multithreaded applications which are relatively memory-heavy and reply on medium-lifespan heap allocations. So in other words, persistent microservices. Serverless functions are, by definition, not this. They are not persistent, they are (generally) single-threaded, and they need to start very quickly with minimal warming. They do often apply moderate-to-significant heap pressure, but this factor is more than outweighed by the others.

V8 is a very good runtime for these kinds of use-cases. Realistically, it may be the best-optimized runtime in existence for these requirements, similar to how the JVM is likely the best-optimized runtime in existence for the persistent microservices case.

Implementation

We should try to have as many of the implementation details as possible in a common backing framework. There are many dozens of different types of AWS Lambdas alone, and Amazon is adding new types all the time. These are expressed to users as entirely independent frameworks, and we should carry that model forward to our downstream users. Maintaining this kind of sprawling zoo of implementations will require the majority of the common functionality to be abstracted and pulled up into a shared framework which forms the foundation of all of the specific implementations. Much of the resource management logic, for example, should be abstractable.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment