Skip to content

Commit fcd28b4

Browse files
committed
add handlers that support async functions
motivation: async/await is coming in 5.5, take first steps to support it changes: * introduce handler initializers that takes async functions (@escaping () async throws -> Void, @escaping () async throws -> State) * add tests
1 parent ad8631e commit fcd28b4

File tree

3 files changed

+209
-1
lines changed

3 files changed

+209
-1
lines changed

Sources/Lifecycle/Lifecycle.swift

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@
1212
//
1313
//===----------------------------------------------------------------------===//
1414

15+
#if compiler(>=5.5)
16+
import _Concurrency
17+
#endif
18+
1519
#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS)
1620
import Darwin
1721
#else
@@ -95,6 +99,28 @@ public struct LifecycleHandler {
9599
}
96100
}
97101

102+
#if compiler(>=5.5)
103+
@available(macOS 12.0, *)
104+
extension LifecycleHandler {
105+
public init(_ handler: @escaping () async throws -> Void) {
106+
self = LifecycleHandler { callback in
107+
detach {
108+
do {
109+
try await handler()
110+
callback(nil)
111+
} catch {
112+
callback(error)
113+
}
114+
}
115+
}
116+
}
117+
118+
public static func async(_ handler: @escaping () async throws -> Void) -> LifecycleHandler {
119+
return LifecycleHandler(handler)
120+
}
121+
}
122+
#endif
123+
98124
// MARK: - Stateful Lifecycle Handlers
99125

100126
/// LifecycleHandler for starting stateful tasks. The state can then be fed into a LifecycleShutdownHandler
@@ -137,6 +163,28 @@ public struct LifecycleStartHandler<State> {
137163
}
138164
}
139165

166+
#if compiler(>=5.5)
167+
@available(macOS 12.0, *)
168+
extension LifecycleStartHandler {
169+
public init(_ handler: @escaping () async throws -> State) {
170+
self = LifecycleStartHandler { callback in
171+
detach {
172+
do {
173+
let state = try await handler()
174+
callback(.success(state))
175+
} catch {
176+
callback(.failure(error))
177+
}
178+
}
179+
}
180+
}
181+
182+
public static func async(_ handler: @escaping () async throws -> State) -> LifecycleStartHandler {
183+
return LifecycleStartHandler(handler)
184+
}
185+
}
186+
#endif
187+
140188
/// LifecycleHandler for shutting down stateful tasks. The state comes from a LifecycleStartHandler
141189
public struct LifecycleShutdownHandler<State> {
142190
private let underlying: (State, @escaping (Error?) -> Void) -> Void
@@ -177,6 +225,28 @@ public struct LifecycleShutdownHandler<State> {
177225
}
178226
}
179227

228+
#if compiler(>=5.5)
229+
@available(macOS 12.0, *)
230+
extension LifecycleShutdownHandler {
231+
public init(_ handler: @escaping (State) async throws -> Void) {
232+
self = LifecycleShutdownHandler { state, callback in
233+
detach {
234+
do {
235+
try await handler(state)
236+
callback(nil)
237+
} catch {
238+
callback(error)
239+
}
240+
}
241+
}
242+
}
243+
244+
public static func async(_ handler: @escaping (State) async throws -> Void) -> LifecycleShutdownHandler {
245+
return LifecycleShutdownHandler(handler)
246+
}
247+
}
248+
#endif
249+
180250
// MARK: - ServiceLifecycle
181251

182252
/// `ServiceLifecycle` provides a basic mechanism to cleanly startup and shutdown the application, freeing resources in order before exiting.
@@ -671,7 +741,7 @@ internal struct _LifecycleTask: LifecycleTask {
671741
}
672742
}
673743

674-
// internal for testing
744+
// internal (instead of private) for testing
675745
internal class StatefulLifecycleTask<State>: LifecycleTask {
676746
let label: String
677747
let shutdownIfNotStarted: Bool = false

Tests/LifecycleTests/ComponentLifecycleTests+XCTest.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,10 @@ extension ComponentLifecycleTests {
6666
("testStatefulNIO", testStatefulNIO),
6767
("testStatefulNIOStartFailure", testStatefulNIOStartFailure),
6868
("testStatefulNIOShutdownFailure", testStatefulNIOShutdownFailure),
69+
("testAsyncAwait", testAsyncAwait),
70+
("testAsyncAwaitStateful", testAsyncAwaitStateful),
71+
("testAsyncAwaitErrorOnStart", testAsyncAwaitErrorOnStart),
72+
("testAsyncAwaitErrorOnShutdown", testAsyncAwaitErrorOnShutdown),
6973
("testMetrics", testMetrics),
7074
]
7175
}

Tests/LifecycleTests/ComponentLifecycleTests.swift

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1249,6 +1249,140 @@ final class ComponentLifecycleTests: XCTestCase {
12491249
XCTAssertFalse(item.shutdown, "expected item to be shutdown")
12501250
}
12511251

1252+
func testAsyncAwait() throws {
1253+
#if compiler(<5.5)
1254+
throw XCTSkip()
1255+
#else
1256+
guard #available(macOS 12.0, *) else {
1257+
throw XCTSkip()
1258+
}
1259+
1260+
class Item {
1261+
var isShutdown: Bool = false
1262+
1263+
func start() async throws {}
1264+
1265+
func shutdown() async throws {
1266+
self.isShutdown = true // not thread safe but okay for this purpose
1267+
}
1268+
}
1269+
1270+
let lifecycle = ComponentLifecycle(label: "test")
1271+
1272+
let item = Item()
1273+
lifecycle.register(label: "test", start: .async(item.start), shutdown: .async(item.shutdown))
1274+
1275+
lifecycle.start { error in
1276+
XCTAssertNil(error, "not expecting error")
1277+
lifecycle.shutdown()
1278+
}
1279+
lifecycle.wait()
1280+
XCTAssertTrue(item.isShutdown, "expected item to be shutdown")
1281+
#endif
1282+
}
1283+
1284+
func testAsyncAwaitStateful() throws {
1285+
#if compiler(<5.5)
1286+
throw XCTSkip()
1287+
#else
1288+
guard #available(macOS 12.0, *) else {
1289+
throw XCTSkip()
1290+
}
1291+
1292+
class Item {
1293+
var isShutdown: Bool = false
1294+
let id: String = UUID().uuidString
1295+
1296+
func start() async throws -> String {
1297+
return self.id
1298+
}
1299+
1300+
func shutdown(state: String) async throws {
1301+
XCTAssertEqual(self.id, state)
1302+
self.isShutdown = true // not thread safe but okay for this purpose
1303+
}
1304+
}
1305+
1306+
let lifecycle = ComponentLifecycle(label: "test")
1307+
1308+
let item = Item()
1309+
lifecycle.registerStateful(label: "test", start: .async(item.start), shutdown: .async(item.shutdown))
1310+
1311+
lifecycle.start { error in
1312+
XCTAssertNil(error, "not expecting error")
1313+
lifecycle.shutdown()
1314+
}
1315+
lifecycle.wait()
1316+
XCTAssertTrue(item.isShutdown, "expected item to be shutdown")
1317+
#endif
1318+
}
1319+
1320+
func testAsyncAwaitErrorOnStart() throws {
1321+
#if compiler(<5.5)
1322+
throw XCTSkip()
1323+
#else
1324+
guard #available(macOS 12.0, *) else {
1325+
throw XCTSkip()
1326+
}
1327+
1328+
class Item {
1329+
var isShutdown: Bool = false
1330+
1331+
func start() async throws {
1332+
throw TestError()
1333+
}
1334+
1335+
func shutdown() async throws {
1336+
self.isShutdown = true // not thread safe but okay for this purpose
1337+
}
1338+
}
1339+
1340+
let lifecycle = ComponentLifecycle(label: "test")
1341+
1342+
let item = Item()
1343+
lifecycle.register(label: "test", start: .async(item.start), shutdown: .async(item.shutdown))
1344+
1345+
lifecycle.start { error in
1346+
XCTAssert(error is TestError, "expected error to match")
1347+
lifecycle.shutdown()
1348+
}
1349+
lifecycle.wait()
1350+
XCTAssertTrue(item.isShutdown, "expected item to be shutdown")
1351+
#endif
1352+
}
1353+
1354+
func testAsyncAwaitErrorOnShutdown() throws {
1355+
#if compiler(<5.5)
1356+
throw XCTSkip()
1357+
#else
1358+
guard #available(macOS 12.0, *) else {
1359+
throw XCTSkip()
1360+
}
1361+
class Item {
1362+
var isShutdown: Bool = false
1363+
1364+
func start() async throws {}
1365+
1366+
func shutdown() async throws {
1367+
self.isShutdown = true // not thread safe but okay for this purpose
1368+
throw TestError()
1369+
}
1370+
}
1371+
1372+
let lifecycle = ComponentLifecycle(label: "test")
1373+
1374+
let item = Item()
1375+
lifecycle.register(label: "test", start: .async(item.start), shutdown: .async(item.shutdown))
1376+
1377+
lifecycle.start { error in
1378+
XCTAssertNil(error, "not expecting error")
1379+
lifecycle.shutdown()
1380+
}
1381+
lifecycle.wait()
1382+
XCTAssertTrue(item.isShutdown, "expected item to be shutdown")
1383+
#endif
1384+
}
1385+
12521386
func testMetrics() {
12531387
let metrics = TestMetrics()
12541388
MetricsSystem.bootstrap(metrics)

0 commit comments

Comments
 (0)