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..