Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Swift 6 mode #26

Merged
merged 2 commits into from
Jun 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 8 additions & 7 deletions .github/workflows/swift.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,24 @@ on:

jobs:
build:
runs-on: macos-13
runs-on: macos-14

steps:
- uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: latest-stable
- name: Check XCode Version
- uses: actions/checkout@v4
- name: List available Xcode versions
run: ls /Applications | grep Xcode
- name: Set up Xcode version
run: sudo xcode-select -s /Applications/Xcode_16.0.app/Contents/Developer
- name: Show current version of Xcode
run: xcodebuild -version
- uses: actions/checkout@v3
- name: Build
run: xcodebuild -scheme SwiftRetrier build -destination "platform=OS X"
- name: Run tests
run: xcodebuild -scheme SwiftRetrier test -destination "platform=OS X"
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: actions/checkout@v4
- name: SwiftLint
uses: raphaelbussa/swiftlint-action@main
with:
Expand Down
3 changes: 2 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,5 +35,6 @@ let package = Package(
dependencies: ["SwiftRetrier"],
plugins: [] + extraPlugins
)
]
],
swiftLanguageVersions: [.version("6")]
)
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Foundation

open class ConstantDelayRetryPolicy: RetryPolicy {
public struct ConstantDelayRetryPolicy: RetryPolicy {

public let delay: TimeInterval

Expand All @@ -16,7 +16,7 @@ open class ConstantDelayRetryPolicy: RetryPolicy {
.retry(delay: retryDelay(for: attemptFailure))
}

public func freshCopy() -> RetryPolicy {
public func policyAfter(attemptFailure: AttemptFailure, delay: TimeInterval) -> any RetryPolicy {
self
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import Foundation

open class ExponentialBackoffRetryPolicy: RetryPolicy {
public struct ExponentialBackoffRetryPolicy: RetryPolicy {

public enum Jitter {
public enum Jitter: Sendable {
case none
case full
case decorrelated(growthFactor: Double = ExponentialBackoffConstants.defaultDecorrelatedJitterGrowthFactor)
Expand All @@ -11,14 +11,16 @@ open class ExponentialBackoffRetryPolicy: RetryPolicy {
public let timeSlot: TimeInterval
public let maxDelay: TimeInterval
public let jitter: Jitter
private var previousDelay: TimeInterval?
private let previousDelay: TimeInterval?

public init(timeSlot: TimeInterval = ExponentialBackoffConstants.defaultTimeSlot,
maxDelay: TimeInterval = ExponentialBackoffConstants.defaultMaxDelay,
jitter: Jitter = ExponentialBackoffConstants.defaultJitter) {
jitter: Jitter = ExponentialBackoffConstants.defaultJitter,
previousDelay: TimeInterval? = nil) {
self.timeSlot = timeSlot
self.maxDelay = maxDelay
self.jitter = jitter
self.previousDelay = previousDelay
}

public func exponentiationBySquaring<T: BinaryInteger>(_ base: T, _ multiplier: T, _ exponent: T) -> T {
Expand Down Expand Up @@ -57,7 +59,6 @@ open class ExponentialBackoffRetryPolicy: RetryPolicy {
} else {
delay = fullJitterDelay(attemptIndex: attemptIndex)
}
previousDelay = delay
return delay
}

Expand All @@ -72,15 +73,15 @@ open class ExponentialBackoffRetryPolicy: RetryPolicy {
}
}

open func retryDelay(for attemptFailure: AttemptFailure) -> TimeInterval {
public func retryDelay(for attemptFailure: AttemptFailure) -> TimeInterval {
min(maxDelay, uncappedDelay(attemptIndex: attemptFailure.index))
}

public func shouldRetry(on attemptFailure: AttemptFailure) -> RetryDecision {
.retry(delay: retryDelay(for: attemptFailure))
}

public func freshCopy() -> RetryPolicy {
ExponentialBackoffRetryPolicy(timeSlot: timeSlot, maxDelay: maxDelay, jitter: jitter)
public func policyAfter(attemptFailure: AttemptFailure, delay: TimeInterval) -> any RetryPolicy {
ExponentialBackoffRetryPolicy(timeSlot: timeSlot, maxDelay: maxDelay, jitter: jitter, previousDelay: delay)
}
}
2 changes: 1 addition & 1 deletion Sources/SwiftRetrier/Core/Model/Job.swift
Original file line number Diff line number Diff line change
@@ -1 +1 @@
public typealias Job<Value> = () async throws -> Value
public typealias Job<Value> = @Sendable () async throws -> Value
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Foundation

public struct AttemptFailure {
public struct AttemptFailure: Sendable {
public let trialStart: Date
public let index: UInt
public let error: Error
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Foundation

public enum RetryDecision {
public enum RetryDecision: Sendable {
case giveUp
case retry(delay: TimeInterval)
}
4 changes: 2 additions & 2 deletions Sources/SwiftRetrier/Core/Model/Policies/RetryPolicy.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import Foundation

public protocol RetryPolicy {
public protocol RetryPolicy: Sendable {
func shouldRetry(on attemptFailure: AttemptFailure) -> RetryDecision
func retryDelay(for attemptFailure: AttemptFailure) -> TimeInterval
func freshCopy() -> RetryPolicy
func policyAfter(attemptFailure: AttemptFailure, delay: TimeInterval) -> any RetryPolicy
}
4 changes: 2 additions & 2 deletions Sources/SwiftRetrier/Core/Model/Retriers/Retrier.swift
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import Foundation
import Combine

public protocol Retrier: Cancellable, AnyObject {
associatedtype Output
public protocol Retrier: Cancellable, AnyObject, Sendable {
associatedtype Output: Sendable

func publisher() -> AnyPublisher<RetrierEvent<Output>, Never>
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import Foundation
public struct GiveUpOnPolicyWrapper: RetryPolicy {

private let wrapped: RetryPolicy
private let giveUpCriterium: (AttemptFailure) -> Bool
private let giveUpCriterium: @Sendable (AttemptFailure) -> Bool

public init(wrapped: RetryPolicy, giveUpCriterium: @escaping (AttemptFailure) -> Bool) {
public init(wrapped: RetryPolicy, giveUpCriterium: @escaping @Sendable (AttemptFailure) -> Bool) {
self.wrapped = wrapped
self.giveUpCriterium = giveUpCriterium
}
Expand All @@ -21,7 +21,10 @@ public struct GiveUpOnPolicyWrapper: RetryPolicy {
return wrapped.shouldRetry(on: attemptFailure)
}

public func freshCopy() -> RetryPolicy {
GiveUpOnPolicyWrapper(wrapped: wrapped.freshCopy(), giveUpCriterium: giveUpCriterium)
public func policyAfter(attemptFailure: AttemptFailure, delay: TimeInterval) -> any RetryPolicy {
GiveUpOnPolicyWrapper(
wrapped: wrapped.policyAfter(attemptFailure: attemptFailure, delay: delay),
giveUpCriterium: giveUpCriterium
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import Foundation

public extension RetryPolicy {

func giveUp(on giveUpCriterium: @escaping (AttemptFailure) -> Bool) -> RetryPolicy {
func giveUp(on giveUpCriterium: @escaping @Sendable (AttemptFailure) -> Bool) -> RetryPolicy {
GiveUpOnPolicyWrapper(wrapped: self, giveUpCriterium: giveUpCriterium)
}

Expand All @@ -17,7 +17,7 @@ public extension RetryPolicy {
})
}

func giveUpOnErrors(matching finalErrorCriterium: @escaping (Error) -> Bool) -> RetryPolicy {
func giveUpOnErrors(matching finalErrorCriterium: @escaping @Sendable (Error) -> Bool) -> RetryPolicy {
GiveUpOnPolicyWrapper(wrapped: self, giveUpCriterium: { finalErrorCriterium($0.error) })
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import Combine
///
/// If the condition publisher completes and it had not emitted any value or the last value it emitted was `false`
/// then the retrier emits a completion embedding `RetryError.conditionPublisherCompleted` and finishes.
public class ConditionalRetrier<Output>: SingleOutputRetrier {
public class ConditionalRetrier<Output: Sendable>: SingleOutputRetrier, @unchecked Sendable {

private let policy: RetryPolicy
private let job: Job<Output>
Expand Down
2 changes: 1 addition & 1 deletion Sources/SwiftRetrier/Core/Retriers/Repeater.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import Combine
/// ```
///
/// On cancellation, the publisher emits a completion embedding a `CancellationError`then finishes.
public class Repeater<Output>: Retrier {
public class Repeater<Output: Sendable>: Retrier, @unchecked Sendable {

private let retrierBuilder: () -> AnySingleOutputRetrier<Output>
private var retrier: AnySingleOutputRetrier<Output>?
Expand Down
8 changes: 5 additions & 3 deletions Sources/SwiftRetrier/Core/Retriers/SimpleRetrier.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,14 @@ import Combine
/// the last attempt error, the publisher emits the attempt failure then a completion embedding the attempt error.
/// - **the retrier is canceled:** any awaiting on the `value` property will throw a `CancellationError`, the publisher
/// emits a completion embedding the same error then finishes.
public class SimpleRetrier<Output>: SingleOutputRetrier {
public class SimpleRetrier<Output: Sendable>: SingleOutputRetrier, @unchecked Sendable {

public let trialStart = Date()
private let subject = PassthroughSubject<RetrierEvent<Output>, Never>()
private var task: Task<Output, Error>!

public init(policy: RetryPolicy, job: @escaping Job<Output>) {
self.task = createTask(policy: policy.freshCopy(), job: job)
self.task = createTask(policy: policy, job: job)
}

@MainActor
Expand Down Expand Up @@ -49,6 +49,7 @@ public class SimpleRetrier<Output>: SingleOutputRetrier {
// Ensure we don't start before any ongoing business on main actor is finished
await MainActor.run {}
do {
var policy = policy
var attemptIndex: UInt = 0
while true {
try Task.checkCancellation()
Expand All @@ -60,7 +61,7 @@ public class SimpleRetrier<Output>: SingleOutputRetrier {
let attemptFailure = AttemptFailure(trialStart: trialStart, index: attemptIndex, error: error)
await sendAttemptFailure(attemptFailure)
try Task.checkCancellation()
let retryDecision = await MainActor.run { [attemptIndex] in
let retryDecision = await MainActor.run { [policy, attemptIndex] in
policy.shouldRetry(on: AttemptFailure(trialStart: trialStart,
index: attemptIndex,
error: error))
Expand All @@ -70,6 +71,7 @@ public class SimpleRetrier<Output>: SingleOutputRetrier {
throw error
case .retry(delay: let delay):
try await Task.sleep(nanoseconds: nanoseconds(delay))
policy = policy.policyAfter(attemptFailure: attemptFailure, delay: delay)
attemptIndex += 1
}
}
Expand Down
4 changes: 2 additions & 2 deletions Sources/SwiftRetrier/DSL/ColdRepeater.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ public struct ColdRepeater {

public extension ColdRepeater {

func giveUp(on giveUpCriterium: @escaping (AttemptFailure) -> Bool) -> ColdRepeater {
func giveUp(on giveUpCriterium: @escaping @Sendable (AttemptFailure) -> Bool) -> ColdRepeater {
let policy = policy.giveUp(on: giveUpCriterium)
return ColdRepeater(policy: policy, repeatDelay: repeatDelay, conditionPublisher: conditionPublisher)
}
Expand All @@ -24,7 +24,7 @@ public extension ColdRepeater {
return ColdRepeater(policy: policy, repeatDelay: repeatDelay, conditionPublisher: conditionPublisher)
}

func giveUpOnErrors(matching finalErrorCriterium: @escaping (Error) -> Bool) -> ColdRepeater {
func giveUpOnErrors(matching finalErrorCriterium: @escaping @Sendable (Error) -> Bool) -> ColdRepeater {
let policy = policy.giveUpOnErrors(matching: finalErrorCriterium)
return ColdRepeater(policy: policy, repeatDelay: repeatDelay, conditionPublisher: conditionPublisher)
}
Expand Down
8 changes: 4 additions & 4 deletions Sources/SwiftRetrier/DSL/ColdRetrier.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ public struct ColdRetrier {

public extension ColdRetrier {

func giveUp(on giveUpCriterium: @escaping (AttemptFailure) -> Bool) -> ColdRetrier {
func giveUp(on giveUpCriterium: @escaping @Sendable (AttemptFailure) -> Bool) -> ColdRetrier {
let policy = policy.giveUp(on: giveUpCriterium)
return ColdRetrier(policy: policy, conditionPublisher: conditionPublisher)
}
Expand All @@ -23,7 +23,7 @@ public extension ColdRetrier {
return ColdRetrier(policy: policy, conditionPublisher: conditionPublisher)
}

func giveUpOnErrors(matching finalErrorCriterium: @escaping (Error) -> Bool) -> ColdRetrier {
func giveUpOnErrors(matching finalErrorCriterium: @escaping @Sendable (Error) -> Bool) -> ColdRetrier {
let policy = policy.giveUpOnErrors(matching: finalErrorCriterium)
return ColdRetrier(policy: policy, conditionPublisher: conditionPublisher)
}
Expand All @@ -40,7 +40,7 @@ public extension ColdRetrier {
}

@discardableResult
func execute<Output>(_ job: @escaping Job<Output>) -> AnySingleOutputRetrier<Output> {
func execute<Output: Sendable>(_ job: @escaping Job<Output>) -> AnySingleOutputRetrier<Output> {
if let conditionPublisher {
return ConditionalRetrier(policy: policy, conditionPublisher: conditionPublisher, job: job)
.eraseToAnySingleOutputRetrier()
Expand All @@ -49,7 +49,7 @@ public extension ColdRetrier {
}

@discardableResult
func callAsFunction<Output>(_ job: @escaping Job<Output>) -> AnySingleOutputRetrier<Output> {
func callAsFunction<Output: Sendable>(_ job: @escaping Job<Output>) -> AnySingleOutputRetrier<Output> {
execute(job)
}
}
2 changes: 1 addition & 1 deletion Sources/SwiftRetrier/TypeErasing/AnyRetrier.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import Foundation
import Combine

public class AnyRetrier<Output>: Retrier {
public class AnyRetrier<Output: Sendable>: Retrier, @unchecked Sendable {

public let publisherBlock: () -> AnyPublisher<RetrierEvent<Output>, Never>
private let cancelBlock: () -> Void
Expand Down
4 changes: 2 additions & 2 deletions Sources/SwiftRetrier/TypeErasing/AnySingleOutputRetrier.swift
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import Foundation
import Combine

public class AnySingleOutputRetrier<Value>: AnyRetrier<Value>, SingleOutputRetrier {
public class AnySingleOutputRetrier<Value: Sendable>: AnyRetrier<Value>, SingleOutputRetrier, @unchecked Sendable {

private let outputBlock: () async throws -> Output
private let outputBlock: @Sendable () async throws -> Output

public init<R>(_ retrier: R) where R: SingleOutputRetrier, R.Output == Value {
self.outputBlock = { try await retrier.value }
Expand Down
2 changes: 1 addition & 1 deletion Sources/SwiftRetrier/Util/OnMain.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Foundation

func onMain(_ block: @escaping () -> Void) {
func onMain(_ block: @escaping @Sendable () -> Void) {
if Thread.isMainThread {
block()
} else {
Expand Down
11 changes: 6 additions & 5 deletions Tests/SwiftRetrierTests/Retriers/ConditionalRetrierTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ class ConditionalRetrierSpecificTests: XCTestCase {
try await taskWait()
}

@MainActor
func test_execution_when_condition_true() {
let condition = Just(true)
.eraseToAnyPublisher()
Expand Down Expand Up @@ -123,20 +124,20 @@ class ConditionalRetrierSpecificTests: XCTestCase {
cancellable.cancel()
}

@MainActor
func test_attempt_on_failure_propagated_during_second_trial() {
var failedOnce = false
var jobExecutionCount = 0
let job = {
let expectationOwnErrorPropagated = expectation(description: "Attempt own error propagated")

let retrier = buildRetrier(trueFalseTruePublisher(), { @MainActor in
jobExecutionCount += 1
try await taskWait()
if !failedOnce {
failedOnce = true
throw defaultError
}
}
let expectationOwnErrorPropagated = expectation(description: "Attempt own error propagated")

let retrier = buildRetrier(trueFalseTruePublisher(), job)
})
let cancellable = retrier.publisher()
.sink(receiveCompletion: { _ in },
receiveValue: {
Expand Down
Loading