Skip to content

Commit 7f70674

Browse files
authored
add handlers that support async functions (#99)
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 7f70674

File tree

3 files changed

+217
-1
lines changed

3 files changed

+217
-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: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1249,6 +1249,148 @@ final class ComponentLifecycleTests: XCTestCase {
12491249
XCTAssertFalse(item.shutdown, "expected item to be shutdown")
12501250
}
12511251

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

0 commit comments

Comments
 (0)