Skip to content

Commit 809008b

Browse files
authored
Merge branch 'master' into add-apigateway-and-alb
2 parents 63907c7 + 920302a commit 809008b

File tree

5 files changed

+305
-32
lines changed

5 files changed

+305
-32
lines changed

Package.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,12 @@ let package = Package(
88
.macOS(.v10_13),
99
],
1010
products: [
11+
// core library
1112
.library(name: "AWSLambdaRuntime", targets: ["AWSLambdaRuntime"]),
13+
// common AWS events
1214
.library(name: "AWSLambdaEvents", targets: ["AWSLambdaEvents"]),
15+
// for testing only
16+
.library(name: "AWSLambdaTesting", targets: ["AWSLambdaTesting"]),
1317
],
1418
dependencies: [
1519
.package(url: "https://github.com/apple/swift-nio.git", from: "2.8.0"),
@@ -26,6 +30,12 @@ let package = Package(
2630
.testTarget(name: "AWSLambdaRuntimeTests", dependencies: ["AWSLambdaRuntime"]),
2731
.target(name: "AWSLambdaEvents", dependencies: []),
2832
.testTarget(name: "AWSLambdaEventsTests", dependencies: ["AWSLambdaEvents"]),
33+
// testing helper
34+
.target(name: "AWSLambdaTesting", dependencies: [
35+
"AWSLambdaRuntime",
36+
.product(name: "NIO", package: "swift-nio"),
37+
]),
38+
.testTarget(name: "AWSLambdaTestingTests", dependencies: ["AWSLambdaTesting"]),
2939
// samples
3040
.target(name: "StringSample", dependencies: ["AWSLambdaRuntime"]),
3141
.target(name: "CodableSample", dependencies: ["AWSLambdaRuntime"]),

Sources/AWSLambdaRuntime/Lambda.swift

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -83,11 +83,12 @@ public enum Lambda {
8383
let lifecycle = Lifecycle(eventLoop: eventLoopGroup.next(), logger: logger, configuration: configuration, factory: factory)
8484
let signalSource = trap(signal: configuration.lifecycle.stopSignal) { signal in
8585
logger.info("intercepted signal: \(signal)")
86-
lifecycle.stop()
87-
}
88-
return lifecycle.start().always { _ in
8986
lifecycle.shutdown()
90-
signalSource.cancel()
87+
}
88+
return lifecycle.start().flatMap {
89+
return lifecycle.shutdownFuture.always { _ in
90+
signalSource.cancel()
91+
}
9192
}
9293
}
9394
}

Sources/AWSLambdaRuntime/LambdaLifecycle.swift

Lines changed: 54 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,30 @@ import NIO
1717
import NIOConcurrencyHelpers
1818

1919
extension Lambda {
20-
internal final class Lifecycle {
20+
/// `Lifecycle` manages the Lambda process lifecycle.
21+
public final class Lifecycle {
2122
private let eventLoop: EventLoop
23+
private let shutdownPromise: EventLoopPromise<Int>
2224
private let logger: Logger
2325
private let configuration: Configuration
2426
private let factory: LambdaHandlerFactory
2527

2628
private var _state = State.idle
2729
private let stateLock = Lock()
2830

31+
/// Create a new `Lifecycle`.
32+
///
33+
/// - parameters:
34+
/// - eventLoop: An `EventLoop` to run the Lambda on.
35+
/// - logger: A `Logger` to log the Lambda events.
36+
/// - factory: A `LambdaHandlerFactory` to create the concrete Lambda handler.
37+
public convenience init(eventLoop: EventLoop, logger: Logger, factory: @escaping LambdaHandlerFactory) {
38+
self.init(eventLoop: eventLoop, logger: logger, configuration: .init(), factory: factory)
39+
}
40+
2941
init(eventLoop: EventLoop, logger: Logger, configuration: Configuration, factory: @escaping LambdaHandlerFactory) {
3042
self.eventLoop = eventLoop
43+
self.shutdownPromise = eventLoop.makePromise(of: Int.self)
3144
self.logger = logger
3245
self.configuration = configuration
3346
self.factory = factory
@@ -39,46 +52,46 @@ extension Lambda {
3952
}
4053
}
4154

42-
private var state: State {
43-
get {
44-
self.stateLock.withLock {
45-
self._state
46-
}
47-
}
48-
set {
49-
self.stateLock.withLockVoid {
50-
precondition(newValue.order > _state.order, "invalid state \(newValue) after \(self._state)")
51-
self._state = newValue
52-
}
53-
}
55+
/// The `Lifecycle` shutdown future.
56+
///
57+
/// - Returns: An `EventLoopFuture` that is fulfilled after the Lambda lifecycle has fully shutdown.
58+
public var shutdownFuture: EventLoopFuture<Int> {
59+
self.shutdownPromise.futureResult
5460
}
5561

56-
func start() -> EventLoopFuture<Int> {
62+
/// Start the `Lifecycle`.
63+
///
64+
/// - Returns: An `EventLoopFuture` that is fulfilled after the Lambda hander has been created and initiliazed, and a first run has been schduled.
65+
public func start() -> EventLoopFuture<Void> {
5766
logger.info("lambda lifecycle starting with \(self.configuration)")
5867
self.state = .initializing
68+
// triggered when the Lambda has finished its last run
69+
let finishedPromise = self.eventLoop.makePromise(of: Int.self)
70+
finishedPromise.futureResult.always { _ in
71+
self.markShutdown()
72+
}.cascade(to: self.shutdownPromise)
5973
var logger = self.logger
6074
logger[metadataKey: "lifecycleId"] = .string(self.configuration.lifecycle.id)
6175
let runner = Runner(eventLoop: self.eventLoop, configuration: self.configuration)
62-
return runner.initialize(logger: logger, factory: self.factory).flatMap { handler in
76+
return runner.initialize(logger: logger, factory: self.factory).map { handler in
6377
self.state = .active(runner, handler)
64-
return self.run()
78+
self.run(promise: finishedPromise)
6579
}
6680
}
6781

68-
func stop() {
69-
self.logger.debug("lambda lifecycle stopping")
70-
self.state = .stopping
82+
// MARK: - Private
83+
84+
/// Begin the `Lifecycle` shutdown.
85+
public func shutdown() {
86+
self.state = .shuttingdown
7187
}
7288

73-
func shutdown() {
74-
self.logger.debug("lambda lifecycle shutdown")
89+
private func markShutdown() {
7590
self.state = .shutdown
7691
}
7792

7893
@inline(__always)
79-
private func run() -> EventLoopFuture<Int> {
80-
let promise = self.eventLoop.makePromise(of: Int.self)
81-
94+
private func run(promise: EventLoopPromise<Int>) {
8295
func _run(_ count: Int) {
8396
switch self.state {
8497
case .active(let runner, let handler):
@@ -96,23 +109,36 @@ extension Lambda {
96109
promise.fail(error)
97110
}
98111
}
99-
case .stopping, .shutdown:
112+
case .shuttingdown:
100113
promise.succeed(count)
101114
default:
102115
preconditionFailure("invalid run state: \(self.state)")
103116
}
104117
}
105118

106119
_run(0)
120+
}
107121

108-
return promise.futureResult
122+
private var state: State {
123+
get {
124+
self.stateLock.withLock {
125+
self._state
126+
}
127+
}
128+
set {
129+
self.stateLock.withLockVoid {
130+
precondition(newValue.order > self._state.order, "invalid state \(newValue) after \(self._state)")
131+
self._state = newValue
132+
}
133+
self.logger.debug("lambda lifecycle state: \(newValue)")
134+
}
109135
}
110136

111137
private enum State {
112138
case idle
113139
case initializing
114140
case active(Runner, ByteBufferLambdaHandler)
115-
case stopping
141+
case shuttingdown
116142
case shutdown
117143

118144
internal var order: Int {
@@ -123,7 +149,7 @@ extension Lambda {
123149
return 1
124150
case .active:
125151
return 2
126-
case .stopping:
152+
case .shuttingdown:
127153
return 3
128154
case .shutdown:
129155
return 4
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the SwiftAWSLambdaRuntime open source project
4+
//
5+
// Copyright (c) 2020 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+
// @testable for access of internal functions - this would only work for testing by design
16+
@testable import AWSLambdaRuntime
17+
import Dispatch
18+
import Logging
19+
import NIO
20+
21+
extension Lambda {
22+
public struct TestConfig {
23+
public var requestId: String
24+
public var traceId: String
25+
public var invokedFunctionArn: String
26+
public var timeout: DispatchTimeInterval
27+
28+
public init(requestId: String = "\(DispatchTime.now().uptimeNanoseconds)",
29+
traceId: String = "Root=\(DispatchTime.now().uptimeNanoseconds);Parent=\(DispatchTime.now().uptimeNanoseconds);Sampled=1",
30+
invokedFunctionArn: String = "arn:aws:lambda:us-west-1:\(DispatchTime.now().uptimeNanoseconds):function:custom-runtime",
31+
timeout: DispatchTimeInterval = .seconds(5)) {
32+
self.requestId = requestId
33+
self.traceId = traceId
34+
self.invokedFunctionArn = invokedFunctionArn
35+
self.timeout = timeout
36+
}
37+
}
38+
39+
public static func test(_ closure: @escaping StringLambdaClosure,
40+
with payload: String,
41+
using config: TestConfig = .init()) throws -> String {
42+
try Self.test(StringLambdaClosureWrapper(closure), with: payload, using: config)
43+
}
44+
45+
public static func test(_ closure: @escaping StringVoidLambdaClosure,
46+
with payload: String,
47+
using config: TestConfig = .init()) throws {
48+
_ = try Self.test(StringVoidLambdaClosureWrapper(closure), with: payload, using: config)
49+
}
50+
51+
public static func test<In: Decodable, Out: Encodable>(
52+
_ closure: @escaping CodableLambdaClosure<In, Out>,
53+
with payload: In,
54+
using config: TestConfig = .init()
55+
) throws -> Out {
56+
try Self.test(CodableLambdaClosureWrapper(closure), with: payload, using: config)
57+
}
58+
59+
public static func test<In: Decodable>(
60+
_ closure: @escaping CodableVoidLambdaClosure<In>,
61+
with payload: In,
62+
using config: TestConfig = .init()
63+
) throws {
64+
_ = try Self.test(CodableVoidLambdaClosureWrapper(closure), with: payload, using: config)
65+
}
66+
67+
public static func test<In, Out, Handler: EventLoopLambdaHandler>(
68+
_ handler: Handler,
69+
with payload: In,
70+
using config: TestConfig = .init()
71+
) throws -> Out where Handler.In == In, Handler.Out == Out {
72+
let logger = Logger(label: "test")
73+
let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)
74+
defer {
75+
try! eventLoopGroup.syncShutdownGracefully()
76+
}
77+
let eventLoop = eventLoopGroup.next()
78+
let context = Context(requestId: config.requestId,
79+
traceId: config.traceId,
80+
invokedFunctionArn: config.invokedFunctionArn,
81+
deadline: .now() + config.timeout,
82+
logger: logger,
83+
eventLoop: eventLoop)
84+
85+
return try eventLoop.flatSubmit {
86+
handler.handle(context: context, payload: payload)
87+
}.wait()
88+
}
89+
}

0 commit comments

Comments
 (0)