Skip to content

Commit 3c3529b

Browse files
authored
adoption of sendable (#252)
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
1 parent 4d0bba4 commit 3c3529b

File tree

7 files changed

+133
-19
lines changed

7 files changed

+133
-19
lines changed

Sources/AWSLambdaRuntimeCore/LambdaContext.swift

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,15 @@
1212
//
1313
//===----------------------------------------------------------------------===//
1414

15+
#if compiler(>=5.6)
16+
@preconcurrency import Dispatch
17+
@preconcurrency import Logging
18+
@preconcurrency import NIOCore
19+
#else
1520
import Dispatch
1621
import Logging
1722
import NIOCore
23+
#endif
1824

1925
// MARK: - InitializationContext
2026

@@ -23,7 +29,7 @@ extension Lambda {
2329
/// The Lambda runtime generates and passes the `InitializationContext` to the Handlers
2430
/// ``ByteBufferLambdaHandler/makeHandler(context:)`` or ``LambdaHandler/init(context:)``
2531
/// as an argument.
26-
public struct InitializationContext {
32+
public struct InitializationContext: _AWSLambdaSendable {
2733
/// `Logger` to log with
2834
///
2935
/// - note: The `LogLevel` can be configured using the `LOG_LEVEL` environment variable.
@@ -67,17 +73,17 @@ extension Lambda {
6773

6874
/// Lambda runtime context.
6975
/// The Lambda runtime generates and passes the `Context` to the Lambda handler as an argument.
70-
public struct LambdaContext: CustomDebugStringConvertible {
71-
final class _Storage {
72-
var requestID: String
73-
var traceID: String
74-
var invokedFunctionARN: String
75-
var deadline: DispatchWallTime
76-
var cognitoIdentity: String?
77-
var clientContext: String?
78-
var logger: Logger
79-
var eventLoop: EventLoop
80-
var allocator: ByteBufferAllocator
76+
public struct LambdaContext: CustomDebugStringConvertible, _AWSLambdaSendable {
77+
final class _Storage: _AWSLambdaSendable {
78+
let requestID: String
79+
let traceID: String
80+
let invokedFunctionARN: String
81+
let deadline: DispatchWallTime
82+
let cognitoIdentity: String?
83+
let clientContext: String?
84+
let logger: Logger
85+
let eventLoop: EventLoop
86+
let allocator: ByteBufferAllocator
8187

8288
init(
8389
requestID: String,

Sources/AWSLambdaRuntimeCore/LambdaHandler.swift

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,13 +58,30 @@ extension LambdaHandler {
5858

5959
public func handle(_ event: Event, context: LambdaContext) -> EventLoopFuture<Output> {
6060
let promise = context.eventLoop.makePromise(of: Output.self)
61+
// using an unchecked sendable wrapper for the handler
62+
// this is safe since lambda runtime is designed to calls the handler serially
63+
let handler = UncheckedSendableHandler(underlying: self)
6164
promise.completeWithTask {
62-
try await self.handle(event, context: context)
65+
try await handler.handle(event, context: context)
6366
}
6467
return promise.futureResult
6568
}
6669
}
6770

71+
/// unchecked sendable wrapper for the handler
72+
/// this is safe since lambda runtime is designed to calls the handler serially
73+
@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *)
74+
fileprivate struct UncheckedSendableHandler<Underlying: LambdaHandler, Event, Output>: @unchecked Sendable where Event == Underlying.Event, Output == Underlying.Output {
75+
let underlying: Underlying
76+
77+
init(underlying: Underlying) {
78+
self.underlying = underlying
79+
}
80+
81+
func handle(_ event: Event, context: LambdaContext) async throws -> Output {
82+
try await self.underlying.handle(event, context: context)
83+
}
84+
}
6885
#endif
6986

7087
// MARK: - EventLoopLambdaHandler

Sources/AWSLambdaRuntimeCore/LambdaRuntime.swift

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,10 +63,17 @@ public final class LambdaRuntime<Handler: ByteBufferLambdaHandler> {
6363

6464
/// Start the `LambdaRuntime`.
6565
///
66-
/// - Returns: An `EventLoopFuture` that is fulfilled after the Lambda hander has been created and initiliazed, and a first run has been scheduled.
67-
///
68-
/// - note: This method must be called on the `EventLoop` the `LambdaRuntime` has been initialized with.
66+
/// - Returns: An `EventLoopFuture` that is fulfilled after the Lambda hander has been created and initialized, and a first run has been scheduled.
6967
public func start() -> EventLoopFuture<Void> {
68+
if self.eventLoop.inEventLoop {
69+
return self._start()
70+
} else {
71+
return self.eventLoop.flatSubmit { self._start() }
72+
}
73+
}
74+
75+
private func _start() -> EventLoopFuture<Void> {
76+
// This method must be called on the `EventLoop` the `LambdaRuntime` has been initialized with.
7077
self.eventLoop.assertInEventLoop()
7178

7279
logger.info("lambda runtime starting with \(self.configuration)")
@@ -189,3 +196,8 @@ public final class LambdaRuntime<Handler: ByteBufferLambdaHandler> {
189196
}
190197
}
191198
}
199+
200+
/// This is safe since lambda runtime synchronizes by dispatching all methods to a single `EventLoop`
201+
#if compiler(>=5.5) && canImport(_Concurrency)
202+
extension LambdaRuntime: @unchecked Sendable {}
203+
#endif

Sources/AWSLambdaRuntimeCore/LambdaRuntimeClient.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,13 @@
1313
//===----------------------------------------------------------------------===//
1414

1515
import Logging
16+
#if compiler(>=5.6)
17+
@preconcurrency import NIOCore
18+
@preconcurrency import NIOHTTP1
19+
#else
1620
import NIOCore
1721
import NIOHTTP1
22+
#endif
1823

1924
/// An HTTP based client for AWS Runtime Engine. This encapsulates the RESTful methods exposed by the Runtime Engine:
2025
/// * /runtime/invocation/next
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the SwiftAWSLambdaRuntime open source project
4+
//
5+
// Copyright (c) 2022 Apple Inc. and the SwiftAWSLambdaRuntime project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
// Sendable bridging types
16+
17+
#if compiler(>=5.6)
18+
public typealias _AWSLambdaSendable = Sendable
19+
#else
20+
public typealias _AWSLambdaSendable = Any
21+
#endif

Sources/AWSLambdaRuntimeCore/Terminator.swift

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import NIOCore
1818
/// Lambda terminator.
1919
/// Utility to manage the lambda shutdown sequence.
2020
public final class LambdaTerminator {
21-
private typealias Handler = (EventLoop) -> EventLoopFuture<Void>
21+
fileprivate typealias Handler = (EventLoop) -> EventLoopFuture<Void>
2222

2323
private var storage: Storage
2424

@@ -99,7 +99,7 @@ extension LambdaTerminator {
9999
}
100100

101101
extension LambdaTerminator {
102-
private final class Storage {
102+
fileprivate final class Storage {
103103
private let lock: Lock
104104
private var index: [RegistrationKey]
105105
private var map: [RegistrationKey: (name: String, handler: Handler)]
@@ -137,3 +137,10 @@ extension LambdaTerminator {
137137
let underlying: [Error]
138138
}
139139
}
140+
141+
// Ideally this would not be @unchecked Sendable, but Sendable checks do not understand locks
142+
// We can transition this to an actor once we drop support for older Swift versions
143+
#if compiler(>=5.5) && canImport(_Concurrency)
144+
extension LambdaTerminator: @unchecked Sendable {}
145+
extension LambdaTerminator.Storage: @unchecked Sendable {}
146+
#endif

Tests/AWSLambdaRuntimeCoreTests/LambdaTest.swift

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,14 @@
1313
//===----------------------------------------------------------------------===//
1414

1515
@testable import AWSLambdaRuntimeCore
16+
#if compiler(>=5.6)
17+
@preconcurrency import Logging
18+
@preconcurrency import NIOPosix
19+
#else
1620
import Logging
17-
import NIOCore
1821
import NIOPosix
22+
#endif
23+
import NIOCore
1924
import XCTest
2025

2126
class LambdaTest: XCTestCase {
@@ -250,6 +255,47 @@ class LambdaTest: XCTestCase {
250255
XCTAssertLessThanOrEqual(context.getRemainingTime(), .seconds(1))
251256
XCTAssertGreaterThan(context.getRemainingTime(), .milliseconds(800))
252257
}
258+
259+
#if compiler(>=5.6)
260+
func testSendable() async throws {
261+
struct Handler: EventLoopLambdaHandler {
262+
typealias Event = String
263+
typealias Output = String
264+
265+
static func makeHandler(context: Lambda.InitializationContext) -> EventLoopFuture<Handler> {
266+
context.eventLoop.makeSucceededFuture(Handler())
267+
}
268+
269+
func handle(_ event: String, context: LambdaContext) -> EventLoopFuture<String> {
270+
context.eventLoop.makeSucceededFuture("hello")
271+
}
272+
}
273+
274+
let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)
275+
defer { XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) }
276+
277+
let server = try MockLambdaServer(behavior: Behavior()).start().wait()
278+
defer { XCTAssertNoThrow(try server.stop().wait()) }
279+
280+
let logger = Logger(label: "TestLogger")
281+
let configuration = Lambda.Configuration(runtimeEngine: .init(requestTimeout: .milliseconds(100)))
282+
283+
let handler1 = Handler()
284+
let task = Task.detached {
285+
print(configuration.description)
286+
logger.info("hello")
287+
let runner = Lambda.Runner(eventLoop: eventLoopGroup.next(), configuration: configuration)
288+
289+
try runner.run(logger: logger, handler: handler1).wait()
290+
291+
try runner.initialize(logger: logger, terminator: LambdaTerminator(), handlerType: Handler.self).flatMap { handler2 in
292+
runner.run(logger: logger, handler: handler2)
293+
}.wait()
294+
}
295+
296+
try await task.value
297+
}
298+
#endif
253299
}
254300

255301
private struct Behavior: LambdaServerBehavior {

0 commit comments

Comments
 (0)