Skip to content

[WIP] DO NOT MERGE - [core] Remove @unchecked Sendable from Lambda Runtime (depends on PR#508) #526

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 3 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
Original file line number Diff line number Diff line change
Expand Up @@ -22,58 +22,59 @@ import AWSLambdaRuntime
// This code is shown for the example only and is not used in this demo.
// This code doesn't perform any type of token validation. It should be used as a reference only.
let policyAuthorizerHandler:
(APIGatewayLambdaAuthorizerRequest, LambdaContext) async throws -> APIGatewayLambdaAuthorizerPolicyResponse = {
(request: APIGatewayLambdaAuthorizerRequest, context: LambdaContext) in
(APIGatewayLambdaAuthorizerRequest, LambdaContext) async throws ->
APIGatewayLambdaAuthorizerPolicyResponse = {
(request: APIGatewayLambdaAuthorizerRequest, context: LambdaContext) in

context.logger.debug("+++ Policy Authorizer called +++")
context.logger.debug("+++ Policy Authorizer called +++")

// typically, this function will check the validity of the incoming token received in the request
// typically, this function will check the validity of the incoming token received in the request

// then it creates and returns a response
return APIGatewayLambdaAuthorizerPolicyResponse(
principalId: "John Appleseed",
// then it creates and returns a response
return APIGatewayLambdaAuthorizerPolicyResponse(
principalId: "John Appleseed",

// this policy allows the caller to invoke any API Gateway endpoint
policyDocument: .init(statement: [
.init(
action: "execute-api:Invoke",
effect: .allow,
resource: "*"
)
// this policy allows the caller to invoke any API Gateway endpoint
policyDocument: .init(statement: [
.init(
action: "execute-api:Invoke",
effect: .allow,
resource: "*"
)

]),
]),

// this is additional context we want to return to the caller
context: [
"abc1": "xyz1",
"abc2": "xyz2",
]
)
}
// this is additional context we want to return to the caller
context: [
"abc1": "xyz1",
"abc2": "xyz2",
]
)
}
// let runtime = LambdaRuntime() { try await policyAuthorizerHandler($0, $1) }
// start polling for new events.
// try await runtime.run()

//
// This is an example of a simple authorizer that always authorizes the request.
// A simple authorizer returns a yes/no decision and optional context key-value pairs
//
// This code doesn't perform any type of token validation. It should be used as a reference only.
let simpleAuthorizerHandler:
(APIGatewayLambdaAuthorizerRequest, LambdaContext) async throws -> APIGatewayLambdaAuthorizerSimpleResponse = {
(_: APIGatewayLambdaAuthorizerRequest, context: LambdaContext) in
let runtime = LambdaRuntime {
(_: APIGatewayLambdaAuthorizerRequest, context: LambdaContext) -> APIGatewayLambdaAuthorizerSimpleResponse in

context.logger.debug("+++ Simple Authorizer called +++")
context.logger.debug("+++ Simple Authorizer called +++")

// typically, this function will check the validity of the incoming token received in the request
// typically, this function will check the validity of the incoming token received in the request

return APIGatewayLambdaAuthorizerSimpleResponse(
// this is the authorization decision: yes or no
isAuthorized: true,
return APIGatewayLambdaAuthorizerSimpleResponse(
// this is the authorization decision: yes or no
isAuthorized: true,

// this is additional context we want to return to the caller
context: ["abc1": "xyz1"]
)
}
// this is additional context we want to return to the caller
context: ["abc1": "xyz1"]
)
}

// create the runtime and start polling for new events.
// in this demo we use the simple authorizer handler
let runtime = LambdaRuntime(body: simpleAuthorizerHandler)
// start polling for new events.
try await runtime.run()
4 changes: 2 additions & 2 deletions Sources/AWSLambdaRuntime/FoundationSupport/Lambda+JSON.swift
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ extension LambdaRuntime {
decoder: JSONDecoder = JSONDecoder(),
encoder: JSONEncoder = JSONEncoder(),
logger: Logger = Logger(label: "LambdaRuntime"),
body: sending @escaping (Event, LambdaContext) async throws -> Output
body: @Sendable @escaping (Event, LambdaContext) async throws -> Output
)
where
Handler == LambdaCodableAdapter<
Expand All @@ -122,7 +122,7 @@ extension LambdaRuntime {
public convenience init<Event: Decodable>(
decoder: JSONDecoder = JSONDecoder(),
logger: Logger = Logger(label: "LambdaRuntime"),
body: sending @escaping (Event, LambdaContext) async throws -> Void
body: @Sendable @escaping (Event, LambdaContext) async throws -> Void
)
where
Handler == LambdaCodableAdapter<
Expand Down
16 changes: 12 additions & 4 deletions Sources/AWSLambdaRuntime/Lambda+Codable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import NIOCore

/// The protocol a decoder must conform to so that it can be used with ``LambdaCodableAdapter`` to decode incoming
/// `ByteBuffer` events.
public protocol LambdaEventDecoder {
public protocol LambdaEventDecoder: Sendable {
/// Decode the `ByteBuffer` representing the received event into the generic `Event` type
/// the handler will receive.
/// - Parameters:
Expand All @@ -28,7 +28,7 @@ public protocol LambdaEventDecoder {

/// The protocol an encoder must conform to so that it can be used with ``LambdaCodableAdapter`` to encode the generic
/// ``LambdaOutputEncoder/Output`` object into a `ByteBuffer`.
public protocol LambdaOutputEncoder {
public protocol LambdaOutputEncoder: Sendable {
associatedtype Output

/// Encode the generic type `Output` the handler has returned into a `ByteBuffer`.
Expand All @@ -52,7 +52,7 @@ public struct LambdaHandlerAdapter<
Event: Decodable,
Output,
Handler: LambdaHandler
>: LambdaWithBackgroundProcessingHandler where Handler.Event == Event, Handler.Output == Output {
>: LambdaWithBackgroundProcessingHandler where Handler.Event == Event, Handler.Output == Output, Handler: Sendable {
@usableFromInline let handler: Handler

/// Initializes an instance given a concrete handler.
Expand Down Expand Up @@ -86,7 +86,15 @@ public struct LambdaCodableAdapter<
Output,
Decoder: LambdaEventDecoder,
Encoder: LambdaOutputEncoder
>: StreamingLambdaHandler where Handler.Event == Event, Handler.Output == Output, Encoder.Output == Output {
>: StreamingLambdaHandler
where
Handler.Event == Event,
Handler.Output == Output,
Encoder.Output == Output,
Handler: Sendable,
Decoder: Sendable,
Encoder: Sendable
{
@usableFromInline let handler: Handler
@usableFromInline let encoder: Encoder
@usableFromInline let decoder: Decoder
Expand Down
16 changes: 8 additions & 8 deletions Sources/AWSLambdaRuntime/LambdaHandlers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import NIOCore
/// Background work can also be executed after returning the response. After closing the response stream by calling
/// ``LambdaResponseStreamWriter/finish()`` or ``LambdaResponseStreamWriter/writeAndFinish(_:)``,
/// the ``handle(_:responseWriter:context:)`` function is free to execute any background work.
public protocol StreamingLambdaHandler: _Lambda_SendableMetatype {
public protocol StreamingLambdaHandler: Sendable, _Lambda_SendableMetatype {
/// The handler function -- implement the business logic of the Lambda function here.
/// - Parameters:
/// - event: The invocation's input data.
Expand Down Expand Up @@ -65,7 +65,7 @@ public protocol LambdaResponseStreamWriter {
///
/// - note: This handler protocol does not support response streaming because the output has to be encoded prior to it being sent, e.g. it is not possible to encode a partial/incomplete JSON string.
/// This protocol also does not support the execution of background work after the response has been returned -- the ``LambdaWithBackgroundProcessingHandler`` protocol caters for such use-cases.
public protocol LambdaHandler {
public protocol LambdaHandler: Sendable {
/// Generic input type.
/// The body of the request sent to Lambda will be decoded into this type for the handler to consume.
associatedtype Event
Expand All @@ -87,7 +87,7 @@ public protocol LambdaHandler {
/// ``LambdaResponseWriter``that is passed in as an argument, meaning that the
/// ``LambdaWithBackgroundProcessingHandler/handle(_:outputWriter:context:)`` function is then
/// free to implement any background work after the result has been sent to the AWS Lambda control plane.
public protocol LambdaWithBackgroundProcessingHandler {
public protocol LambdaWithBackgroundProcessingHandler: Sendable {
/// Generic input type.
/// The body of the request sent to Lambda will be decoded into this type for the handler to consume.
associatedtype Event
Expand Down Expand Up @@ -151,17 +151,17 @@ public struct StreamingClosureHandler: StreamingLambdaHandler {
/// A ``LambdaHandler`` conforming handler object that can be constructed with a closure.
/// Allows for a handler to be defined in a clean manner, leveraging Swift's trailing closure syntax.
public struct ClosureHandler<Event: Decodable, Output>: LambdaHandler {
let body: (Event, LambdaContext) async throws -> Output
let body: @Sendable (Event, LambdaContext) async throws -> Output

/// Initialize with a closure handler over generic `Input` and `Output` types.
/// - Parameter body: The handler function written as a closure.
public init(body: sending @escaping (Event, LambdaContext) async throws -> Output) where Output: Encodable {
public init(body: @Sendable @escaping (Event, LambdaContext) async throws -> Output) where Output: Encodable {
self.body = body
}

/// Initialize with a closure handler over a generic `Input` type, and a `Void` `Output`.
/// - Parameter body: The handler function written as a closure.
public init(body: @escaping (Event, LambdaContext) async throws -> Void) where Output == Void {
public init(body: @Sendable @escaping (Event, LambdaContext) async throws -> Void) where Output == Void {
self.body = body
}

Expand Down Expand Up @@ -202,7 +202,7 @@ extension LambdaRuntime {
encoder: sending Encoder,
decoder: sending Decoder,
logger: Logger = Logger(label: "LambdaRuntime"),
body: sending @escaping (Event, LambdaContext) async throws -> Output
body: @Sendable @escaping (Event, LambdaContext) async throws -> Output
)
where
Handler == LambdaCodableAdapter<
Expand Down Expand Up @@ -232,7 +232,7 @@ extension LambdaRuntime {
public convenience init<Event: Decodable, Decoder: LambdaEventDecoder>(
decoder: sending Decoder,
logger: Logger = Logger(label: "LambdaRuntime"),
body: sending @escaping (Event, LambdaContext) async throws -> Void
body: @Sendable @escaping (Event, LambdaContext) async throws -> Void
)
where
Handler == LambdaCodableAdapter<
Expand Down
22 changes: 4 additions & 18 deletions Sources/AWSLambdaRuntime/LambdaRuntime.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,9 @@ import FoundationEssentials
import Foundation
#endif

// We need `@unchecked` Sendable here, as `NIOLockedValueBox` does not understand `sending` today.
// We don't want to use `NIOLockedValueBox` here anyway. We would love to use Mutex here, but this
// sadly crashes the compiler today.
public final class LambdaRuntime<Handler>: @unchecked Sendable where Handler: StreamingLambdaHandler {
// TODO: We want to change this to Mutex as soon as this doesn't crash the Swift compiler on Linux anymore
public final class LambdaRuntime<Handler>: Sendable where Handler: StreamingLambdaHandler {
@usableFromInline
let handlerMutex: NIOLockedValueBox<Handler?>
let handler: Handler
@usableFromInline
let logger: Logger
@usableFromInline
Expand All @@ -39,7 +35,7 @@ public final class LambdaRuntime<Handler>: @unchecked Sendable where Handler: St
eventLoop: EventLoop = Lambda.defaultEventLoop,
logger: Logger = Logger(label: "LambdaRuntime")
) {
self.handlerMutex = NIOLockedValueBox(handler)
self.handler = handler
self.eventLoop = eventLoop

// by setting the log level here, we understand it can not be changed dynamically at runtime
Expand All @@ -64,16 +60,6 @@ public final class LambdaRuntime<Handler>: @unchecked Sendable where Handler: St

@inlinable
internal func _run() async throws {
let handler = self.handlerMutex.withLockedValue { handler in
let result = handler
handler = nil
return result
}

guard let handler else {
throw LambdaRuntimeError(code: .runtimeCanOnlyBeStartedOnce)
}

// are we running inside an AWS Lambda runtime environment ?
// AWS_LAMBDA_RUNTIME_API is set when running on Lambda
// https://docs.aws.amazon.com/lambda/latest/dg/runtimes-api.html
Expand Down Expand Up @@ -110,7 +96,7 @@ public final class LambdaRuntime<Handler>: @unchecked Sendable where Handler: St
) { runtimeClient in
try await Lambda.runLoop(
runtimeClient: runtimeClient,
handler: handler,
handler: self.handler,
logger: self.logger
)
}
Expand Down