Skip to content

Improves Developer Ergonomics for LambdaHandler Conformances #272

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

Closed
wants to merge 14 commits into from
Closed
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions Sources/AWSLambdaRuntimeCore/HTTPClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -172,8 +172,6 @@ private final class LambdaChannelHandler: ChannelDuplexHandler {
private var state: State = .idle
private var lastError: Error?

init() {}

func write(context: ChannelHandlerContext, data: NIOAny, promise: EventLoopPromise<Void>?) {
guard case .idle = self.state else {
preconditionFailure("invalid state, outstanding request")
Expand Down
7 changes: 3 additions & 4 deletions Sources/AWSLambdaRuntimeCore/Lambda+LocalServer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,17 +36,16 @@ extension Lambda {
/// - body: Code to run within the context of the mock server. Typically this would be a Lambda.run function call.
///
/// - note: This API is designed strictly for local testing and is behind a DEBUG flag
internal static func withLocalServer<Value>(invocationEndpoint: String? = nil, _ body: @escaping () -> Value) throws -> Value {
internal static func startLocalServer(invocationEndpoint: String? = nil) throws -> LocalLambda.Server {
let server = LocalLambda.Server(invocationEndpoint: invocationEndpoint)
try server.start().wait()
defer { try! server.stop() }
return body()
return server
}
}

// MARK: - Local Mock Server

private enum LocalLambda {
internal enum LocalLambda {
struct Server {
private let logger: Logger
private let group: EventLoopGroup
Expand Down
83 changes: 42 additions & 41 deletions Sources/AWSLambdaRuntimeCore/Lambda.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,60 +33,61 @@ public enum Lambda {
/// - handlerType: The Handler to create and invoke.
///
/// - note: This is a blocking operation that will run forever, as its lifecycle is managed by the AWS Lambda Runtime Engine.
@discardableResult
internal static func run<Handler: ByteBufferLambdaHandler>(
configuration: LambdaConfiguration = .init(),
handlerType: Handler.Type
) -> Result<Int, Error> {
let _run = { (configuration: LambdaConfiguration) -> Result<Int, Error> in
Backtrace.install()
var logger = Logger(label: "Lambda")
logger.logLevel = configuration.general.logLevel
) throws -> Int {
Copy link
Author

@hectormatos2011 hectormatos2011 Sep 30, 2022

Choose a reason for hiding this comment

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

The changes here is just code cleanup that should make the binary a little smaller. The logic here is exactly the same and removes the implicitly unwrapped optional we had for var result: Result<Int, Error>! I can revert this if you don't like it.

var result: Result<Int, Error> = .success(0)

// start local server for debugging in DEBUG mode only
#if DEBUG
var localServer: LocalLambda.Server? = nil
if Handler.isLocalServer {
localServer = try Lambda.startLocalServer()
}
#endif

var result: Result<Int, Error>!
MultiThreadedEventLoopGroup.withCurrentThreadAsEventLoop { eventLoop in
let runtime = LambdaRuntime<Handler>(eventLoop: eventLoop, logger: logger, configuration: configuration)
Backtrace.install()
var logger = Logger(label: "Lambda")
logger.logLevel = configuration.general.logLevel

MultiThreadedEventLoopGroup.withCurrentThreadAsEventLoop { eventLoop in
let runtime = LambdaRuntime<Handler>(eventLoop: eventLoop, logger: logger, configuration: configuration)
#if DEBUG
let signalSource = trap(signal: configuration.lifecycle.stopSignal) { signal in
logger.info("intercepted signal: \(signal)")
runtime.shutdown()
}
#endif

runtime.start().flatMap {
runtime.shutdownFuture
}.whenComplete { lifecycleResult in
#if DEBUG
let signalSource = trap(signal: configuration.lifecycle.stopSignal) { signal in
logger.info("intercepted signal: \(signal)")
runtime.shutdown()
}
signalSource.cancel()
#endif

runtime.start().flatMap {
runtime.shutdownFuture
}.whenComplete { lifecycleResult in
#if DEBUG
signalSource.cancel()
#endif
eventLoop.shutdownGracefully { error in
if let error = error {
preconditionFailure("Failed to shutdown eventloop: \(error)")
}
eventLoop.shutdownGracefully { error in
if let error = error {
preconditionFailure("Failed to shutdown eventloop: \(error)")
}
result = lifecycleResult
}
result = lifecycleResult
}

logger.info("shutdown completed")
return result
}

// start local server for debugging in DEBUG mode only
logger.info("shutdown completed")

#if DEBUG
if Lambda.env("LOCAL_LAMBDA_SERVER_ENABLED").flatMap(Bool.init) ?? false {
do {
return try Lambda.withLocalServer {
_run(configuration)
}
} catch {
return .failure(error)
}
} else {
return _run(configuration)
}
#else
return _run(configuration)
try localServer?.stop()
#endif

switch result {
case .success(let count):
return count
case .failure(let error):
throw error
}
}
}

Expand Down
35 changes: 8 additions & 27 deletions Sources/AWSLambdaRuntimeCore/LambdaConfiguration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,42 +17,23 @@ import Logging
import NIOCore

internal struct LambdaConfiguration: CustomStringConvertible {
let general: General
let lifecycle: Lifecycle
let runtimeEngine: RuntimeEngine

init() {
self.init(general: .init(), lifecycle: .init(), runtimeEngine: .init())
}

init(general: General? = nil, lifecycle: Lifecycle? = nil, runtimeEngine: RuntimeEngine? = nil) {
self.general = general ?? General()
self.lifecycle = lifecycle ?? Lifecycle()
self.runtimeEngine = runtimeEngine ?? RuntimeEngine()
}
var general: General = .init()
var lifecycle: Lifecycle = .init()
var runtimeEngine: RuntimeEngine = .init()
Copy link
Author

Choose a reason for hiding this comment

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

This is probably unnecessary if you don't like it but I updated this to be easier to read. You don't necessarily need the init functions you had before if these are just structs. Updating to vars for these internal structs still keeps the same ability to initialize these structs with the default values you had defined in your previous init functions.

Let me know if you don't want this!


struct General: CustomStringConvertible {
let logLevel: Logger.Level

init(logLevel: Logger.Level? = nil) {
self.logLevel = logLevel ?? Lambda.env("LOG_LEVEL").flatMap(Logger.Level.init) ?? .info
}
var logLevel = Lambda.env("LOG_LEVEL").flatMap(Logger.Level.init) ?? .info

var description: String {
"\(General.self)(logLevel: \(self.logLevel))"
}
}

struct Lifecycle: CustomStringConvertible {
let id: String
let maxTimes: Int
let stopSignal: Signal

init(id: String? = nil, maxTimes: Int? = nil, stopSignal: Signal? = nil) {
self.id = id ?? "\(DispatchTime.now().uptimeNanoseconds)"
self.maxTimes = maxTimes ?? Lambda.env("MAX_REQUESTS").flatMap(Int.init) ?? 0
self.stopSignal = stopSignal ?? Lambda.env("STOP_SIGNAL").flatMap(Int32.init).flatMap(Signal.init) ?? Signal.TERM
precondition(self.maxTimes >= 0, "maxTimes must be equal or larger than 0")
var id: String = "\(DispatchTime.now().uptimeNanoseconds)"
var stopSignal: Signal = Lambda.env("STOP_SIGNAL").flatMap(Int32.init).flatMap(Signal.init) ?? Signal.TERM
var maxTimes: Int = Lambda.env("MAX_REQUESTS").flatMap(Int.init) ?? 0 {
didSet { precondition(self.maxTimes >= 0, "maxTimes must be equal or larger than 0") }
}

var description: String {
Expand Down
9 changes: 4 additions & 5 deletions Sources/AWSLambdaRuntimeCore/LambdaContext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,12 @@

#if compiler(>=5.6)
@preconcurrency import Dispatch
@preconcurrency import Logging
@preconcurrency import NIOCore
Copy link
Author

Choose a reason for hiding this comment

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

Xcode 14 complains about these. I don't think they're needed anymore? If this breaks something before Xcode 14 I may have to remove this

#else
import Dispatch
#endif

import Logging
import NIOCore
#endif

// MARK: - InitializationContext

Expand All @@ -45,14 +44,14 @@ public struct LambdaInitializationContext: _AWSLambdaSendable {

/// ``LambdaTerminator`` to register shutdown operations.
public let terminator: LambdaTerminator

init(logger: Logger, eventLoop: EventLoop, allocator: ByteBufferAllocator, terminator: LambdaTerminator) {
self.eventLoop = eventLoop
self.logger = logger
self.allocator = allocator
self.terminator = terminator
}

/// This interface is not part of the public API and must not be used by adopters. This API is not part of semver versioning.
public static func __forTestsOnly(
logger: Logger,
Expand Down
71 changes: 68 additions & 3 deletions Sources/AWSLambdaRuntimeCore/LambdaHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,24 @@ import NIOCore
/// ``ByteBufferLambdaHandler``.
@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *)
public protocol LambdaHandler: EventLoopLambdaHandler {
/// The Lambda initialization method.
/// The lambda functions input. In most cases this should be `Codable`. If your event originates from an
/// AWS service, have a look at [AWSLambdaEvents](https://github.com/swift-server/swift-aws-lambda-events),
/// which provides a number of commonly used AWS Event implementations.
associatedtype Event = Self.Event where Event == Self.Event
/// The lambda functions output. Can be `Void`.
associatedtype Output = Self.Output where Output == Self.Output

/// The empty Lambda initialization method.
/// Use this method to initialize resources that will be used in every request.
///
/// Examples for this can be HTTP or database clients.
init() async throws

/// The Lambda initialization method.
/// Use this method to initialize resources that will be used in every request. Defaults to
/// calling ``LambdaHandler/init()``
///
/// Examples for this can be HTTP or database clients.
/// - parameters:
/// - context: Runtime ``LambdaInitializationContext``.
init(context: LambdaInitializationContext) async throws
Expand All @@ -48,6 +62,10 @@ public protocol LambdaHandler: EventLoopLambdaHandler {

@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *)
extension LambdaHandler {
public init(context: LambdaInitializationContext) async throws {
try await self.init()
}

public static func makeHandler(context: LambdaInitializationContext) -> EventLoopFuture<Self> {
let promise = context.eventLoop.makePromise(of: Self.self)
promise.completeWithTask {
Expand Down Expand Up @@ -176,6 +194,36 @@ extension EventLoopLambdaHandler where Output == Void {
/// ``LambdaHandler`` based APIs.
/// Most users are not expected to use this protocol.
public protocol ByteBufferLambdaHandler {
#if DEBUG
/// Informs the Lambda whether or not it should run as a local server.
///
/// If not implemented, this variable has a default value that follows this priority:
///
/// 1. The value of the `LOCAL_LAMBDA_SERVER_ENABLED` environment variable.
/// 2. If the env variable isn't found, defaults to `true` if running directly in Xcode.
/// 3. If not running in Xcode and the env variable is missing, defaults to `false`.
/// 4. No-op on `RELEASE` (production) builds. The AWSLambdaRuntime framework will not compile
/// any logic accessing this property.
///
/// The following is an example of this variable within a simple ``LambdaHandler`` that uses
/// `Codable` types for its associated `Event` and `Output` types:
///
/// ```swift
/// import AWSLambdaRuntime
/// import Foundation
///
/// @main
/// struct EntryHandler: LambdaHandler {
/// static let isLocalServer = true
///
/// func handle(_ event: Event, context: LambdaContext) async throws -> Output {
/// try await client.processResponse(for: event)
/// }
/// }
/// ```
static var isLocalServer: Bool { get }
#endif

/// Create your Lambda handler for the runtime.
///
/// Use this to initialize all your resources that you want to cache between invocations. This could be database
Expand All @@ -197,6 +245,23 @@ public protocol ByteBufferLambdaHandler {
}

extension ByteBufferLambdaHandler {
#if DEBUG
/// If running this Lambda in Xcode, this value defaults to `true` if the presence of the
/// `LOCAL_LAMBDA_SERVER_ENABLED` environment variable cannot be found. Otherwise, this value
/// defaults to `false`.
public static var isLocalServer: Bool {
var enabled = Lambda.env("LOCAL_LAMBDA_SERVER_ENABLED").flatMap(Bool.init)

#if Xcode
if enabled == nil {
enabled = true
}
#endif

return enabled ?? false
}
#endif

/// Initializes and runs the Lambda function.
///
/// If you precede your ``ByteBufferLambdaHandler`` conformer's declaration with the
Expand All @@ -205,8 +270,8 @@ extension ByteBufferLambdaHandler {
///
/// The lambda runtime provides a default implementation of the method that manages the launch
/// process.
public static func main() {
_ = Lambda.run(configuration: .init(), handlerType: Self.self)
public static func main() throws {
try Lambda.run(configuration: .init(), handlerType: Self.self)
}
}

Expand Down
5 changes: 0 additions & 5 deletions Sources/AWSLambdaRuntimeCore/LambdaRuntimeClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,8 @@
//===----------------------------------------------------------------------===//

import Logging
#if compiler(>=5.6)
@preconcurrency import NIOCore
@preconcurrency import NIOHTTP1
#else
Copy link
Author

Choose a reason for hiding this comment

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

Same as before, Xcode 14 says these aren't needed. Will revert this change if it breaks something before Swift 5.7

import NIOCore
import NIOHTTP1
#endif

/// An HTTP based client for AWS Runtime Engine. This encapsulates the RESTful methods exposed by the Runtime Engine:
/// * /runtime/invocation/next
Expand Down
6 changes: 1 addition & 5 deletions Sources/AWSLambdaRuntimeCore/Terminator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,7 @@ import NIOCore
public final class LambdaTerminator {
fileprivate typealias Handler = (EventLoop) -> EventLoopFuture<Void>

private var storage: Storage

init() {
self.storage = Storage()
}
Copy link
Author

Choose a reason for hiding this comment

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

More code cleanup. You don't need the init if all you're doing is providing a default value to the private storage variable

private var storage: Storage = Storage()

/// Register a shutdown handler with the terminator.
///
Expand Down
Loading