Skip to content

Commit

Permalink
Merge pull request #32 from FabrizioBrancati/bugfix/async-while-loop
Browse files Browse the repository at this point in the history
Fix `while` loop for async tasks in `executionBlock`
  • Loading branch information
FabrizioBrancati authored May 3, 2024
2 parents 591b705 + de34afb commit ed9c55a
Show file tree
Hide file tree
Showing 4 changed files with 82 additions and 1 deletion.
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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.
Expand Down
9 changes: 8 additions & 1 deletion Sources/Queuer/ConcurrentOperation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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)
Expand Down
42 changes: 42 additions & 0 deletions Tests/QueuerTests/ConcurrentOperationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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")
Expand Down

0 comments on commit ed9c55a

Please sign in to comment.