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 all 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
1 change: 0 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@ let package = Package(
.product(name: "NIOFoundationCompat", package: "swift-nio"),
]),
.testTarget(name: "AWSLambdaRuntimeTests", dependencies: [
.byName(name: "AWSLambdaRuntimeCore"),
.byName(name: "AWSLambdaRuntime"),
]),
// testing helper
Expand Down
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
4 changes: 2 additions & 2 deletions Sources/AWSLambdaRuntimeCore/Lambda+String.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import NIOCore
extension EventLoopLambdaHandler where Event == String {
/// Implementation of a `ByteBuffer` to `String` decoding.
@inlinable
public func decode(buffer: ByteBuffer) throws -> String {
public func decode(buffer: ByteBuffer) throws -> Event {
Copy link
Author

Choose a reason for hiding this comment

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

These changes help to solve ambiguities in the swift compiler across default implementations of EventLoopLambdaHandler when importing AWSLambdaRuntime or running tests for the package in Xcode

var buffer = buffer
guard let string = buffer.readString(length: buffer.readableBytes) else {
fatalError("buffer.readString(length: buffer.readableBytes) failed")
Expand All @@ -28,7 +28,7 @@ extension EventLoopLambdaHandler where Event == String {
extension EventLoopLambdaHandler where Output == String {
/// Implementation of `String` to `ByteBuffer` encoding.
@inlinable
public func encode(allocator: ByteBufferAllocator, value: String) throws -> ByteBuffer? {
public func encode(allocator: ByteBufferAllocator, value: Output) throws -> ByteBuffer? {
// FIXME: reusable buffer
var buffer = allocator.buffer(capacity: value.utf8.count)
buffer.writeString(value)
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
29 changes: 4 additions & 25 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 Expand Up @@ -193,24 +192,4 @@ public struct LambdaContext: CustomDebugStringConvertible, _AWSLambdaSendable {
public var debugDescription: String {
"\(Self.self)(requestID: \(self.requestID), traceID: \(self.traceID), invokedFunctionARN: \(self.invokedFunctionARN), cognitoIdentity: \(self.cognitoIdentity ?? "nil"), clientContext: \(self.clientContext ?? "nil"), deadline: \(self.deadline))"
}

/// 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(
requestID: String,
traceID: String,
invokedFunctionARN: String,
timeout: DispatchTimeInterval,
logger: Logger,
eventLoop: EventLoop
) -> LambdaContext {
LambdaContext(
requestID: requestID,
traceID: traceID,
invokedFunctionARN: invokedFunctionARN,
deadline: .now() + timeout,
logger: logger,
eventLoop: eventLoop,
allocator: ByteBufferAllocator()
)
}
}
Loading