Skip to content

Commit 12121f5

Browse files
Aaron Liberatorerolivieri
authored andcommitted
Hystrix support (#42)
* Hystrix support * Updates to observer pattern * Minor tweak * Removes weak adds comments * Stats * docs * [skip ci] updates readme * documentation update [ci skip] * documentation update [ci skip] * documentation update [ci skip]
1 parent 948eb0f commit 12121f5

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

56 files changed

+4407
-195
lines changed

README.md

Lines changed: 44 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ The Circuit Breaker design pattern is used to increase application stability, im
1111
* [Installation](#installation)
1212
* [Usage](#usage)
1313
* [API](#api)
14+
* [License](#license)
1415

1516
## Swift version
1617
The latest version of CircuitBreaker works with the `4.0.3` and newer version of the Swift binaries. You can download this version of the Swift binaries by following this [link](https://swift.org/download/#releases).
@@ -28,7 +29,7 @@ To leverage the CircuitBreaker package in your Swift application, you should spe
2829

2930
dependencies: [
3031
// Swift 4
31-
.package(url: "https://github.com/IBM-Swift/CircuitBreaker.git", .upToNextMajor(from: "4.0.0")),
32+
.package(url: "https://github.com/IBM-Swift/CircuitBreaker.git", .upToNextMajor(from: "5.0.0")),
3233
...
3334

3435
])
@@ -189,6 +190,20 @@ CircuitBreaker(timeout: Int = 1000, resetTimeout: Int = 60000, maxFailures: Int
189190
* `command` Contextual function to circuit break, which allows user defined failures (the context provides an indirect reference to the corresponding circuit breaker instance).
190191

191192
### Stats
193+
194+
#### Tracked Stats:
195+
* Total Requests
196+
* Concurrent Requests
197+
* Rejected Requests
198+
* Successful Responses
199+
* Average Execution Response Time
200+
* Average Total Response Time
201+
* Failed Responses
202+
* Total Timeouts
203+
* Total Latency
204+
* Total Execution Latency
205+
* Hystrix Compliant Snapshot
206+
192207
```swift
193208
...
194209
// Create CircuitBreaker
@@ -199,18 +214,37 @@ breaker.run(commandArgs: (a: 10, b: 20), fallbackArgs: "Something went wrong.")
199214

200215
// Log Stats snapshot
201216
breaker.snapshot()
217+
218+
// Hystrix compliant snapshot
219+
let snapshot = breaker.snapshot
202220
...
203221
```
204222

205-
#### Tracked Stats:
206-
* Total Requests
207-
* Concurrent Requests
208-
* Rejected Requests
209-
* Successful Responses
210-
* Average Response Time
211-
* Failed Responses
212-
* Total Timeouts
213-
* Total Latency
223+
#### Observing stats
224+
The CircuitBreaker library provides an interface for observing new CircuitBreaker instances in order to register and track stat changes. In the initialization of a CircuitBreaker instance, the linked monitors are notified of its instantiation allowing them to begin tracking the instance's stats. The CircuitBreaker instance exposes a Hystrix compliant stat snapshot to the monitor which can then be processed accordingly.
225+
226+
```swift
227+
228+
/// Initialize stat monitors
229+
let monitor1 = SwiftMetrics()
230+
let monitor2 = ...
231+
...
232+
let monitorN = ...
233+
234+
/// Register monitors
235+
CircuitBreaker.addMonitor(monitor1)
236+
CircuitBreaker.addMonitor(monitor2)
237+
...
238+
CircuitBreaker.addMonitor(monitorN)
239+
240+
// Create instances of CircuitBreaker
241+
let circuit1 = CircuitBreaker()
242+
let circuit2 = CircuitBreaker()
243+
...
244+
let circuitN = CircuitBreaker()
245+
```
246+
247+
As mentioned above, the initializer takes care of notifying each one of the monitors of the new CircuitBreaker instance.
214248

215249
## License
216250
This Swift package is licensed under Apache 2.0. Full license text is available in [LICENSE](LICENSE).

Sources/CircuitBreaker/BreakerError.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/**
2-
* Copyright IBM Corporation 2017
2+
* Copyright IBM Corporation 2017, 2018
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.

Sources/CircuitBreaker/Bulkhead.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/**
2-
* Copyright IBM Corporation 2017
2+
* Copyright IBM Corporation 2017, 2018
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.

Sources/CircuitBreaker/CircuitBreaker.swift

Lines changed: 41 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/**
2-
* Copyright IBM Corporation 2017
2+
* Copyright IBM Corporation 2017, 2018
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -31,6 +31,12 @@ public class CircuitBreaker<A, B> {
3131

3232
// MARK: Public Fields
3333

34+
/// Name of Circuit Breaker Instance
35+
public private(set) var name: String
36+
37+
// Name of Circuit Breaker Group
38+
public private(set) var group: String?
39+
3440
/// Execution timeout for command contect (Default: 1000 ms)
3541
public let timeout: Int
3642

@@ -75,6 +81,8 @@ public class CircuitBreaker<A, B> {
7581
/// Initializes CircuitBreaker instance with asyncronous context command (Advanced usage)
7682
///
7783
/// - Parameters:
84+
/// - name: name of the circuit instance
85+
/// - group: optional group description
7886
/// - timeout: Execution timeout for command contect (Default: 1000 ms)
7987
/// - resetTimeout: Timeout to reset circuit (Default: 6000 ms)
8088
/// - maxFailures: Maximum number of failures allowed before opening circuit (Default: 5)
@@ -86,13 +94,17 @@ public class CircuitBreaker<A, B> {
8694
/// - fallback: Function user specifies to signal timeout or fastFail completion.
8795
/// Required format: (BreakerError, (fallbackArg1, fallbackArg2,...)) -> Void
8896
///
89-
public init(timeout: Int = 1000,
97+
public init(name: String,
98+
group: String? = nil,
99+
timeout: Int = 1000,
90100
resetTimeout: Int = 60000,
91101
maxFailures: Int = 5,
92102
rollingWindow: Int = 10000,
93103
bulkhead: Int = 0,
94104
command: @escaping AnyContextFunction<A>,
95105
fallback: @escaping AnyFallback<B>) {
106+
self.name = name
107+
self.group = group
96108
self.timeout = timeout
97109
self.resetTimeout = resetTimeout
98110
self.maxFailures = maxFailures
@@ -101,6 +113,10 @@ public class CircuitBreaker<A, B> {
101113
self.command = command
102114
self.failures = FailureQueue(size: maxFailures)
103115
self.bulkhead = (bulkhead > 0) ? Bulkhead.init(limit: bulkhead) : nil
116+
117+
// Link to Observers
118+
119+
MonitorCollection.sharedInstance.values.forEach { $0.register(breakerRef: self) }
104120
}
105121

106122
// MARK: Class Methods
@@ -122,41 +138,37 @@ public class CircuitBreaker<A, B> {
122138

123139
if let bulkhead = self.bulkhead {
124140
bulkhead.enqueue {
125-
self.callFunction(commandArgs: commandArgs, fallbackArgs: fallbackArgs)
141+
self.callFunction(startTime: startTime, commandArgs: commandArgs, fallbackArgs: fallbackArgs)
126142
}
127143
} else {
128-
callFunction(commandArgs: commandArgs, fallbackArgs: fallbackArgs)
144+
callFunction(startTime: startTime, commandArgs: commandArgs, fallbackArgs: fallbackArgs)
129145
}
130146

131-
self.breakerStats.trackLatency(latency: Int(Date().timeIntervalSince(startTime)))
132-
133147
case .closed:
134148
let startTime = Date()
135149

136150
if let bulkhead = self.bulkhead {
137151
bulkhead.enqueue {
138-
self.callFunction(commandArgs: commandArgs, fallbackArgs: fallbackArgs)
152+
self.callFunction(startTime: startTime, commandArgs: commandArgs, fallbackArgs: fallbackArgs)
139153
}
140154
} else {
141-
callFunction(commandArgs: commandArgs, fallbackArgs: fallbackArgs)
155+
callFunction(startTime: startTime, commandArgs: commandArgs, fallbackArgs: fallbackArgs)
142156
}
143-
144-
self.breakerStats.trackLatency(latency: Int(Date().timeIntervalSince(startTime)))
145157
}
146158
}
147159

148160
/// Method to print current stats
149-
public func snapshot() {
161+
public func logSnapshot() {
150162
breakerStats.snapshot()
151163
}
152164

153165
/// Method to notifcy circuit of a completion with a failure
154-
func notifyFailure(error: BreakerError, fallbackArgs: B) {
166+
internal func notifyFailure(error: BreakerError, fallbackArgs: B) {
155167
handleFailure(error: error, fallbackArgs: fallbackArgs)
156168
}
157169

158170
/// Method to notifcy circuit of a successful completion
159-
func notifySuccess() {
171+
internal func notifySuccess() {
160172
handleSuccess()
161173
}
162174

@@ -180,9 +192,9 @@ public class CircuitBreaker<A, B> {
180192
}
181193

182194
/// Wrapper for calling and handling CircuitBreaker command
183-
private func callFunction(commandArgs: A, fallbackArgs: B) {
195+
private func callFunction(startTime: Date, commandArgs: A, fallbackArgs: B) {
184196

185-
let invocation = Invocation(breaker: self, commandArgs: commandArgs, fallbackArgs: fallbackArgs)
197+
let invocation = Invocation(startTime: startTime, breaker: self, commandArgs: commandArgs, fallbackArgs: fallbackArgs)
186198

187199
setTimeout { [weak invocation, weak self] in
188200
if invocation?.nofityTimedOut() == true {
@@ -251,6 +263,7 @@ public class CircuitBreaker<A, B> {
251263
private func handleSuccess() {
252264
semaphoreCircuit.wait()
253265
Log.verbose("Handling success...")
266+
254267
if state == .halfopen {
255268
close()
256269
}
@@ -300,3 +313,16 @@ public class CircuitBreaker<A, B> {
300313
resetTimer?.resume()
301314
}
302315
}
316+
317+
extension CircuitBreaker: StatsProvider {
318+
319+
/// Method to create link a StatsMonitor Instance
320+
public static func addMonitor(monitor: StatsMonitor) {
321+
MonitorCollection.sharedInstance.values.append(monitor)
322+
}
323+
324+
/// Property to compute snapshot
325+
public var snapshot: Snapshot {
326+
return Snapshot(name: name, group: group, stats: self.breakerStats, state: breakerState)
327+
}
328+
}

Sources/CircuitBreaker/Collection.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/**
2-
* Copyright IBM Corporation 2017
2+
* Copyright IBM Corporation 2017, 2018
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.

Sources/CircuitBreaker/Invocation.swift

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/**
2-
* Copyright IBM Corporation 2017
2+
* Copyright IBM Corporation 2017, 2018
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -32,6 +32,10 @@ public class Invocation<A, B> {
3232
/// Completion state of invocation
3333
private(set) var completed: Bool = false
3434

35+
private let startTime: Date
36+
37+
private let startExecutionTime = Date()
38+
3539
// Semaphore to avoid race conditions in the state of the invocation
3640
private let semaphoreCompleted = DispatchSemaphore(value: 1)
3741

@@ -42,10 +46,11 @@ public class Invocation<A, B> {
4246
/// - breaker CircuitBreaker Instance
4347
/// - commandArgs Arguments for command context
4448
///
45-
public init(breaker: CircuitBreaker<A, B>, commandArgs: A, fallbackArgs: B) {
49+
public init(startTime: Date, breaker: CircuitBreaker<A, B>, commandArgs: A, fallbackArgs: B) {
4650
self.breaker = breaker
4751
self.commandArgs = commandArgs
4852
self.fallbackArgs = fallbackArgs
53+
self.startTime = startTime
4954
}
5055

5156
/// Marks invocation as having timed out if and only if the execution
@@ -56,6 +61,9 @@ public class Invocation<A, B> {
5661
if !self.completed {
5762
setTimedOut()
5863
semaphoreCompleted.signal()
64+
// Revisit Execution versus Total Latency
65+
// Track latency for timeout in same way as brakes (https://github.com/awolden/brakes/)
66+
breaker?.breakerStats.trackTotalLatency(latency: Int(Date().timeIntervalSince(startTime)))
5967
return true
6068
}
6169
semaphoreCompleted.signal()
@@ -83,6 +91,8 @@ public class Invocation<A, B> {
8391
self.setCompleted()
8492
semaphoreCompleted.signal()
8593
breaker?.notifySuccess()
94+
breaker?.breakerStats.trackTotalLatency(latency: Int(Date().timeIntervalSince(startTime)))
95+
breaker?.breakerStats.trackExecutionLatency(latency: Int(Date().timeIntervalSince(startTime)))
8696
return
8797
}
8898
semaphoreCompleted.signal()
@@ -99,6 +109,8 @@ public class Invocation<A, B> {
99109
self.setCompleted()
100110
semaphoreCompleted.signal()
101111
breaker?.notifyFailure(error: error, fallbackArgs: fallbackArgs)
112+
breaker?.breakerStats.trackTotalLatency(latency: Int(Date().timeIntervalSince(startTime)))
113+
breaker?.breakerStats.trackExecutionLatency(latency: Int(Date().timeIntervalSince(startTime)))
102114
return
103115
}
104116
semaphoreCompleted.signal()
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/**
2+
* Copyright IBM Corporation 2017, 2018
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
**/
16+
17+
import Foundation
18+
19+
/// Protocol identifying a stats monitor
20+
public protocol StatsMonitor {
21+
22+
/// References to monitored CircuitBreaker instances
23+
var refs: [StatsProvider] { get set }
24+
25+
/// Method to register a stats provider
26+
///
27+
/// - Parameters:
28+
/// - breakerRef: The StatsProvider to monitor
29+
func register(breakerRef: StatsProvider)
30+
31+
}
32+
33+
/// Protocol identifying a stats provider
34+
public protocol StatsProvider: class {
35+
36+
/// Registers a monitor for a breaker reference
37+
static func addMonitor(monitor: StatsMonitor)
38+
39+
/// Histrix compliant instance
40+
var snapshot: Snapshot { get }
41+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/**
2+
* Copyright IBM Corporation 2017, 2018
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
**/
16+
17+
internal struct MonitorCollection {
18+
19+
internal var values: [StatsMonitor] = []
20+
21+
internal static var sharedInstance = MonitorCollection()
22+
23+
private init() {}
24+
25+
}

0 commit comments

Comments
 (0)