From d195b1d3084f9e217eef21faa252406d66c45007 Mon Sep 17 00:00:00 2001 From: Fabrizio Brancati Date: Thu, 2 May 2024 14:57:41 +0200 Subject: [PATCH 1/5] Fix while loop for async tasks in `executionBlock` --- Sources/Queuer/ConcurrentOperation.swift | 9 ++++- .../ConcurrentOperationTests.swift | 40 +++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) 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..d809073 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,38 @@ final class ConcurrentOperationTests: XCTestCase { } } + 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: 5) + let finalOrder = await order.order + XCTAssertEqual(finalOrder, [0, 0, 0, 1, 1, 1, 2]) + } + func testCanceledChainedRetry() { let queue = Queuer(name: "ConcurrentOperationTestCanceledChainedRetry") let testExpectation = expectation(description: "Canceled Chained Retry") From f1684c48ff142325cfa19ac608d4cb21e7607e3f Mon Sep 17 00:00:00 2001 From: Fabrizio Brancati Date: Thu, 2 May 2024 15:19:10 +0200 Subject: [PATCH 2/5] Update changelog entries --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) 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 From 8d5e9a9da2cffa71d0b7ac57c8eb0e2171b3d1a2 Mon Sep 17 00:00:00 2001 From: Fabrizio Brancati Date: Thu, 2 May 2024 17:22:49 +0200 Subject: [PATCH 3/5] Increase timeout --- Tests/QueuerTests/ConcurrentOperationTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/QueuerTests/ConcurrentOperationTests.swift b/Tests/QueuerTests/ConcurrentOperationTests.swift index d809073..320635b 100644 --- a/Tests/QueuerTests/ConcurrentOperationTests.swift +++ b/Tests/QueuerTests/ConcurrentOperationTests.swift @@ -143,7 +143,7 @@ final class ConcurrentOperationTests: XCTestCase { } } - await fulfillment(of: [testExpectation], timeout: 5) + await fulfillment(of: [testExpectation], timeout: 10) let finalOrder = await order.order XCTAssertEqual(finalOrder, [0, 0, 0, 1, 1, 1, 2]) } From b348129e8e8e66cc41d33bfc7a2b88f5adeb386a Mon Sep 17 00:00:00 2001 From: Fabrizio Brancati Date: Fri, 3 May 2024 16:03:15 +0200 Subject: [PATCH 4/5] Add async task in an operation documentation --- README.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) 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. From 8187a046b841264ae72c2272e642f37c69a37063 Mon Sep 17 00:00:00 2001 From: Fabrizio Brancati Date: Fri, 3 May 2024 18:45:29 +0200 Subject: [PATCH 5/5] Temporary removing async test --- Tests/QueuerTests/ConcurrentOperationTests.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Tests/QueuerTests/ConcurrentOperationTests.swift b/Tests/QueuerTests/ConcurrentOperationTests.swift index 320635b..4019208 100644 --- a/Tests/QueuerTests/ConcurrentOperationTests.swift +++ b/Tests/QueuerTests/ConcurrentOperationTests.swift @@ -116,6 +116,7 @@ final class ConcurrentOperationTests: XCTestCase { } } + #if !os(Linux) func testAsyncChainedRetry() async { let queue = Queuer(name: "ConcurrentOperationTestChainedRetry") let testExpectation = expectation(description: "Chained Retry") @@ -147,6 +148,7 @@ final class ConcurrentOperationTests: XCTestCase { let finalOrder = await order.order XCTAssertEqual(finalOrder, [0, 0, 0, 1, 1, 1, 2]) } + #endif func testCanceledChainedRetry() { let queue = Queuer(name: "ConcurrentOperationTestCanceledChainedRetry")