Skip to content

Allow integration with API Frameworks: Public Lifecycle #58

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

Merged
merged 3 commits into from
Apr 22, 2020

Conversation

fabianfett
Copy link
Member

@tachyonics Since you are planning something like this for smoke as well. Would this fit your needs?

Motivation

A common pattern in Lambda land is to allow apps written with common api frameworks to be run within Lambda behind an APIGateway or Application Load Balancer. An example for this is: awslabs/aws-lambda-go-api-proxy, which is were I used this pattern for myself for the very first time.

Since SiwftNIO doesn't have such a widespread middleware api for handling http requests as go does, there is no way for us to just allow extension of the lambda by merely presenting one middleware hook for developers.

But that doesn't mean we should not allow such a use-case. We need to find a way though that is easy to use for experienced framework developers, but not in the reach of early adopters. After trying out a number of things especially around injecting ELGs into Lambda.run(), I came to the conclusion, that the potentially best way is to open up Lambda.Lifecycle.

Changes

  • Lambda.Lifecycle is now public
  • there is a new public convenience initializer on Lambda.Lifecycle (so we don't need to leak Configuration)
  • the start, stop and shutdown methods are now public

Result

On a private remote I can prepare my vapor shim to be used with our new lambda runtime.

@fabianfett fabianfett requested a review from tomerd April 15, 2020 19:47
@tomerd
Copy link
Contributor

tomerd commented Apr 15, 2020

hey @fabianfett can you elaborate how making the lifecycle public helps? or iow, how you plan to use the lifecycle in order to integrate higher level frameworks like vapor, smoke, etc

@fabianfett
Copy link
Member Author

fabianfett commented Apr 16, 2020

@tomerd sure.

This is how I would use the Lambda.Lifecycle for Vapor. This implements the Server protocol and can for this reason replace the default HTTPServer:

public class LambdaServer: Server {
  
  struct Handler: EventLoopLambdaHandler {
    typealias In  = APIGateway.Request
    typealias Out = APIGateway.Response
    
    private let application: Application
    private let responder: Responder
    
    
    init(application: Application, responder: Responder) {
      self.application = application
      self.responder   = responder
    }
    
    public func handle(context: Lambda.Context, payload: APIGateway.Request)
      -> EventLoopFuture<APIGateway.Response>
    {
      context.logger.info("API.GatewayRequest: \(payload)")
      
      let vaporRequest: Vapor.Request
      do {
        vaporRequest = try Vapor.Request(req: payload, in: context, for: self.application)
      }
      catch {
        return context.eventLoop.makeFailedFuture(error)
      }
      
      return self.responder.respond(to: vaporRequest)
        .map { APIGateway.Response(response: $0) }
    }
  }
  
  private let application     : Application
  private let responder       : Responder
//  private let configuration   : Configuration
  private let eventLoopGroup  : EventLoopGroup
  private var lambdaLifecycle : Lambda.Lifecycle?
  
  private var onShutdownFuture: EventLoopFuture<Void>?
  
  init(application      : Application,
       responder        : Responder,
//       configuration  : Configuration,
       on eventLoopGroup: EventLoopGroup)
  {
    self.application     = application
    self.responder       = responder
    self.eventLoopGroup  = eventLoopGroup
  }
  
  public func start(hostname: String?, port: Int?) throws {
    
    let handler = Handler(application: self.application, responder: self.responder)
    
    self.lambdaLifecycle = Lambda.Lifecycle(
      eventLoop: eventLoopGroup.next(),
      logger: self.application.logger) {
        $0.makeSucceededFuture(handler)
    }
    
    self.onShutdownFuture = self.lambdaLifecycle?.start().map() { _ in Void() }
  }
  
  public var onShutdown: EventLoopFuture<Void> {
    return self.onShutdownFuture!
  }
  
  public func shutdown() {
    // ignore
    // should never be executed
    self.lambdaLifecycle?.shutdown()
    self.lambdaLifecycle = nil
  }
}

Thanks to some syntactic sugar, developers will be able to deploy their code to lambda by just adding two lines of code:

import VaporLambdaRuntime

app.servers.use(.lambda)

@tomerd
Copy link
Contributor

tomerd commented Apr 16, 2020

hey @fabianfett this is great imo, love the simple and elegant integration. that said, I am a bit concerned about exposing Lifecycle given the assumptions made there about the shutdown behavior. I need to think about this a bit more, either simplify Lifecycle or offer a different extension point that is less fragile

@tomerd
Copy link
Contributor

tomerd commented Apr 17, 2020

/cc @tachyonics to see if we can achieve smoke integration in a similar fashion

@tachyonics
Copy link
Contributor

I'm not clear what this would give us over the LambdaHandler integration point, which I was planning to use.

The current integration I have uses an initialiser protocol very similar to this-
https://github.com/amzn/smoke-framework-examples/blob/master/PersistenceExampleService/Sources/PersistenceExampleService/PersistenceExamplePerInvocationContextInitializer.swift

With the application started with

Lambda.runAsOperationServer(MyApplicationInitializer.init)

This is different but is consistent with how SmokeFramework starts up elsewhere (SmokeFramework 2.x has moved to something very like the Lambda startup which is an evolution on how SmokeFramework 1.x started up).

@tomerd tomerd force-pushed the public-lifecycle branch from f67c43d to 7d1da8c Compare April 22, 2020 20:10
@tomerd tomerd force-pushed the public-lifecycle branch from 7d1da8c to 19ea341 Compare April 22, 2020 20:12
@tomerd
Copy link
Contributor

tomerd commented Apr 22, 2020

hey @fabianfett as discussed, added a commit to make Lifecycle safe as a public API, lmk what you think

@tomerd
Copy link
Contributor

tomerd commented Apr 22, 2020

@tachyonics sounds good, was raising awareness in case the helped with smoke integration but looks like you already have that figured out so awesome!

self.logger.debug("lambda lifecycle stopping")
self.state = .stopping
public func shutdown() {
self.state = .shuttingdown
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should wrap this in an

eventLoop.submit {
  self.state = .shuttingdown
}

to ensure needed thread safety.

Copy link
Contributor

@tomerd tomerd Apr 22, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

state is protected with lock, right?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You‘re right. Sorry.

@fabianfett
Copy link
Member Author

@tomerd I‘m happy with this. Can be merged!

@tomerd tomerd merged commit 378adf8 into master Apr 22, 2020
@fabianfett fabianfett deleted the public-lifecycle branch April 23, 2020 06:36
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

Successfully merging this pull request may close these issues.

3 participants