Git Product home page Git Product logo

feral's Introduction

feral feral-core Scala version support javadoc Discord

feral is a framework for writing serverless functions in Scala with Cats Effect and deploying them to the cloud, targeting both JVM and JavaScript runtimes. By providing an idiomatic, purely functional interface, feral is both composable—integrations with natchez and http4s are provided out-of-the-box—and also highly customizable. The initial focus has been on supporting AWS Lambda and will expand to other serverless providers.

Getting started

Feral is published for Scala 2.13 and 3.2+ with artifacts for both JVM and Scala.js 1.13+.

// Scala.js setup
addSbtPlugin("org.typelevel" %% "sbt-feral-lambda" % "0.2.2") // in plugins.sbt
enablePlugins(LambdaJSPlugin) // in build.sbt

// JVM setup
libraryDependencies += "org.typelevel" %% "feral-lambda" % "0.2.2"

// Optional, specialized integrations, available for both JS and JVM
libraryDependencies += "org.typelevel" %%% "feral-lambda-http4s" % "0.2.2"
libraryDependencies += "org.typelevel" %%% "feral-lambda-cloudformation-custom-resource" % "0.2.2"

Next, implement your Lambda. Please refer to the examples for a tutorial.

There are several options to deploy your Lambda. For example you can use the Lambda console, the SAM CLI, or the serverless framework.

To deploy a Scala.js Lambda, you will need to know the following:

  1. The runtime for your Lambda is Node.js 18.
  2. The handler for your Lambda is index.yourLambdaName.
    • index refers to the index.js file containing the JavaScript sources for your Lambda.
    • yourLambdaName is the name of the Scala object you created that extends from IOLambda.
  3. Run sbt npmPackage to package your Lambda for deployment. Note that you can currently only have one Lambda per sbt (sub-)project. If you have multiple, you will need to select the one to deploy using Compile / mainClass := Some("my.lambda.handler").
  4. For the tooling of your choice, follow their instructions for deploying a Node.js Lambda using the contents of the target/scala-2.13/npm-package/ directory.

As the feral project develops, one of the goals is to provide an sbt plugin that simplifies and automates the deployment process. If this appeals to you, please contribute feature requests, ideas, and/or code!

Why go feral?

The premise that you can (and should!) write production-ready serverless functions in Scala targeting JavaScript may be a surprising one. This project—and the rapid maturity of the Typelevel.js ecosystem—is motivated by three ideas.

  1. JavaScript is the ideal compile target for serverless functions.

    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 rely 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 (the JavaScript engine in Node.js) 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.

  2. Scala.js and Cats Effect work together to provide powerful, well-defined semantics for writing JavaScript applications.

    It hopefully should not take much convincing that Scala is a fantastic language to use, regardless of the ultimate compile target. But what might be unexpected by those new to Scala.js is how well it preserves Scala's JVM semantics in JavaScript. Save a few edge-cases, by and large Scala programs behave the same on JS as they do on the JVM.

    Cats Effect takes this a step further by establishing semantics for asynchronous programming (aka laws) and guaranteeing them across the JVM and JS. In fact, the initial testing of these semantics on Scala.js revealed a fairness issue that culminated in the deprecation of the default global ExecutionContext in Scala.js. As a replacement, the MacrotaskExecutor project was extracted from Cats Effect and is now the official recommendation for all Scala.js applications. Cats Effect IO is specifically optimized to take advantage of the MacrotaskExecutor's fairness properties while maximizing throughput and performance.

    IO also has features to enrich the observability and debuggability of your JavaScript applications during development. Tracing and enhanced exceptions capture the execution graph of a process in your program, even across asynchronous boundaries, while fiber dumps enable you to introspect the traces of all the concurrent processes in your program at any given time.

  3. Your favorite Typelevel libraries are already designed for Scala.js.

    Thanks to the platform-independent semantics, software built using abstractions from Cats Effect and other Typelevel libraries can often be easily cross-compiled for Scala.js. One spectacular example of this is skunk, a data access library for Postgres that was never intended to target JavaScript. However, due to its whole-hearted adoption of purely functional asynchronous programming, today it also runs on Node.js with virtually no changes to its source code.

    In practice, this means you can directly transfer your knowledge and experience writing Scala for the JVM to writing Scala.js and in many cases share code with your JVM applications. The following libraries offer identical APIs across the JVM and JS platforms:

feral's People

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

feral's Issues

feral example code Http4sLambda.scala does not work for JVM in AWS Lambda

I tried to deploy the example code "Http4sLambda.scala " on AWS lambda. After deploying to lambda, and setting it up with API Gateway, I tried to call API "/foo". It should return the string "bar", but it returns empty response.
I get Response status as 200, but this is how the header contents look like:

Date - Fri, 14 Jul 2023 12:58:19 GMT
Content-Type - application/json
Content-Length - 0
Connection - keep-alive
x-amzn-RequestId- 6ac720.......
x-amz-apigw-id - IDfwg......
X-Amzn-Trace-Id - Root= 1-64b14......

I also tried to add print statements, but nothing get printed in logs. This is the code that I used:
https://github.com/typelevel/feral/blob/main/examples/src/main/scala/feral/examples/Http4sLambda.scala

Later, I was able to successfully run my APIs using this library:
https://github.com/wfaler/http4s-lambda

Same lambda, same API Gateway, only jar is replaced. So this is a bug in either feral itself. It is not working for JVM code.

Java version I used: 17
Scala version: 2.13.11

Make Feral more amenable to SnapStart optimization?

A few weeks ago AWS announced SnapStart, a feature to improve cold-start performance for JVM Lambdas. A few lines of config to make my Lambdas magically faster? Yes please!

With SnapStart, Lambda initializes your function when you publish a function version. Lambda takes a Firecracker microVM snapshot of the memory and disk state of the initialized execution environment, encrypts the snapshot, and caches it for low-latency access. When you invoke the function version for the first time, and as the invocations scale up, Lambda resumes new execution environments from the cached snapshot instead of initializing them from scratch, improving startup latency.

My understanding of that paragraph is that they will "initialize" by calling the constructor on the handler class, but they won't invoke the Lambda, i.e. call the handler method.

Unfortunately (if I'm understanding Feral's code correctly) it appears that Feral Lambdas won't benefit much from this optimization. As I understand it, Feral does pretty much nothing at class initialization time. The resource defined in def init is acquired the first time the Lambda is invoked, and then memoized for reuse by subsequent invocations. So the SnapStart snapshot will capture the JVM startup and a bit of classloading, but none of the work performed while acquiring the init resource.

It would be nice if we could make everything that happens in IOSetup happen eagerly at class init time to take full advantage of SnapStart.

For now we can emulate this in user-land by eschewing def init and just doing a good old unsafeRunSync:

class MyLambda extends IOLambda.Simple[KinesisStreamEvent, INothing]:
  type Init = Unit

  private def buildAlgebra: IO[MyAlgebra[IO]] = ??? // the stuff that would usually go in `def init`

  private val algebra: MyAlgebra[IO] = buildAlgebra[IO].unsafeRunSync()

  override def apply(
      event: KinesisStreamEvent,
      context: Context[IO],
      init: Init
  ): IO[Option[INothing]] = algebra.process(event, context)

Disclaimer: I haven't done any benchmarking with SnapStart and Feral yet.

Use context function for handler in Scala 3?

TIL about context functions. H/t @som-snytt.

They are new in Scala 3, and provide a ?=> syntax for anonymous functions using implicit (aka context) parameters. This is exactly our setup with LambdaEnv. Not only would this let Scala 3 users skip the boilerplate implicit env =>, there seems to be no Scala 3 equivalent of making a non-context parameter into a given; see scala/scala3#14167.

Doing this will require splitting our sources on the Scala 2/3 axis in addition to the JVM/JS axis 😩

Why not Dispatcher?

Hello from Scalar 2023 in Warsaw! I saw a presentation by Kamil Kloch about cats-effect and learned about Dispatcher being the go-to mechanism for running IO's in unsafe territory.

I'm in a position where I've home-rolled a solution that is similar to Feral (build a resource, manually initialize, toss away the finalizer bc lifetimes are hard) and in the last mile call unsafeRunSync.

The presentation I watched was advocating that there shouldn't be a reason to do that and just use a Dispatcher .use() instead "because reasons". Something about the Dispatcher having a better handle on initializing what is required to run IOs?

  • A) How is the Dispatcher's run different than just using the unsafe global provided?
  • B) Should feral replace its unsafeRunSync with a Dispatcher per the advice above? Is there some nuance/extra dimension we are missing?

Edit: I believe Dispatcher thoughts are a continuation on #33

Idea: Simple and scalable mechanism for bundling lambdas into services

I wonder how feasible it would be to eventually implement a framework which could take Feral-defined lambdas and bundle them together as a persistent microservice (which presumably would run on the JVM). The observation here is that serverless is usually really great when you're a smaller project with lower traffic and less time for devops BS, but as you scale up it starts to get really expensive and it would be nice to have a clean migration path for taking your existing stuff and moving it onto something like EKS.

I feel like Feral is kind of uniquely positioned to offer something useful in this space, long-term (i.e. probably not right now), because the abstraction level is so much higher and because the runtime semantics are so uniform across Native, JS, and JVM. One of the many challenges with this type of thing is the ideal platform for serverless functions is either JS or Native, while the ideal platform for bundled persistent microservices is JVM. In theory, we can move more fluidly between those spaces.

Anyway, it's just a seed of a thought, but maybe something that could turn into something.

(from https://discord.com/channels/632277896739946517/918373380003082250/1023915447885758484)

Make a microsite with organized docs

Emphasis on organized, as we continue adding clouds that each need their own docs it's going to get complicated. Not entirely sure if we'll be able to use mdoc or will need mdoc.js for the Scala.js-only modules. It's a bit of a strange situation, since mdoc.js is intended for interactive browser-based Scala.js snippets, but we'll be compiling code targeting Node.js that won't run in the browser. So not quite sure if/how that would work.

Regarding site generator, I'm amenable to Laika, which I had a pleasant experience using for http4s-dom. Also the new docs generation capabilities in Scaladoc 3 seem interesting, see scalameta/mdoc#588 (comment).

MiMa Configuration

Is MiMa set up correctly in this build? I briefly looked at #456 this evening and wanted to confirm that MiMa enabled, and I'm getting empty mimaPreviousArtifacts values, which I thought was surprising.

sbt:feral> show mimaPreviousArtifacts
[info] scalafix-output / mimaPreviousArtifacts
[info] 	Set()
[info] lambdaJVM / mimaPreviousArtifacts
[info] 	Set()
[info] lambdaCloudFormationCustomResourceJS / mimaPreviousArtifacts
[info] 	Set()
[info] scalafix-input / mimaPreviousArtifacts
[info] 	Set()
[info] lambdaJS / mimaPreviousArtifacts
[info] 	Set()
[info] unidocs / mimaPreviousArtifacts
[info] 	Set()
[info] lambdaCloudFormationCustomResourceJVM / mimaPreviousArtifacts
[info] 	Set()
[info] scalafix-tests / mimaPreviousArtifacts
[info] 	Set()
[info] sbtLambda / mimaPreviousArtifacts
[info] 	Set()
[info] examplesJS / mimaPreviousArtifacts
[info] 	Set()
[info] lambdaHttp4sJVM / mimaPreviousArtifacts
[info] 	Set()
[info] examplesJVM / mimaPreviousArtifacts
[info] 	Set()
[info] scalafix-rules / mimaPreviousArtifacts
[info] 	Set()
[info] lambdaHttp4sJS / mimaPreviousArtifacts
[info] 	Set()
[info] mimaPreviousArtifacts
[info] 	Set()

@armanbilge do you have any thoughts?

ES Module support

Native ES modules are finally supported in all current LTS NodeJS versions 🎉. More and more Node libraries are moving to ES modules. ES modules can use a top-level import for CommonJS, but CommonJS cannot import ES modules with top-level imports. This means libraries still using CommonJS risk getting left behind in the Node ecosystem.

Feral currently uses a dynamic export update to export the handler function. From what I could see, this is done to load resources only once instead of every time the lambda is invoked. However, ES modules do not support dynamic export updates. All exports have to be there as export statements. The recommended way for the resource use-case is by using top-level awaits.

To keep Node.js library interop and keep the library modern, it would be ideal for Feral to also support building as a ES Module. I'm not sure if it is possible to instruct Scala.JS to emit different code based on the module setting, or perhaps a different entrypoint should be available for ES Module users.

Lambda output empty with 1.0.0-M1

First of all, this is a great project! And I love the name :)

When using 1.0.0-M1 I get empty output from lambda (Scala 2.13 compiled for JVM 11). Tried downgrading to 0.1.0-M8 and that one works perfectly fine. Could it be that this commit broke the output?: da29dc7

`context.callbackWaitsForEmptyEventLoop` is a `var`, actually

This is a very interesting example on https://docs.aws.amazon.com/lambda/latest/dg/nodejs-handler.html.

In the following example, the response from Amazon S3 is returned to the invoker as soon as it's available. The timeout running on the event loop is frozen, and it continues running the next time the function is invoked.

const AWS = require('aws-sdk')
const s3 = new AWS.S3()

exports.handler = function(event, context, callback) {
  context.callbackWaitsForEmptyEventLoop = false
  s3.listBuckets(null, callback)
  setTimeout(function () {
    console.log('Timeout complete.')
  }, 5000)
}

We are using the Promise variant of the handler, rather than callback-based. The docs seem to suggest this is different. So maybe this isn't relevant at all, but the bit about the timeout being "frozen" is pretty interesting :) might be worth looking into.

New Name?

So… I think "feral" and the project description are pretty clever, but I'm concerned about the word's negative connotations. I've told several people offline about the project and all of them gave me a raised eyebrow or otherwise recoiled a bit at the name. It feels like kind of an unintended dig at the whole serverless concept.

@armanbilge @djspiewak What do you think about changing the name to something more boring, like cats-effect-serverless or something like that?

Custom Lambda runtime?

Following from #132, @Baccata points out that AWS offers a similarly low level interface to build a custom Lambda Runtime.

https://docs.aws.amazon.com/lambda/latest/dg/runtimes-custom.html

The advantage of implementing a custom runtime is the capability to process multiple events concurrently, but it's unclear if AWS supports this. On the one hand, polling for incoming events is completely within the lambda's control, and an id is used to pair responses with events so they don't necessarily need to be processed in order. But on the other, there are some strange assumptions such as the use of environment variables to pass around tracing headers, which makes me think this is unsupported behavior.

This should be further investigated.

Document how to deploy with scala-cli

scala-cli is extremely convenient for packaging a small Scala project into a deployable artifact (e.g. Jar or JS file). We should make some instructions how to do this and then how to actually deploy it.

Integrate with Google Cloud Functions

Did some preliminary research, there seem to be:

  1. Http functions. These are a distinct type of function, and will need separate JVM and JS implementations. The JS implementation can use the node-serverless module in http4s.
  2. Background functions. I think these are more similar to Lambda, in that there is a common interface and various types of triggers that are parsed as JSON.

natchez integration and lambda init

There's a problem when using tracing in combo with resources that are cached (in init or setup part of execution), which is even included in one of the examples: https://github.com/typelevel/feral/blob/main/examples/src/main/scala/feral/examples/KinesisLambda.scala#L52-L53.

The problem:

  1. Actual root span cannot be used in init because it will never be closed. This is even worse if you reuse Trace[IO] instance inside request handler code, because all spans there would become children of this never-ending root span. And some natchez backend implementations (like XRay) only send root span together with all children when root span is finished. So you get no recorded spans as a result.
  2. If you use fake root span to solve the first problem (smth like this: typelevel/natchez#566 (comment)), then spans produced during init (like DB connection pool initialization with skunk) are separate from spans produced in the first request (handler code) of that lambda instance.

This means that TracedHandler implementation is useless if you need to have Trace[IO] during init. In case of skunk, you would want to see SQL query spans inside request spans and for that you need to reuse same instance of Trace[IO] for all requests.

How it should work:

It should be possible to build a single Trace[IO] instance for the lifetime of lambda instance. Spans produced during initialization would get recorded as children of root span for the first ever request to that lambda instance.

I hope I managed to describe this clearly 😅. Would be great to work this out and get that example fixed so that it would show how to use skunk and tracing with feral.

Deprecate/remove `INothing`?

To follow suit with typelevel/fs2#2870.

@bpholt had an excellent minimization in #52 (comment) demonstrating why we needed INothing instead of Nothing. Unfortunately, subsequent refactors made it no longer applicable and I was unable to find another one, tracked in #73.

I took a quick try at replacing INothing with Nothing. So far I encountered two issues:

  • To get the Encoder[INothing] into implicit scope on Scala 3, we put it inside a companion object for INothing. We can't do this with Nothing.

    type INothing <: Nothing
    object INothing {

  • This compile test no longer compiles, which may mean trouble for inference when using Kleisli-based tracing.

    TracedHandler(ioEntryPoint, Kleisli[IO, Span[IO], Option[INothing]](???))

    type mismatch;
     found   : cats.data.Kleisli[cats.effect.IO,natchez.Span[cats.effect.IO],Option[Nothing]]
     required: cats.data.Kleisli[[+A]cats.effect.IO[A],natchez.Span[[+A]cats.effect.IO[A]],Option[Result]]
    Note: Option[Nothing] <: Option[Result], but class Kleisli is invariant in type B.
    You may wish to define B as +B instead.
    

    Update: the broken compile test is only broken on Scala 2, Scala 3 seems okay.

Can't print logs using `IO.println`

It seems that I can't get logs from my lambda built with Feral.
I've been reproducing with this simple example (using Scala 3.3.0 and Feral 0.2.3):

object Lambda extends IOLambda[Unit, Unit] {
  override def handler: Resource[IO, LambdaEnv[IO, Unit] => IO[Option[Unit]]] =
    for {
      _ <- IO.println("trying to log something").toResource
    } yield { (env: LambdaEnv[IO, Unit]) =>
      IO.println("trying to log something else") *> IO.unit.map(_.some)
    }
}

When executing sam local invoke LambdaFunction

I get:

Invoking hawk.Lambda::handler (java17)
Decompressing
/Users/me/Documents/path/to/my/uberjar.jar
Local image is up-to-date
Using local image: public.ecr.aws/lambda/java:17-rapid-arm64.

Mounting /private/var/folders/h8/1xxcb8j53q9gvtdvl0t674y00000gp/T/tmpsq7sev9x as
/var/task:ro,delegated, inside runtime container
START RequestId: 78653470-2d5a-4292-ab79-caf86ef6a0f6 Version: $LATEST
END RequestId: 78653470-2d5a-4292-ab79-caf86ef6a0f6
REPORT RequestId: 78653470-2d5a-4292-ab79-caf86ef6a0f6	Init Duration: 0.06 ms	Duration: 670.52 ms	Billed Duration: 671 ms	Memory Size: 1024 MB	Max Memory Used: 1024 MB
{}%

The jar was build using sbt assembly and the following SAM template was used:

Resources:
  LambdaFunction:
    Type: AWS::Serverless::Function
    Properties:
      Handler: hawk.Lambda::handler
      CodeUri: path/to/my/uberjar.jar
      Runtime: java17
      Architectures:
        - arm64
      MemorySize: 1024
      Events:
        S3Event:
          Type: S3
          Properties:
            Bucket:
              Ref: MyS3Bucket
            Events: s3:ObjectCreated:*
  MyS3Bucket:
    Type: AWS::S3::Bucket
Outputs:
  LambdaFunction:
    Description: Lambda function ARN
    Value: !GetAtt LambdaFunction.Arn
  S3Bucket:
    Description: S3 bucket name
    Value: !Ref MyS3Bucket

On the other hand, this minimal Java example works fine:

public class HelloWorldLambda implements RequestHandler<Void, Void> {
    @Override
    public Void handleRequest(Void input, Context context) {
        System.out.println("Hello world");
        return null;
    }
}

When executing sam local invoke HelloWorldLambdaFunction I get:

Invoking org.example.HelloWorldLambda::handleRequest (java17)
Decompressing /Users/me/Documents/path/to/my/uberjar.jar
Local image is up-to-date
Using local image: public.ecr.aws/lambda/java:17-rapid-arm64.

Mounting /private/var/folders/h8/1xxcb8j53q9gvtdvl0t674y00000gp/T/tmpc8ca3r5b as /var/task:ro,delegated, inside runtime container
START RequestId: e789cd07-f461-4566-a7c3-8e51c6e3481a Version: $LATEST
Hello world
END RequestId: e789cd07-f461-4566-a7c3-8e51c6e3481a
REPORT RequestId: e789cd07-f461-4566-a7c3-8e51c6e3481a	Init Duration: 0.13 ms	Duration: 129.56 ms	Billed Duration: 130 ms	Memory Size: 512 MB	Max Memory Used: 512 MB
null%

The jar was build using mvn clean package and the following SAM template was used:

Resources:
  HelloWorldLambdaFunction:
    Type: AWS::Serverless::Function
    Properties:
      FunctionName: HelloWorldLambdaFunction
      Handler: org.example.HelloWorldLambda::handleRequest
      Runtime: java17
      Architectures:
        - arm64
      CodeUri: path/to/my/uberjar.jar
      MemorySize: 512
      Timeout: 10
      Policies:
        - AWSLambdaBasicExecutionRole

Outputs:
  HelloWorldLambdaFunction:
    Description: HelloWorld Lambda Function ARN
    Value: !GetAtt HelloWorldLambdaFunction.Arn

v0.1.0

This has been on my mind, and now that Circe 0.14.2 landed we no longer have milestone dependencies of our own :)

I think the feral-lambda core module has been pretty stable. We did break binary-compatibility when fixing some events, and that may generally be annoying going forward depending on how Amazon evolves their APIs.

Things I'd like to do:

  • open up the LambdaEnv and Context APIs, I think I saw @bpholt doing some shenanigans to mock them
  • make sure the API can support graceful shutdown of resources, even if we don't implement it yet. h/t @fdietze for reminding me of this issue
  • reassure myself that folks are happy with the API 😅 I was reminded this week that the MTL style can be confusing/controversial, see http4s/http4s#4758 (comment)
  • review the events module. de-case-classing may be the best for longterm bincompat, but will be a PITA
  • anything else?

There's also a milestone with some issues in it, but they're mostly nice-to-haves. The smithy4s integration would still be good if we are okay to tie our binary-compatibility to them.

https://github.com/typelevel/feral/milestone/1

I'm used to working on projects where bincompat is a Big Deal, and it doesn't have to be here yet. I'd like to stay stable if we can, but worst case is 0.2.0 :)

cc @bpholt and @kubukoz

Scala Native support

It's here and it's very possible! Just needs #134 which I think is easy. Figuring out how to test it will be more challenging.

Use `Instant` for modeling timestamps

AKA how do we feel about a scala-java-time dependency.

Pros:

  • Instant is most idiomatic representation of a timestamp

Cons:

  • Bloats generated JS

Mitigation:

  • Estimated to be a 120 kb dependency in http4s/http4s-dom#30 (comment)
  • JS size is not as a big a deal for Node.js lambda compared to browser.
  • skunk and http4s already bring in this dependency anyway.

`cloudformation.ResponseSerializationSuite` test failure

Ping @bpholt.

https://github.com/typelevel/feral/runs/4835158804?check_suite_focus=true#step:10:41

==> X feral.lambda.cloudformation.ResponseSerializationSuite.CloudFormationCustomResource should PUT the response to the given URI 1.98s munit.FailException: Failing seed: TMdL2IA_H4DCXl0kbz5HogldPyArS7jjBPgancWELoF=
You can reproduce this failure by adding the following override to your suite:

  override def scalaCheckInitialSeed = "TMdL2IA_H4DCXl0kbz5HogldPyArS7jjBPgancWELoF="

Exception raised on property evaluation.
> ARG_0: feral.lambda.LambdaEnv$$anon$2@18b
> Exception: java.lang.AssertionError: assertion failed

expect(body eqv expectedJson)
       |    |   |
       |    |   {
  "Status" : "FAILED",
  "Reason" : "unexpected CloudFormation request type `⮖並둈턴鎫恦暔厓ၯ럺炼攮ર꬏\r줅贷龤∣쐬텉塄戾Ⴁ欷老⫯ᘋ䗔쭤屑ऩ㍘ℎ扢⼼뺛鼟㬻붮詃汼圳ὐ䚓住ꮟ썵芓첲ꃻ훘폓磹磻ᯉ覦與匲糧̪픀杵㓼鍵ᱪﰩ痢ṱ褕쁽ꤸᵻ袖ᇘ條᝵䈴牑ꛀ麢㜷츝鳸꦳귐ยጷ`",
  "PhysicalResourceId" : "<",
  "StackId" : "ဉ",
  "RequestId" : "fe87926c-e1b8-43f1-b7e3-61f83a34a0bc",
  "LogicalResourceId" : "",
  "Data" : {
    "StackTrace" : [
      "java.lang.IllegalArgumentException: unexpected CloudFormation request type `⮖並둈턴鎫恦暔厓ၯ럺炼攮ર꬏\r줅贷龤∣쐬텉塄戾Ⴁ欷老⫯ᘋ䗔쭤屑ऩ㍘ℎ扢⼼뺛鼟㬻붮詃汼圳ὐ䚓住ꮟ썵芓첲ꃻ훘폓磹磻ᯉ覦與匲糧̪픀杵㓼鍵ᱪﰩ痢ṱ褕쁽ꤸᵻ袖ᇘ條᝵䈴牑ꛀ麢㜷츝鳸꦳귐ยጷ`"
    ]
  }
}
       |    false
       {
  "Status" : "FAILED",
  "Reason" : "unexpected CloudFormation request type `⮖並둈턴鎫恦暔厓ၯ럺炼攮ર꬏\r줅贷龤∣쐬텉塄戾Ⴁ欷老⫯ᘋ䗔쭤屑ऩ㍘ℎ扢⼼뺛鼟㬻붮詃汼圳ὐ䚓住ꮟ썵芓첲ꃻ훘폓磹磻ᯉ覦與匲糧̪픀杵㓼鍵ᱪﰩ痢ṱ褕쁽ꤸᵻ袖ᇘ條᝵䈴牑ꛀ麢㜷츝鳸꦳귐ยጷ`",
  "PhysicalResourceId" : "<",
  "StackId" : "ဉ",
  "RequestId" : "fe87926c-e1b8-43f1-b7e3-61f83a34a0bc",
  "LogicalResourceId" : "",
  "Data" : {
    "StackTrace" : [
      "java.lang.IllegalArgumentException: unexpected CloudFormation request type `⮖並둈턴鎫恦暔厓ၯ럺炼攮ર꬏"
    ]
  }
}

[error] Failed: Total 1, Failed 1, Errors 0, Passed 0
[error] Failed tests:
[error] 	feral.lambda.cloudformation.ResponseSerializationSuite
[error] (lambdaCloudFormationCustomResourceJS / Test / test) sbt.TestsFailedException: Tests unsuccessful
[error] Total time: 10 s, completed Jan 17, 2022 12:55:47 AM

Implement Google Functions Framework

https://github.com/GoogleCloudPlatform/functions-framework#functions-framework-contract

A Functions Framework consists of two parts:

  • A package that instantiates a web server and invokes function code in response to an HTTP request. This package may include additional functionality to further minimize boilerplate.
  • A script or tool that converts a source code transform of function code into app code ("the function-to-app converter")

This is lower-level than Cloud Functions, which enables us to implement concurrency, and thus achieve better efficiency than non-concurrent serverless options like Lambda.

For performance, efficiency and correctness reasons, the framework must be able to handle multiple concurrent invocations of the developer's function.

Concurrency is configurable. By default each Cloud Run container instance can receive up to 80 requests at the same time; you can increase this to a maximum of 1000. Note that in comparison, Functions-as-a-Service (FaaS) solutions like Cloud Functions have a fixed concurrency of 1.

Note that these frameworks are containerized and deployed to e.g. Cloud Run. This is different from FaaS, where you typically upload an npm package or a jar.

These tips for running Java in Cloud Run (and only Java!) suggest that Node.js is still an ideal runtime even for the framework setup:
https://cloud.google.com/run/docs/tips/java

There's also a testkit:
https://github.com/GoogleCloudPlatform/functions-framework-conformance

Finally, none of this is specific to Google Cloud per se: it works with ordinary HTTP events and platform-agnostic CloudEvents and is deployable to kubernetes/knative ... although, not sure how widely adopted these are outside of Google (AWS does not support CloudEvents, cloudevents/spec#435).

This also makes testing locally much easier, since it does not rely on emulators of serverless environments.

Create models for the myriad of AWS Lambda event types

For feral-lambda to be useful for the "zoo" of Lambda types available on AWS, we need to provide models for the various events (aka "triggers") and responses used by each of these lambdas. These will live in the feral-lambda-events module.

We are currently sourcing them from the Java and JavaScript/TypeScript implementations.

The TypeScript definitions are especially important, because they are the definitive representation of these events as "plain old JavaScript objects" which is isomorphic to their JSON representation. Therefore, our models should match them exactly in name, structure, etc. in order for parsing to work correctly.

For an example of this, see the existing events:
https://github.com/typelevel/feral/blob/main/lambda/shared/src/main/scala/feral/lambda/events/

  • ActiveMQEvent
  • APIGatewayCustomAuthorizerEvent
  • APIGatewayProxyRequestEvent #409
  • APIGatewayProxyResponseEvent #409
  • APIGatewayV2CustomAuthorizerEvent
  • APIGatewayV2HTTPEvent
  • APIGatewayV2HTTPResponse
  • APIGatewayV2WebSocketEvent #476
  • APIGatewayV2WebSocketResponse #476
  • ApplicationLoadBalancerRequestEvent
  • ApplicationLoadBalancerResponseEvent
  • AppSyncLambdaAuthorizerEvent
  • AppSyncLambdaAuthorizerResponse
  • CloudFormationCustomResourceEvent
  • CloudFrontEvent
  • CloudWatchLogsEvent
  • CodeCommitEvent
  • CognitoEvent
  • CognitoUserPoolCreateAuthChallengeEvent
  • CognitoUserPoolCustomMessageEvent
  • CognitoUserPoolDefineAuthChallengeEvent
  • CognitoUserPoolEvent
  • CognitoUserPoolMigrateUserEvent
  • CognitoUserPoolPostAuthenticationEvent
  • CognitoUserPoolPostConfirmationEvent
  • CognitoUserPoolPreAuthenticationEvent
  • CognitoUserPoolPreSignUpEvent
  • CognitoUserPoolPreTokenGenerationEvent
  • CognitoUserPoolVerifyAuthChallengeResponseEvent
  • ConfigEvent
  • ConnectEvent
  • DynamodbEvent #68
  • IoTButtonEvent
  • KafkaEvent
  • KinesisAnalyticsFirehoseInputPreprocessingEvent
  • KinesisAnalyticsInputPreprocessingResponse
  • KinesisAnalyticsOutputDeliveryEvent
  • KinesisAnalyticsOutputDeliveryResponse
  • KinesisAnalyticsStreamsInputPreprocessingEvent
  • KinesisEvent #75
  • KinesisFirehoseEvent
  • LambdaDestinationEvent
  • LexEvent
  • RabbitMQEvent
  • S3BatchEvent #231
  • S3BatchResponse #231
  • S3Event #354
  • ScheduledEvent
  • SecretsManagerRotationEvent
  • SimpleIAMPolicyResponse
  • SNSEvent #195
  • SQSEvent #58
  • any more?

Lambda modules organization?

This keeps coming up, e.g. #2, #6 (comment), #48, #50 (comment). So at some point we should decide what we want to do :)

Just some assorted thoughts, all IMHO:

  • One reason to keep lambda-events separate from lambda is b/c a shapeless dependency (historically) was a big deal. Not sure if that's true anymore, and irrelevant to Scala 3 or if we hand-write our encoders/decoders.
  • It's a bit weird if some events are in lambda-events and others are with specialized lambdas. But also, these models aren't really shared interfaces, just kind of helpers ... if e.g. the CloudFormation lambda wants to define its own version of its events and eschew this dependency, I think that could be fine.
  • Besides defining events, it could be nice to define aliases for Lambda that pair events with results. This idea makes me consider if we should just merge lambda-events with lambda.
  • I'm open to merging lambda-natchez into lambda so we can offer tracing middleware out-of-the-box. natchez-core is lightweight.
  • I still don't think we need a specific XRay integration, but could totally be wrong.

sbt plugin

As described in https://gist.github.com/djspiewak/37a4ea0d7a5237144ec8b56a76ed080d

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.

I have an example of how to deploy an AWS lambda with SAM in https://github.com/christopherdavenport/js-test/tree/serverless.

Easy:

Hard:

  • Interact with the SAM YAML template. This seems to be a combination of:

    1. boilerplate our sbt plugin should provide, like the path to the generated JS, the name of the handler function
    2. config our sbt plugin could try and figure out, but hard to retrieve in practice: e.g. the type of event. These kinds of settings seem perfect for a g8 for various kinds of lambdas.
    3. any other custom user config

    Not sure what the best approach is. One option is to go full sbt-gh-actions and manage the template entirely from within sbt. Otherwise, will need some kind of hybrid approach.

Ideally we should have a plugin for both JVM lambdas and JS lambdas (or a hybrid plugin that can handle both) but let's focus on JS use-case for now since that's newer/more foreign to Scala folks. The nice thing is the SJS plugin can also adjust the Scala.js settings that tend to trip users up (namely, module configs).

Integrate with Cloudflare Workers

I started this in #4. It's an old and big PR, so I'll probably divvy it up.

Assorted notes:

  • There are both workers, and also these DurableObject things which I think get deployed independently. DurableObjects are hard/complex, and better for a follow-up.
  • Workers can now be written as ES6 modules. https://blog.cloudflare.com/workers-javascript-modules/
    scala-js/scala-js#3893
  • There is no local runtime for workers. They run in a custom V8 based runtime with Web Worker style APIs. So testing is going to be annoying. Aha, there is https://github.com/cloudflare/miniflare now :)
  • An sbt-plugin to deploy a worker should be easy, just uploading to an endpoint.
  • The JS sources for a worker are restricted to 1 mb gzipped. Looking at you, http4s MimeDB 👀

MVP / 0.1.0

@kubukoz lit the fire today to try and get a MVP out and @bpholt recently expressed similar sentiment in #60 (comment). We have lots of good stuff here, and people seem interested :)

TODO

  • #36
  • #38
  • #60 (my preference) or #51 ?
  • #52, #54, #55
  • Models for a few events, can someone tell me what the important ones are?
    #48
  • Docs and an example or two #88 #93

Based on the feedback I've gotten I would love to run with #60. It's somewhat complex, but very composable IMO, and we can offer lambdas with pre-baked stacks for users who want something simpler.

Is `LambdaEnv` powerful enough?

The LambdaEnv was born in #60 to help the implementation of lambda middlewares. But notably, LambdaEnv doesn't actually enable transformation of the Event or Context, like an http4s middleware does: it is read-only. This suits our purposes perfectly fine so far, but I am curious if there is some legit need to actually transform the Event before passing it on for further processing.

Should the return type of init really be a Resource?

I noticed in IOSetup, the finalizer for the Resource is never run (the finalizer returned from allocated is discarded). I think that's a reasonable thing to do, but it makes the typesignature of IOLambda.Simple#init a little disingenuous:

def init: Resource[IO, Init]

The user goes to the trouble of building a Resource, and probably assumes everything will be cleaned up when the Lambda instance is terminated, when in fact the finalizer is dropped on the floor.

Maybe def init: IO[Init] would be less misleading? Or at least a comment explaining that the finalizer won't be run would be helpful.

Properly finalize resources on Lambda shutdown

Spinning @bpholt's comment out of #134 (comment).

Currently, we don't run the finalizer for the Resource acquired at the Lambda's startup, which is probably okay in many instances but not ideal. Knowing when to run this finalizer is difficult without some notification/callback from the Lambda environment, and also being explicitly granted some time/backpressure to complete such finalization.

There does seem to be a Lambda Extensions API that supports a shutdown hook, but it's not obvious to me how that would help us inside the actual Lambda.

Shutdown: This phase is triggered if the Lambda function does not receive any invocations for a period of time. In the Shutdown phase, Lambda shuts down the runtime, alerts the extensions to let them stop cleanly, and then removes the environment. Lambda sends a Shutdown event to each extension, which tells the extension that the environment is about to be shut down.

https://docs.aws.amazon.com/lambda/latest/dg/runtimes-extensions-api.html

I also recall someone mentioning that running these finalizers can be time-consuming (i.e. expensive) and doesn't make sense in many cases. So, something to chew on.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.