diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a04f3d..6c6e621 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,16 @@ - `1.1.x` Releases - [1.1.0](#110---quality-of-service) - `1.0.x` Releases - [1.0.0](#100---first-queue) +## Develop + +### Fixed + +- Fixed a bug that would run the `executionBlock` indefinitely when using async/await APIs - [#32](https://github.com/FabrizioBrancati/Queuer/pull/32) + +### Improved + +- Improved documentation + ## [3.0.0](https://github.com/FabrizioBrancati/Queuer/releases/tag/3.0.0) - The Phoenix ### 24 Apr 2024 diff --git a/README.md b/README.md index 44c6ccf..fadeb70 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,7 @@ Add the dependency to any targets you've declared in your manifest: - [Automatically Retry an Operation](https://github.com/FabrizioBrancati/Queuer#automatically-retry-an-operation) - [Manually Retry an Operation](https://github.com/FabrizioBrancati/Queuer#manually-retry-an-operation) - [Manually Finish an Operation](https://github.com/FabrizioBrancati/Queuer#manually-finish-an-operation) +- [Async Task in an Operation](https://github.com/FabrizioBrancati/Queuer#async-tasks-in-an-operation) - [Scheduler](https://github.com/FabrizioBrancati/Queuer#scheduler) - [Semaphore](https://github.com/FabrizioBrancati/Queuer#semaphore) @@ -304,6 +305,27 @@ concurrentOperation.finish(success: true) > [!CAUTION] > If you don't call the `finish(success:)` function, your queue will be blocked and it will never ends. +### Async Task in an Operation + +If you want to use async/await tasks, you need to set `manualFinish` to `true` in order to be fully in control of the `Operation` lifecycle. + +> [!NOTE] +> Read more about manually finish an `Operation` [here](https://github.com/FabrizioBrancati/Queuer#manually-finish-an-operation). + +```swift +let concurrentOperation = ConcurrentOperation { operation in + Task { + /// Your asynchonous task here + operation.finish(success: true) // or false + } + /// Your asynchonous task here +} +concurrentOperation.manualFinish = true +``` + +> [!CAUTION] +> If you don't set `manualFinish` to `true`, your `Operation` will finish before the async task is completed. + ### Scheduler A `Scheduler` is a struct that uses the GDC's `DispatchSourceTimer` to create a timer that can execute functions with a specified interval and quality of service. diff --git a/Sources/Queuer/ConcurrentOperation.swift b/Sources/Queuer/ConcurrentOperation.swift index a1b3b9b..00977da 100644 --- a/Sources/Queuer/ConcurrentOperation.swift +++ b/Sources/Queuer/ConcurrentOperation.swift @@ -87,6 +87,10 @@ open class ConcurrentOperation: Operation { /// either by passing `false` or `true` to the function. open var manualFinish = false + /// Keep track of the last executed attempt. + /// This avoids running the `executionBlock` more than once per retry. + private var lastExecutedAttempt = 0 + /// Creates the `Operation` with an execution block. /// /// - Parameters: @@ -122,7 +126,10 @@ open class ConcurrentOperation: Operation { open func execute() { if let executionBlock { while shouldRetry, !manualRetry { - executionBlock(self) + if lastExecutedAttempt != currentAttempt { + executionBlock(self) + lastExecutedAttempt = currentAttempt + } if !manualFinish { finish(success: success) diff --git a/Tests/QueuerTests/ConcurrentOperationTests.swift b/Tests/QueuerTests/ConcurrentOperationTests.swift index 0e459d6..4019208 100644 --- a/Tests/QueuerTests/ConcurrentOperationTests.swift +++ b/Tests/QueuerTests/ConcurrentOperationTests.swift @@ -27,6 +27,14 @@ import Queuer import XCTest +actor Order { + var order: [Int] = [] + + func append(_ element: Int) { + order.append(element) + } +} + final class ConcurrentOperationTests: XCTestCase { func testInitWithExecutionBlock() { let queue = Queuer(name: "ConcurrentOperationTestInitWithExecutionBlock") @@ -108,6 +116,40 @@ final class ConcurrentOperationTests: XCTestCase { } } + #if !os(Linux) + func testAsyncChainedRetry() async { + let queue = Queuer(name: "ConcurrentOperationTestChainedRetry") + let testExpectation = expectation(description: "Chained Retry") + let order = Order() + + let concurrentOperation1 = ConcurrentOperation { operation in + Task { + try? await Task.sleep(for: .seconds(1)) + await order.append(0) + operation.finish(success: false) + } + } + concurrentOperation1.manualFinish = true + let concurrentOperation2 = ConcurrentOperation { operation in + Task { + await order.append(1) + operation.finish(success: false) + } + } + concurrentOperation2.manualFinish = true + queue.addChainedOperations([concurrentOperation1, concurrentOperation2]) { + Task { + await order.append(2) + testExpectation.fulfill() + } + } + + await fulfillment(of: [testExpectation], timeout: 10) + let finalOrder = await order.order + XCTAssertEqual(finalOrder, [0, 0, 0, 1, 1, 1, 2]) + } + #endif + func testCanceledChainedRetry() { let queue = Queuer(name: "ConcurrentOperationTestCanceledChainedRetry") let testExpectation = expectation(description: "Canceled Chained Retry")