Skip to content

Commit

Permalink
adoption of sendable (#252)
Browse files Browse the repository at this point in the history
motivation: adopt to sendable requirments in swift 5.6

changes:
* define sendable shims for protocols and structs that may be used in async context
* adjust tests
* add a test to make sure no warning are emitted
  • Loading branch information
tomerd authored Apr 14, 2022
1 parent 4d0bba4 commit 3c3529b
Show file tree
Hide file tree
Showing 7 changed files with 133 additions and 19 deletions.
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)
}
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
#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

0 comments on commit 3c3529b

Please sign in to comment.