Skip to content

Commit 8184348

Browse files
tomerdGitHub Enterprise
authored and
GitHub Enterprise
committed
more refactoring (#14)
motivation: improve code quality, handle keep-alive changes: * refactor http client to keep warm connection to runtime engine when possible and handle request timeouts * refactor configuration into the main lambda class to surface it more clearly * refactor lifecycle code handle shutdown properly and easier to reason about * add tests
1 parent 3993bfb commit 8184348

10 files changed

+463
-313
lines changed

Sources/SwiftAwsLambda/HttpClient.swift

Lines changed: 171 additions & 169 deletions
Large diffs are not rendered by default.

Sources/SwiftAwsLambda/Lambda+Codable.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ extension Lambda {
3232
}
3333

3434
// for testing
35-
internal static func run<In: Decodable, Out: Encodable>(maxTimes: Int = 0, _ closure: @escaping LambdaCodableClosure<In, Out>) -> LambdaLifecycleResult {
35+
internal static func run<In: Decodable, Out: Encodable>(maxTimes: Int = 0, closure: @escaping LambdaCodableClosure<In, Out>) -> LambdaLifecycleResult {
3636
return self.run(handler: LambdaClosureWrapper(closure), maxTimes: maxTimes)
3737
}
3838

Sources/SwiftAwsLambda/Lambda.swift

Lines changed: 128 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -49,46 +49,48 @@ public enum Lambda {
4949
@discardableResult
5050
internal static func run(handler: LambdaHandler, maxTimes: Int = 0, stopSignal: Signal = .TERM) -> LambdaLifecycleResult {
5151
do {
52-
return try self.runAsync(handler: handler, maxTimes: maxTimes, stopSignal: stopSignal).map { .success($0) }.wait()
52+
let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount)
53+
defer { try! eventLoopGroup.syncShutdownGracefully() }
54+
let result = try self.runAsync(eventLoopGroup: eventLoopGroup, handler: handler, maxTimes: maxTimes, stopSignal: stopSignal).wait()
55+
return .success(result)
5356
} catch {
5457
return .failure(error)
5558
}
5659
}
5760

58-
internal static func runAsync(handler: LambdaHandler, maxTimes: Int = 0, stopSignal: Signal = .TERM) -> EventLoopFuture<Int> {
61+
internal static func runAsync(eventLoopGroup: EventLoopGroup, handler: LambdaHandler, maxTimes: Int = 0, stopSignal: Signal = .TERM) -> EventLoopFuture<Int> {
5962
Backtrace.install()
6063
let logger = Logger(label: "Lambda")
61-
let lifecycle = Lifecycle(logger: logger, handler: handler, maxTimes: maxTimes)
64+
let config = Config(lifecycle: .init(maxTimes: maxTimes))
65+
let lifecycle = Lifecycle(eventLoop: eventLoopGroup.next(), logger: logger, config: config, handler: handler)
6266
let signalSource = trap(signal: stopSignal) { signal in
6367
logger.info("intercepted signal: \(signal)")
6468
lifecycle.stop()
6569
}
6670
return lifecycle.start().always { _ in
67-
lifecycle.stop()
71+
lifecycle.shutdown()
6872
signalSource.cancel()
6973
}
7074
}
7175

7276
private class Lifecycle {
77+
private let eventLoop: EventLoop
7378
private let logger: Logger
74-
private let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount)
79+
private let config: Lambda.Config
7580
private let handler: LambdaHandler
76-
private let max: Int
7781

7882
private var _state = LifecycleState.idle
7983
private let stateLock = Lock()
8084

81-
init(logger: Logger, handler: LambdaHandler, maxTimes: Int) {
82-
assert(maxTimes >= 0, "maxTimes must be larger than 0")
85+
init(eventLoop: EventLoop, logger: Logger, config: Lambda.Config, handler: LambdaHandler) {
86+
self.eventLoop = eventLoop
8387
self.logger = logger
88+
self.config = config
8489
self.handler = handler
85-
self.max = maxTimes
86-
self.logger.info("lambda lifecycle init")
8790
}
8891

8992
deinit {
90-
self.logger.info("lambda lifecycle deinit")
91-
assert(self.state == .shutdown, "invalid state, expected shutdown")
93+
precondition(self.state == .shutdown, "invalid state \(self.state)")
9294
}
9395

9496
private var state: LifecycleState {
@@ -99,66 +101,139 @@ public enum Lambda {
99101
}
100102
set {
101103
self.stateLock.withLockVoid {
102-
assert(newValue.rawValue > _state.rawValue, "invalid state \(newValue) after \(_state)")
104+
precondition(newValue.rawValue > _state.rawValue, "invalid state \(newValue) after \(_state)")
103105
self._state = newValue
104106
}
105107
}
106108
}
107109

108110
func start() -> EventLoopFuture<Int> {
111+
logger.info("lambda lifecycle starting with \(self.config)")
109112
self.state = .initializing
110-
let lifecycleId = NSUUID().uuidString
111-
let eventLoop = self.eventLoopGroup.next()
112113
var logger = self.logger
113-
logger[metadataKey: "lifecycleId"] = .string(lifecycleId)
114-
logger.info("lambda lifecycle starting")
115-
116-
let runner = LambdaRunner(eventLoop: eventLoop, lambdaHandler: handler, lifecycleId: lifecycleId)
114+
logger[metadataKey: "lifecycleId"] = .string(self.config.lifecycle.id)
115+
let runner = LambdaRunner(eventLoop: self.eventLoop, config: self.config, lambdaHandler: self.handler)
117116
return runner.initialize(logger: logger).flatMap { _ in
118117
self.state = .active
119-
return self.run(logger: logger, eventLoop: eventLoop, runner: runner, count: 0)
118+
return self.run(logger: logger, runner: runner, count: 0)
120119
}
121120
}
122121

123122
func stop() {
123+
self.logger.info("lambda lifecycle stopping")
124+
self.state = .stopping
125+
}
126+
127+
func shutdown() {
128+
self.logger.info("lambda lifecycle shutdown")
129+
self.state = .shutdown
130+
}
131+
132+
private func run(logger: Logger, runner: LambdaRunner, count: Int) -> EventLoopFuture<Int> {
124133
switch self.state {
125-
case .stopping:
126-
return self.logger.info("lambda lifecycle aready stopping")
127-
case .shutdown:
128-
return self.logger.info("lambda lifecycle aready shutdown")
134+
case .active:
135+
if self.config.lifecycle.maxTimes > 0, count >= self.config.lifecycle.maxTimes {
136+
return self.eventLoop.makeSucceededFuture(count)
137+
}
138+
var logger = logger
139+
logger[metadataKey: "lifecycleIteration"] = "\(count)"
140+
return runner.run(logger: logger).flatMap { _ in
141+
// recursive! per aws lambda runtime spec the polling requests are to be done one at a time
142+
self.run(logger: logger, runner: runner, count: count + 1)
143+
}
144+
case .stopping, .shutdown:
145+
return self.eventLoop.makeSucceededFuture(count)
129146
default:
130-
self.logger.info("lambda lifecycle stopping")
131-
self.state = .stopping
132-
try! self.eventLoopGroup.syncShutdownGracefully()
133-
self.state = .shutdown
147+
preconditionFailure("invalid run state: \(self.state)")
134148
}
135149
}
150+
}
136151

137-
private func run(logger: Logger, eventLoop: EventLoop, runner: LambdaRunner, count: Int) -> EventLoopFuture<Int> {
138-
var logger = logger
139-
logger[metadataKey: "lifecycleIteration"] = "\(count)"
140-
return runner.run(logger: logger).flatMap { _ in
141-
switch self.state {
142-
case .idle, .initializing:
143-
preconditionFailure("invalid run state: \(self.state)")
144-
case .active:
145-
if self.max > 0, count >= self.max {
146-
return eventLoop.makeSucceededFuture(count)
147-
}
148-
// recursive! per aws lambda runtime spec the polling requests are to be done one at a time
149-
return self.run(logger: logger, eventLoop: eventLoop, runner: runner, count: count + 1)
150-
case .stopping, .shutdown:
151-
return eventLoop.makeSucceededFuture(count)
152-
}
153-
}.flatMapErrorThrowing { error in
154-
// if we run into errors while shutting down, we ignore them
155-
switch self.state {
156-
case .stopping, .shutdown:
157-
return count
158-
default:
159-
throw error
160-
}
152+
internal struct Config: CustomStringConvertible {
153+
let lifecycle: Lifecycle
154+
let runtimeEngine: RuntimeEngine
155+
156+
var description: String {
157+
return "\(Config.self):\n \(self.lifecycle)\n \(self.runtimeEngine)"
158+
}
159+
160+
init(lifecycle: Lifecycle = .init(), runtimeEngine: RuntimeEngine = .init()) {
161+
self.lifecycle = lifecycle
162+
self.runtimeEngine = runtimeEngine
163+
}
164+
165+
struct Lifecycle: CustomStringConvertible {
166+
let id: String
167+
let maxTimes: Int
168+
169+
init(id: String? = nil, maxTimes: Int? = nil) {
170+
self.id = id ?? NSUUID().uuidString
171+
self.maxTimes = maxTimes ?? 0
172+
precondition(self.maxTimes >= 0, "maxTimes must be equal or larger than 0")
173+
}
174+
175+
var description: String {
176+
return "\(Lifecycle.self)(id: \(self.id), maxTimes: \(self.maxTimes))"
177+
}
178+
}
179+
180+
struct RuntimeEngine: CustomStringConvertible {
181+
let baseURL: HTTPURL
182+
let keepAlive: Bool
183+
let requestTimeout: TimeAmount?
184+
185+
init(baseURL: String? = nil, keepAlive: Bool? = nil, requestTimeout: TimeAmount? = nil) {
186+
self.baseURL = HTTPURL(baseURL ?? Environment.string(Consts.hostPortEnvVariableName).flatMap { "http://\($0)" } ?? "http://\(Defaults.host):\(Defaults.port)")
187+
self.keepAlive = keepAlive ?? true
188+
self.requestTimeout = requestTimeout ?? Environment.int(Consts.requestTimeoutEnvVariableName).flatMap { .milliseconds(Int64($0)) }
189+
}
190+
191+
var description: String {
192+
return "\(RuntimeEngine.self)(baseURL: \(self.baseURL), keepAlive: \(self.keepAlive), requestTimeout: \(String(describing: self.requestTimeout)))"
193+
}
194+
}
195+
}
196+
197+
internal struct HTTPURL: Equatable, CustomStringConvertible {
198+
private let url: URL
199+
let host: String
200+
let port: Int
201+
202+
init(_ url: String) {
203+
guard let url = Foundation.URL(string: url) else {
204+
preconditionFailure("invalid url")
205+
}
206+
guard let host = url.host else {
207+
preconditionFailure("invalid url host")
161208
}
209+
guard let port = url.port else {
210+
preconditionFailure("invalid url port")
211+
}
212+
self.url = url
213+
self.host = host
214+
self.port = port
215+
}
216+
217+
init(url: URL, host: String, port: Int) {
218+
self.url = url
219+
self.host = host
220+
self.port = port
221+
}
222+
223+
func appendingPathComponent(_ pathComponent: String) -> HTTPURL {
224+
return .init(url: self.url.appendingPathComponent(pathComponent), host: self.host, port: self.port)
225+
}
226+
227+
var path: String {
228+
return self.url.path
229+
}
230+
231+
var query: String? {
232+
return self.url.query
233+
}
234+
235+
var description: String {
236+
return self.url.description
162237
}
163238
}
164239

@@ -193,6 +268,7 @@ public protocol LambdaHandler {
193268
}
194269

195270
extension LambdaHandler {
271+
@inlinable
196272
public func initialize(callback: @escaping LambdaInitCallBack) {
197273
callback(.success(()))
198274
}

Sources/SwiftAwsLambda/LambdaRunner.swift

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,11 @@ internal final class LambdaRunner {
2323
private let eventLoop: EventLoop
2424
private let lifecycleId: String
2525

26-
init(eventLoop: EventLoop, lambdaHandler: LambdaHandler, lifecycleId: String) {
26+
init(eventLoop: EventLoop, config: Lambda.Config, lambdaHandler: LambdaHandler) {
2727
self.eventLoop = eventLoop
28-
self.runtimeClient = LambdaRuntimeClient(eventLoop: self.eventLoop)
28+
self.runtimeClient = LambdaRuntimeClient(eventLoop: self.eventLoop, config: config.runtimeEngine)
2929
self.lambdaHandler = lambdaHandler
30-
self.lifecycleId = lifecycleId
30+
self.lifecycleId = config.lifecycle.id
3131
}
3232

3333
/// Run the user provided initializer. This *must* only be called once.
@@ -56,9 +56,9 @@ internal final class LambdaRunner {
5656
return self.lambdaHandler.handle(eventLoop: self.eventLoop, lifecycleId: self.lifecycleId, context: context, payload: payload).map { (context, $0) }
5757
}.flatMap { context, result in
5858
// 3. report results to runtime engine
59-
self.runtimeClient.reportResults(logger: logger, context: context, result: result)
60-
}.peekError { error in
61-
logger.error("failed reporting results to lambda runtime engine: \(error)")
59+
self.runtimeClient.reportResults(logger: logger, context: context, result: result).peekError { error in
60+
logger.error("failed reporting results to lambda runtime engine: \(error)")
61+
}
6262
}.always { result in
6363
// we are done!
6464
logger.info("lambda invocation sequence completed \(result.successful ? "successfully" : "with failure")")

0 commit comments

Comments
 (0)