Skip to content

Commit

Permalink
Swift 6 mode (#26)
Browse files Browse the repository at this point in the history
* Swift 6 mode, refs #25

* Update workflow
  • Loading branch information
PierreMardon authored Jun 16, 2024
1 parent 4db9d72 commit af389d7
Show file tree
Hide file tree
Showing 26 changed files with 93 additions and 73 deletions.
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

0 comments on commit af389d7

Please sign in to comment.