Skip to content
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

adoption of sendable #252

Merged
merged 5 commits into from
Apr 14, 2022
Merged
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
30 changes: 18 additions & 12 deletions Sources/AWSLambdaRuntimeCore/LambdaContext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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.
Expand Down Expand Up @@ -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,
Expand Down
19 changes: 18 additions & 1 deletion Sources/AWSLambdaRuntimeCore/LambdaHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -58,13 +58,30 @@ extension LambdaHandler {

public func handle(_ event: Event, context: LambdaContext) -> EventLoopFuture<Output> {
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)
tomerd marked this conversation as resolved.
Show resolved Hide resolved
}
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<Underlying: LambdaHandler, Event, Output>: @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
Expand Down
18 changes: 15 additions & 3 deletions Sources/AWSLambdaRuntimeCore/LambdaRuntime.swift
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,17 @@ public final class LambdaRuntime<Handler: ByteBufferLambdaHandler> {

/// 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<Void> {
if self.eventLoop.inEventLoop {
return self._start()
} else {
return self.eventLoop.flatSubmit { self._start() }
}
}

private func _start() -> EventLoopFuture<Void> {
// 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)")
Expand Down Expand Up @@ -189,3 +196,8 @@ public final class LambdaRuntime<Handler: ByteBufferLambdaHandler> {
}
}
}

/// 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
5 changes: 5 additions & 0 deletions Sources/AWSLambdaRuntimeCore/LambdaRuntimeClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
21 changes: 21 additions & 0 deletions Sources/AWSLambdaRuntimeCore/Sendable.swift
Original file line number Diff line number Diff line change
@@ -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
11 changes: 9 additions & 2 deletions Sources/AWSLambdaRuntimeCore/Terminator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import NIOCore
/// Lambda terminator.
/// Utility to manage the lambda shutdown sequence.
public final class LambdaTerminator {
private typealias Handler = (EventLoop) -> EventLoopFuture<Void>
fileprivate typealias Handler = (EventLoop) -> EventLoopFuture<Void>

private var storage: Storage

Expand Down Expand Up @@ -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)]
Expand Down Expand Up @@ -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
Copy link
Member

Choose a reason for hiding this comment

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

We can transition this to an actor once we have custom executors :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

that too

#if compiler(>=5.5) && canImport(_Concurrency)
extension LambdaTerminator: @unchecked Sendable {}
extension LambdaTerminator.Storage: @unchecked Sendable {}
#endif
48 changes: 47 additions & 1 deletion Tests/AWSLambdaRuntimeCoreTests/LambdaTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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<Handler> {
context.eventLoop.makeSucceededFuture(Handler())
}

func handle(_ event: String, context: LambdaContext) -> EventLoopFuture<String> {
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 {
Expand Down