Skip to content

Commit ab3ce64

Browse files
fabianfetttomerd
andauthored
Use NIO in Single Threaded Mode (#68)
motivation: better performance changes: employ SwiftNIO's new Single Threaded Mode Co-authored-by: tomer doron <tomerd@apple.com>
1 parent d3b7517 commit ab3ce64

File tree

4 files changed

+45
-35
lines changed

4 files changed

+45
-35
lines changed

Package.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,9 @@ let package = Package(
1818
.library(name: "AWSLambdaTesting", targets: ["AWSLambdaTesting"]),
1919
],
2020
dependencies: [
21-
.package(url: "https://github.com/apple/swift-nio.git", from: "2.8.0"),
22-
.package(url: "https://github.com/apple/swift-log.git", from: "1.0.0"),
23-
.package(url: "https://github.com/swift-server/swift-backtrace.git", from: "1.1.0"),
21+
.package(url: "https://github.com/apple/swift-nio.git", .upToNextMajor(from: "2.17.0")),
22+
.package(url: "https://github.com/apple/swift-log.git", .upToNextMajor(from: "1.0.0")),
23+
.package(url: "https://github.com/swift-server/swift-backtrace.git", .upToNextMajor(from: "1.1.0")),
2424
],
2525
targets: [
2626
.target(name: "AWSLambdaRuntime", dependencies: [

Sources/AWSLambdaRuntimeCore/Lambda.swift

Lines changed: 31 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -80,41 +80,49 @@ public enum Lambda {
8080
@discardableResult
8181
internal static func run(configuration: Configuration = .init(), factory: @escaping (EventLoop) throws -> Handler) -> Result<Int, Error> {
8282
self.run(configuration: configuration, factory: { eventloop -> EventLoopFuture<Handler> in
83-
do {
84-
let handler = try factory(eventloop)
85-
return eventloop.makeSucceededFuture(handler)
86-
} catch {
87-
return eventloop.makeFailedFuture(error)
83+
let promise = eventloop.makePromise(of: Handler.self)
84+
// if we have a callback based handler factory, we offload the creation of the handler
85+
// onto the default offload queue, to ensure that the eventloop is never blocked.
86+
Lambda.defaultOffloadQueue.async {
87+
do {
88+
promise.succeed(try factory(eventloop))
89+
} catch {
90+
promise.fail(error)
91+
}
8892
}
93+
return promise.futureResult
8994
})
9095
}
9196

9297
// for testing and internal use
9398
@discardableResult
9499
internal static func run(configuration: Configuration = .init(), factory: @escaping HandlerFactory) -> Result<Int, Error> {
95-
do {
96-
let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) // only need one thread, will improve performance
97-
defer { try! eventLoopGroup.syncShutdownGracefully() }
98-
let result = try self.runAsync(eventLoopGroup: eventLoopGroup, configuration: configuration, factory: factory).wait()
99-
return .success(result)
100-
} catch {
101-
return .failure(error)
102-
}
103-
}
104-
105-
internal static func runAsync(eventLoopGroup: EventLoopGroup, configuration: Configuration, factory: @escaping HandlerFactory) -> EventLoopFuture<Int> {
106100
Backtrace.install()
107101
var logger = Logger(label: "Lambda")
108102
logger.logLevel = configuration.general.logLevel
109-
let lifecycle = Lifecycle(eventLoop: eventLoopGroup.next(), logger: logger, configuration: configuration, factory: factory)
110-
let signalSource = trap(signal: configuration.lifecycle.stopSignal) { signal in
111-
logger.info("intercepted signal: \(signal)")
112-
lifecycle.shutdown()
113-
}
114-
return lifecycle.start().flatMap {
115-
return lifecycle.shutdownFuture.always { _ in
103+
104+
var result: Result<Int, Error>!
105+
MultiThreadedEventLoopGroup.withCurrentThreadAsEventLoop { eventLoop in
106+
let lifecycle = Lifecycle(eventLoop: eventLoop, logger: logger, configuration: configuration, factory: factory)
107+
let signalSource = trap(signal: configuration.lifecycle.stopSignal) { signal in
108+
logger.info("intercepted signal: \(signal)")
109+
lifecycle.shutdown()
110+
}
111+
112+
lifecycle.start().flatMap {
113+
lifecycle.shutdownFuture
114+
}.whenComplete { lifecycleResult in
116115
signalSource.cancel()
116+
eventLoop.shutdownGracefully { error in
117+
if let error = error {
118+
preconditionFailure("Failed to shutdown eventloop: \(error)")
119+
}
120+
}
121+
result = lifecycleResult
117122
}
118123
}
124+
125+
logger.info("shutdown completed")
126+
return result
119127
}
120128
}

Sources/AWSLambdaRuntimeCore/LambdaHandler.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ public protocol LambdaHandler: EventLoopLambdaHandler {
3939
func handle(context: Lambda.Context, payload: In, callback: @escaping (Result<Out, Error>) -> Void)
4040
}
4141

42-
private extension Lambda {
42+
internal extension Lambda {
4343
static let defaultOffloadQueue = DispatchQueue(label: "LambdaHandler.offload")
4444
}
4545

Tests/AWSLambdaRuntimeCoreTests/LambdaTest.swift

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -128,27 +128,29 @@ class LambdaTest: XCTestCase {
128128
assertLambdaLifecycleResult(result, shouldFailWithError: TestError("kaboom"))
129129
}
130130

131-
func testStartStop() {
131+
func testStartStopInDebugMode() {
132132
let server = MockLambdaServer(behavior: Behavior())
133133
XCTAssertNoThrow(try server.start().wait())
134134
defer { XCTAssertNoThrow(try server.stop().wait()) }
135135

136136
let signal = Signal.ALRM
137137
let maxTimes = 1000
138138
let configuration = Lambda.Configuration(lifecycle: .init(maxTimes: maxTimes, stopSignal: signal))
139-
let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount)
140-
defer { XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) }
141139

142-
let future = Lambda.runAsync(eventLoopGroup: eventLoopGroup, configuration: configuration, factory: { $0.makeSucceededFuture(EchoHandler()) })
143140
DispatchQueue(label: "test").async {
141+
// we need to schedule the signal before we start the long running `Lambda.run`, since
142+
// `Lambda.run` will block the main thread.
144143
usleep(100_000)
145144
kill(getpid(), signal.rawValue)
146145
}
147-
future.whenSuccess { result in
148-
XCTAssertGreaterThan(result, 0, "should have stopped before any request made")
149-
XCTAssertLessThan(result, maxTimes, "should have stopped before \(maxTimes)")
146+
let result = Lambda.run(configuration: configuration, factory: { $0.makeSucceededFuture(EchoHandler()) })
147+
148+
guard case .success(let invocationCount) = result else {
149+
return XCTFail("expected to have not failed")
150150
}
151-
XCTAssertNoThrow(try future.wait())
151+
152+
XCTAssertGreaterThan(invocationCount, 0, "should have stopped before any request made")
153+
XCTAssertLessThan(invocationCount, maxTimes, "should have stopped before \(maxTimes)")
152154
}
153155

154156
func testTimeout() {

0 commit comments

Comments
 (0)