forked from swift-server/swift-aws-lambda-runtime
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[Draft] Detached tasks (swift-server#334)
* First prototype * Fix build * Removes task cancellation swift-server#334 (comment) * Force user to handle errors swift-server#334 (comment) * Remove EventLoop API swift-server#334 (comment) * Make DetachedTaskContainer internal swift-server#334 (comment) swift-server#334 (comment) * Removes @unchecked Sendable swift-server#334 (comment) * Invoke awaitAll() from async context * Fix ambiguous expression type for swift 5.7 * Fix visibility of detachedBackgroundTask * Add swift-doc * Add example usage to readme * Add tests --------- Co-authored-by: Sébastien Stormacq <sebastien.stormacq@gmail.com>
- Loading branch information
Showing
5 changed files
with
239 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
//===----------------------------------------------------------------------===// | ||
// | ||
// This source file is part of the SwiftAWSLambdaRuntime open source project | ||
// | ||
// Copyright (c) 2022 Apple Inc. and the SwiftAWSLambdaRuntime project authors | ||
// Licensed under Apache License v2.0 | ||
// | ||
// See LICENSE.txt for license information | ||
// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors | ||
// | ||
// SPDX-License-Identifier: Apache-2.0 | ||
// | ||
//===----------------------------------------------------------------------===// | ||
import Foundation | ||
import NIOConcurrencyHelpers | ||
import NIOCore | ||
import Logging | ||
|
||
/// A container that allows tasks to finish after a synchronous invocation | ||
/// has produced its response. | ||
actor DetachedTasksContainer: Sendable { | ||
|
||
struct Context: Sendable { | ||
let eventLoop: EventLoop | ||
let logger: Logger | ||
} | ||
|
||
private var context: Context | ||
private var storage: [RegistrationKey: EventLoopFuture<Void>] = [:] | ||
|
||
init(context: Context) { | ||
self.context = context | ||
} | ||
|
||
/// Adds a detached async task. | ||
/// | ||
/// - Parameters: | ||
/// - name: The name of the task. | ||
/// - task: The async task to execute. | ||
/// - Returns: A `RegistrationKey` for the registered task. | ||
func detached(task: @Sendable @escaping () async -> Void) { | ||
let key = RegistrationKey() | ||
let promise = context.eventLoop.makePromise(of: Void.self) | ||
promise.completeWithTask(task) | ||
let task = promise.futureResult.always { [weak self] result in | ||
guard let self else { return } | ||
Task { | ||
await self.removeTask(forKey: key) | ||
} | ||
} | ||
self.storage[key] = task | ||
} | ||
|
||
func removeTask(forKey key: RegistrationKey) { | ||
self.storage.removeValue(forKey: key) | ||
} | ||
|
||
/// Awaits all registered tasks to complete. | ||
/// | ||
/// - Returns: An `EventLoopFuture<Void>` that completes when all tasks have finished. | ||
func awaitAll() -> EventLoopFuture<Void> { | ||
let tasks = storage.values | ||
if tasks.isEmpty { | ||
return context.eventLoop.makeSucceededVoidFuture() | ||
} else { | ||
let context = context | ||
return EventLoopFuture.andAllComplete(Array(tasks), on: context.eventLoop).flatMap { [weak self] in | ||
guard let self else { | ||
return context.eventLoop.makeSucceededFuture(()) | ||
} | ||
let promise = context.eventLoop.makePromise(of: Void.self) | ||
promise.completeWithTask { | ||
try await self.awaitAll().get() | ||
} | ||
return promise.futureResult | ||
} | ||
} | ||
} | ||
} | ||
|
||
extension DetachedTasksContainer { | ||
/// Lambda detached task registration key. | ||
struct RegistrationKey: Hashable, CustomStringConvertible, Sendable { | ||
var value: String | ||
|
||
init() { | ||
// UUID basically | ||
self.value = UUID().uuidString | ||
} | ||
|
||
var description: String { | ||
self.value | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
//===----------------------------------------------------------------------===// | ||
// | ||
// This source file is part of the SwiftAWSLambdaRuntime open source project | ||
// | ||
// Copyright (c) 2017-2018 Apple Inc. and the SwiftAWSLambdaRuntime project authors | ||
// Licensed under Apache License v2.0 | ||
// | ||
// See LICENSE.txt for license information | ||
// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors | ||
// | ||
// SPDX-License-Identifier: Apache-2.0 | ||
// | ||
//===----------------------------------------------------------------------===// | ||
|
||
@testable import AWSLambdaRuntimeCore | ||
import NIO | ||
import XCTest | ||
import Logging | ||
|
||
class DetachedTasksTest: XCTestCase { | ||
|
||
actor Expectation { | ||
var isFulfilled = false | ||
func fulfill() { | ||
isFulfilled = true | ||
} | ||
} | ||
|
||
func testAwaitTasks() async throws { | ||
let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) | ||
defer { XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) } | ||
|
||
let context = DetachedTasksContainer.Context( | ||
eventLoop: eventLoopGroup.next(), | ||
logger: Logger(label: "test") | ||
) | ||
let expectation = Expectation() | ||
|
||
let container = DetachedTasksContainer(context: context) | ||
await container.detached { | ||
try! await Task.sleep(for: .milliseconds(200)) | ||
await expectation.fulfill() | ||
} | ||
|
||
try await container.awaitAll().get() | ||
let isFulfilled = await expectation.isFulfilled | ||
XCTAssert(isFulfilled) | ||
} | ||
|
||
func testAwaitChildrenTasks() async throws { | ||
let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) | ||
defer { XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) } | ||
|
||
let context = DetachedTasksContainer.Context( | ||
eventLoop: eventLoopGroup.next(), | ||
logger: Logger(label: "test") | ||
) | ||
let expectation1 = Expectation() | ||
let expectation2 = Expectation() | ||
|
||
let container = DetachedTasksContainer(context: context) | ||
await container.detached { | ||
await container.detached { | ||
try! await Task.sleep(for: .milliseconds(300)) | ||
await expectation1.fulfill() | ||
} | ||
try! await Task.sleep(for: .milliseconds(200)) | ||
await container.detached { | ||
try! await Task.sleep(for: .milliseconds(100)) | ||
await expectation2.fulfill() | ||
} | ||
} | ||
|
||
try await container.awaitAll().get() | ||
let isFulfilled1 = await expectation1.isFulfilled | ||
let isFulfilled2 = await expectation2.isFulfilled | ||
XCTAssert(isFulfilled1) | ||
XCTAssert(isFulfilled2) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters