diff --git a/Sources/AWSLambdaRuntimeCore/LambdaContext.swift b/Sources/AWSLambdaRuntimeCore/LambdaContext.swift index d6fec657..6a0596f0 100644 --- a/Sources/AWSLambdaRuntimeCore/LambdaContext.swift +++ b/Sources/AWSLambdaRuntimeCore/LambdaContext.swift @@ -12,9 +12,15 @@ // //===----------------------------------------------------------------------===// +#if compiler(>=5.6) +@preconcurrency import Dispatch +@preconcurrency import Logging +@preconcurrency import NIOCore +#else import Dispatch import Logging import NIOCore +#endif // MARK: - InitializationContext @@ -23,7 +29,7 @@ extension Lambda { /// The Lambda runtime generates and passes the `InitializationContext` to the Handlers /// ``ByteBufferLambdaHandler/makeHandler(context:)`` or ``LambdaHandler/init(context:)`` /// as an argument. - public struct InitializationContext { + public struct InitializationContext: _AWSLambdaSendable { /// `Logger` to log with /// /// - note: The `LogLevel` can be configured using the `LOG_LEVEL` environment variable. @@ -67,17 +73,17 @@ extension Lambda { /// Lambda runtime context. /// The Lambda runtime generates and passes the `Context` to the Lambda handler as an argument. -public struct LambdaContext: CustomDebugStringConvertible { - final class _Storage { - var requestID: String - var traceID: String - var invokedFunctionARN: String - var deadline: DispatchWallTime - var cognitoIdentity: String? - var clientContext: String? - var logger: Logger - var eventLoop: EventLoop - var allocator: ByteBufferAllocator +public struct LambdaContext: CustomDebugStringConvertible, _AWSLambdaSendable { + final class _Storage: _AWSLambdaSendable { + let requestID: String + let traceID: String + let invokedFunctionARN: String + let deadline: DispatchWallTime + let cognitoIdentity: String? + let clientContext: String? + let logger: Logger + let eventLoop: EventLoop + let allocator: ByteBufferAllocator init( requestID: String, diff --git a/Sources/AWSLambdaRuntimeCore/LambdaHandler.swift b/Sources/AWSLambdaRuntimeCore/LambdaHandler.swift index 76d35af2..8bb61179 100644 --- a/Sources/AWSLambdaRuntimeCore/LambdaHandler.swift +++ b/Sources/AWSLambdaRuntimeCore/LambdaHandler.swift @@ -58,13 +58,30 @@ extension LambdaHandler { public func handle(_ event: Event, context: LambdaContext) -> EventLoopFuture { let promise = context.eventLoop.makePromise(of: Output.self) + // using an unchecked sendable wrapper for the handler + // this is safe since lambda runtime is designed to calls the handler serially + let handler = UncheckedSendableHandler(underlying: self) promise.completeWithTask { - try await self.handle(event, context: context) + try await handler.handle(event, context: context) } return promise.futureResult } } +/// unchecked sendable wrapper for the handler +/// this is safe since lambda runtime is designed to calls the handler serially +@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *) +fileprivate struct UncheckedSendableHandler: @unchecked Sendable where Event == Underlying.Event, Output == Underlying.Output { + let underlying: Underlying + + init(underlying: Underlying) { + self.underlying = underlying + } + + func handle(_ event: Event, context: LambdaContext) async throws -> Output { + try await self.underlying.handle(event, context: context) + } +} #endif // MARK: - EventLoopLambdaHandler diff --git a/Sources/AWSLambdaRuntimeCore/LambdaRuntime.swift b/Sources/AWSLambdaRuntimeCore/LambdaRuntime.swift index 0619dfa1..19057e14 100644 --- a/Sources/AWSLambdaRuntimeCore/LambdaRuntime.swift +++ b/Sources/AWSLambdaRuntimeCore/LambdaRuntime.swift @@ -63,10 +63,17 @@ public final class LambdaRuntime { /// Start the `LambdaRuntime`. /// - /// - Returns: An `EventLoopFuture` that is fulfilled after the Lambda hander has been created and initiliazed, and a first run has been scheduled. - /// - /// - note: This method must be called on the `EventLoop` the `LambdaRuntime` has been initialized with. + /// - Returns: An `EventLoopFuture` that is fulfilled after the Lambda hander has been created and initialized, and a first run has been scheduled. public func start() -> EventLoopFuture { + if self.eventLoop.inEventLoop { + return self._start() + } else { + return self.eventLoop.flatSubmit { self._start() } + } + } + + private func _start() -> EventLoopFuture { + // This method must be called on the `EventLoop` the `LambdaRuntime` has been initialized with. self.eventLoop.assertInEventLoop() logger.info("lambda runtime starting with \(self.configuration)") @@ -189,3 +196,8 @@ public final class LambdaRuntime { } } } + +/// This is safe since lambda runtime synchronizes by dispatching all methods to a single `EventLoop` +#if compiler(>=5.5) && canImport(_Concurrency) +extension LambdaRuntime: @unchecked Sendable {} +#endif diff --git a/Sources/AWSLambdaRuntimeCore/LambdaRuntimeClient.swift b/Sources/AWSLambdaRuntimeCore/LambdaRuntimeClient.swift index 7303ef1c..ddb946a5 100644 --- a/Sources/AWSLambdaRuntimeCore/LambdaRuntimeClient.swift +++ b/Sources/AWSLambdaRuntimeCore/LambdaRuntimeClient.swift @@ -13,8 +13,13 @@ //===----------------------------------------------------------------------===// import Logging +#if compiler(>=5.6) +@preconcurrency import NIOCore +@preconcurrency import NIOHTTP1 +#else 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 diff --git a/Sources/AWSLambdaRuntimeCore/Sendable.swift b/Sources/AWSLambdaRuntimeCore/Sendable.swift new file mode 100644 index 00000000..936403e4 --- /dev/null +++ b/Sources/AWSLambdaRuntimeCore/Sendable.swift @@ -0,0 +1,21 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2022 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +// Sendable bridging types + +#if compiler(>=5.6) +public typealias _AWSLambdaSendable = Sendable +#else +public typealias _AWSLambdaSendable = Any +#endif diff --git a/Sources/AWSLambdaRuntimeCore/Terminator.swift b/Sources/AWSLambdaRuntimeCore/Terminator.swift index 9ad62d3a..bd1737e0 100644 --- a/Sources/AWSLambdaRuntimeCore/Terminator.swift +++ b/Sources/AWSLambdaRuntimeCore/Terminator.swift @@ -18,7 +18,7 @@ import NIOCore /// Lambda terminator. /// Utility to manage the lambda shutdown sequence. public final class LambdaTerminator { - private typealias Handler = (EventLoop) -> EventLoopFuture + fileprivate typealias Handler = (EventLoop) -> EventLoopFuture private var storage: Storage @@ -99,7 +99,7 @@ extension LambdaTerminator { } extension LambdaTerminator { - private final class Storage { + fileprivate final class Storage { private let lock: Lock private var index: [RegistrationKey] private var map: [RegistrationKey: (name: String, handler: Handler)] @@ -137,3 +137,10 @@ extension LambdaTerminator { let underlying: [Error] } } + +// Ideally this would not be @unchecked Sendable, but Sendable checks do not understand locks +// We can transition this to an actor once we drop support for older Swift versions +#if compiler(>=5.5) && canImport(_Concurrency) +extension LambdaTerminator: @unchecked Sendable {} +extension LambdaTerminator.Storage: @unchecked Sendable {} +#endif diff --git a/Tests/AWSLambdaRuntimeCoreTests/LambdaTest.swift b/Tests/AWSLambdaRuntimeCoreTests/LambdaTest.swift index 3da730f0..a5fd7daf 100644 --- a/Tests/AWSLambdaRuntimeCoreTests/LambdaTest.swift +++ b/Tests/AWSLambdaRuntimeCoreTests/LambdaTest.swift @@ -13,9 +13,14 @@ //===----------------------------------------------------------------------===// @testable import AWSLambdaRuntimeCore +#if compiler(>=5.6) +@preconcurrency import Logging +@preconcurrency import NIOPosix +#else import Logging -import NIOCore import NIOPosix +#endif +import NIOCore import XCTest class LambdaTest: XCTestCase { @@ -250,6 +255,47 @@ class LambdaTest: XCTestCase { XCTAssertLessThanOrEqual(context.getRemainingTime(), .seconds(1)) XCTAssertGreaterThan(context.getRemainingTime(), .milliseconds(800)) } + + #if compiler(>=5.6) + func testSendable() async throws { + struct Handler: EventLoopLambdaHandler { + typealias Event = String + typealias Output = String + + static func makeHandler(context: Lambda.InitializationContext) -> EventLoopFuture { + context.eventLoop.makeSucceededFuture(Handler()) + } + + func handle(_ event: String, context: LambdaContext) -> EventLoopFuture { + context.eventLoop.makeSucceededFuture("hello") + } + } + + let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) + defer { XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) } + + let server = try MockLambdaServer(behavior: Behavior()).start().wait() + defer { XCTAssertNoThrow(try server.stop().wait()) } + + let logger = Logger(label: "TestLogger") + let configuration = Lambda.Configuration(runtimeEngine: .init(requestTimeout: .milliseconds(100))) + + let handler1 = Handler() + let task = Task.detached { + print(configuration.description) + logger.info("hello") + let runner = Lambda.Runner(eventLoop: eventLoopGroup.next(), configuration: configuration) + + try runner.run(logger: logger, handler: handler1).wait() + + try runner.initialize(logger: logger, terminator: LambdaTerminator(), handlerType: Handler.self).flatMap { handler2 in + runner.run(logger: logger, handler: handler2) + }.wait() + } + + try await task.value + } + #endif } private struct Behavior: LambdaServerBehavior {