diff --git a/Sources/CircuitBreaker/CircuitBreaker.swift b/Sources/CircuitBreaker/CircuitBreaker.swift
index 08a3e58..943570a 100644
--- a/Sources/CircuitBreaker/CircuitBreaker.swift
+++ b/Sources/CircuitBreaker/CircuitBreaker.swift
@@ -77,13 +77,19 @@ public class CircuitBreaker {
/// The Circuit Breaker's current state.
public private(set) var breakerState: State {
get {
- return state
+ breakerStateSemaphore.wait()
+ let tempState = state
+ breakerStateSemaphore.signal()
+ return tempState
}
set {
+ breakerStateSemaphore.wait()
state = newValue
+ breakerStateSemaphore.signal()
}
}
+ // This the backing store for the state and must be accessed via breakerState
private(set) var state = State.closed
private let failures: FailureQueue
//fallback function is invoked ONLY when failing fast OR when timing out OR when application
@@ -93,9 +99,11 @@ public class CircuitBreaker {
private let bulkhead: Bulkhead?
/// Dispatch
+ // resetTimer should be started via startResetTimer
private var resetTimer: DispatchSourceTimer?
private let semaphoreCircuit = DispatchSemaphore(value: 1)
-
+ private let timerSemaphore = DispatchSemaphore(value: 1)
+ private let breakerStateSemaphore = DispatchSemaphore(value: 1)
private let queue = DispatchQueue(label: "Circuit Breaker Queue", attributes: .concurrent)
// MARK: Initializers
@@ -215,16 +223,12 @@ public class CircuitBreaker {
/// Method to force the circuit open.
public func forceOpen() {
- semaphoreCircuit.wait()
open()
- semaphoreCircuit.signal()
}
/// Method to force the circuit closed.
public func forceClosed() {
- semaphoreCircuit.wait()
close()
- semaphoreCircuit.signal()
}
/// Method to force the circuit half open.
@@ -340,6 +344,7 @@ public class CircuitBreaker {
/// Reset timer setup
private func startResetTimer(delay: DispatchTimeInterval) {
+ timerSemaphore.wait()
// Cancel previous timer if any
resetTimer?.cancel()
@@ -352,6 +357,7 @@ public class CircuitBreaker {
resetTimer?.schedule(deadline: .now() + delay)
resetTimer?.resume()
+ timerSemaphore.signal()
}
}
diff --git a/Tests/CircuitBreakerTests/CircuitBreakerTests.swift b/Tests/CircuitBreakerTests/CircuitBreakerTests.swift
index 763f20b..78a010e 100644
--- a/Tests/CircuitBreakerTests/CircuitBreakerTests.swift
+++ b/Tests/CircuitBreakerTests/CircuitBreakerTests.swift
@@ -58,14 +58,16 @@ class CircuitBreakerTests: XCTestCase {
("testBulkhead", testBulkhead),
("testBulkheadCtxFunction", testBulkheadCtxFunction),
("testBulkheadFullQueue", testBulkheadFullQueue),
- ("testFallback", testFallback),
("testStateCycle", testStateCycle),
+ ("testFallback", testFallback),
("testRollingWindow", testRollingWindow),
("testSmallRollingWindow", testSmallRollingWindow)
]
}
// Test instance vars
+ let semaphore = DispatchSemaphore(value: 1)
+ let dispatchGroup = DispatchGroup()
var timedOut: Bool = false
var fastFailed: Bool = false
var invocationErrored = false
@@ -74,10 +76,12 @@ class CircuitBreakerTests: XCTestCase {
override func setUp() {
super.setUp()
//HeliumLogger.use(LoggerMessageType.debug)
+ semaphore.wait()
timedOut = false
fastFailed = false
testCalled = false
invocationErrored = false
+ semaphore.signal()
}
func dispatchTime(afterMs: Int) -> DispatchTime {
@@ -109,6 +113,7 @@ class CircuitBreakerTests: XCTestCase {
// There is no 1-tuple in Swift...
// https://medium.com/swift-programming/facets-of-swift-part-2-tuples-4bfe58d21abf#.v4rj4md9c
func fallbackFunction(error: BreakerError, expectedError: BreakerError) -> Void {
+ semaphore.wait()
switch error {
case .timeout:
timedOut = true
@@ -116,30 +121,28 @@ class CircuitBreakerTests: XCTestCase {
fastFailed = true
default:
invocationErrored = true
-
}
+ semaphore.signal()
// Validate the outcome was the desired one
XCTAssertEqual(error, expectedError, "Breaker error was not the expected one.")
}
func time(milliseconds: Int) {
- #if os(Linux)
usleep(UInt32(milliseconds * 1000))
- #elseif swift(>=4.2)
- RunLoop.current.run(mode: RunLoop.Mode.default, before: Date(timeIntervalSinceNow: 0.001))
- let time: Double = Double(milliseconds) / 1000
- RunLoop.current.run(mode: RunLoop.Mode.default, before: Date(timeIntervalSinceNow: time))
- #else
- RunLoop.current.run(mode: .defaultRunLoopMode, before: Date(timeIntervalSinceNow: 0.001))
- let time: Double = Double(milliseconds) / 1000
- RunLoop.current.run(mode: .defaultRunLoopMode, before: Date(timeIntervalSinceNow: time))
- #endif
}
func timeCtxFunction(invocation: Invocation<(Int), BreakerError>) {
time(milliseconds: invocation.commandArgs)
invocation.notifySuccess()
}
+
+ func timeDispatchGroupFunction(invocation: Invocation) {
+ let args = invocation.commandArgs
+ DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(args), execute: {
+ invocation.notifySuccess()
+ self.dispatchGroup.leave()
+ })
+ }
func test(inv: Invocation<(Void), BreakerError>) { testCalled = true; inv.notifySuccess() }
@@ -376,12 +379,17 @@ class CircuitBreakerTests: XCTestCase {
// Test timeout
func testTimeout() {
- // Command will timeout, breaker will still be closed.
- let breaker = CircuitBreaker(name: "Test", timeout: 50, command: timeCtxFunction, fallback: fallbackFunction)
+ let expectation1 = expectation(description: "Command will timeout, breaker will still be closed.")
+
+ let breaker = CircuitBreaker(name: "Test", timeout: 50, command: timeDispatchGroupFunction, fallback: fallbackFunction)
+ dispatchGroup.enter()
breaker.run(commandArgs: 100, fallbackArgs: BreakerError.timeout)
-
- XCTAssertEqual(breaker.breakerState, State.closed)
- XCTAssertEqual(self.timedOut, true)
+ dispatchGroup.notify(queue: .main) {
+ XCTAssertEqual(breaker.breakerState, State.closed)
+ XCTAssertEqual(self.timedOut, true)
+ expectation1.fulfill()
+ }
+ waitForExpectations(timeout: 20)
}
// Test timeout and reset
@@ -558,31 +566,25 @@ class CircuitBreakerTests: XCTestCase {
func testBulkheadFullQueue() {
let expectation1 = expectation(description: "Wait for a predefined amount of time and then return.")
- func timeBulkhead(invocation: Invocation<(Bool, Int), BreakerError>) {
- let args = invocation.commandArgs
- sleep(UInt32(args.1 / 1000))
- if args.0 {
- expectation1.fulfill()
- }
- }
-
let timeout = 200
let maxFailures = 4
// Validate test case configuration
XCTAssertTrue(maxFailures > 1)
- let breaker = CircuitBreaker(name: "Test", timeout: timeout, maxFailures: maxFailures, bulkhead: 2, command: timeBulkhead, fallback: fallbackFunction)
+ let breaker = CircuitBreaker(name: "Test", timeout: timeout, maxFailures: maxFailures, bulkhead: 2, command: timeDispatchGroupFunction, fallback: fallbackFunction)
- for index in 1..