diff --git a/Sources/Retry/Backoff/Algorithms/FullJitterExponentialBackoff.swift b/Sources/Retry/Backoff/Algorithms/FullJitterExponentialBackoff.swift index 9f2bcd4..27ab843 100644 --- a/Sources/Retry/Backoff/Algorithms/FullJitterExponentialBackoff.swift +++ b/Sources/Retry/Backoff/Algorithms/FullJitterExponentialBackoff.swift @@ -79,8 +79,7 @@ where ClockType: Clock, RandomNumberGeneratorType: RandomNumberGenerator { let maxDelayInClockTicks = min(baseDelayInClockTicks * Double(1 << exponent), maxDelayInClockTicks) - let delayInClockTicks = Double.random(in: 0...maxDelayInClockTicks, - using: &randomNumberGenerator) + let delayInClockTicks = randomNumberGenerator.random(in: 0...maxDelayInClockTicks) // Unfortunately, `DurationProtocol` does not have a `Duration * Double` operator, so we need to cast to `Int`. // We make sure to cast to `Int` at the end rather than at the beginning so that the imprecision is bounded. @@ -88,13 +87,13 @@ where ClockType: Clock, RandomNumberGeneratorType: RandomNumberGenerator { } } -extension FullJitterExponentialBackoff where RandomNumberGeneratorType == SystemRandomNumberGenerator { +extension FullJitterExponentialBackoff where RandomNumberGeneratorType == StandardRandomNumberGenerator { init(clock: ClockType, baseDelay: ClockType.Duration, maxDelay: ClockType.Duration?) { self.init(clock: clock, baseDelay: baseDelay, maxDelay: maxDelay, - randomNumberGenerator: SystemRandomNumberGenerator()) + randomNumberGenerator: StandardRandomNumberGenerator()) } } diff --git a/Sources/Retry/Backoff/Algorithms/Random Number Generator/RandomNumberGenerator.swift b/Sources/Retry/Backoff/Algorithms/Random Number Generator/RandomNumberGenerator.swift new file mode 100644 index 0000000..7498d7e --- /dev/null +++ b/Sources/Retry/Backoff/Algorithms/Random Number Generator/RandomNumberGenerator.swift @@ -0,0 +1,31 @@ +// MIT License +// +// Copyright © 2023 Darren Mo. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +/// A protocol that allows one to specify a different implementation from the standard one (e.g. for automated tests). +/// +/// - Remark: Cannot use the Swift standard library’s `RandomNumberGenerator` protocol for this purpose as detailed here: +/// https://github.com/apple/swift/issues/70557 +protocol RandomNumberGenerator { + func random( + in range: ClosedRange + ) -> T where T: BinaryFloatingPoint, T.RawSignificand: FixedWidthInteger +} diff --git a/Sources/Retry/Backoff/Algorithms/Random Number Generator/StandardRandomNumberGenerator.swift b/Sources/Retry/Backoff/Algorithms/Random Number Generator/StandardRandomNumberGenerator.swift new file mode 100644 index 0000000..eb10415 --- /dev/null +++ b/Sources/Retry/Backoff/Algorithms/Random Number Generator/StandardRandomNumberGenerator.swift @@ -0,0 +1,29 @@ +// MIT License +// +// Copyright © 2023 Darren Mo. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +struct StandardRandomNumberGenerator: RandomNumberGenerator { + func random( + in range: ClosedRange + ) -> T where T: BinaryFloatingPoint, T.RawSignificand: FixedWidthInteger { + return T.random(in: range) + } +} diff --git a/Tests/RetryTests/Fakes/RandomNumberGeneratorFake.swift b/Tests/RetryTests/Fakes/RandomNumberGeneratorFake.swift index 7960a5a..05e5146 100644 --- a/Tests/RetryTests/Fakes/RandomNumberGeneratorFake.swift +++ b/Tests/RetryTests/Fakes/RandomNumberGeneratorFake.swift @@ -20,6 +20,8 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. +@testable import Retry + class RandomNumberGeneratorFake: RandomNumberGenerator { enum Mode { case min @@ -31,15 +33,15 @@ class RandomNumberGeneratorFake: RandomNumberGenerator { self.mode = mode } - func next() -> UInt64 { + func random( + in range: ClosedRange + ) -> T where T : BinaryFloatingPoint, T.RawSignificand : FixedWidthInteger { switch mode { case .min: - // Add `1` to work around the following issue with Swift’s random number generator implementation: - // https://github.com/apple/swift/issues/70557 - return .min + 1 + return range.lowerBound case .max: - return .max + return range.upperBound } } }