├── .github └── workflows │ └── crystal.yml ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── doc ├── CircuitBreaker.html ├── css │ └── style.css ├── index.html └── js │ └── doc.js ├── shard.yml ├── spec ├── circuit_breaker_spec.cr ├── circuit_state_spec.cr └── error_watcher_spec.cr └── src ├── circuit_breaker.cr ├── circuit_state.cr └── error_watcher.cr /.github/workflows/crystal.yml: -------------------------------------------------------------------------------- 1 | name: Crystal CI 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | schedule: 9 | - cron: "0 0 * * 1" 10 | 11 | jobs: 12 | build: 13 | 14 | runs-on: ubuntu-latest 15 | 16 | container: 17 | image: crystallang/crystal 18 | 19 | steps: 20 | - uses: actions/checkout@v3 21 | - name: Install dependencies 22 | run: shards install 23 | - name: Run tests 24 | run: crystal spec 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: crystal 2 | crystal: 3 | - latest 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Thomas Peikert 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # circuit_breaker 2 | 3 | ###### This project is being built weekly with the latest crystal version (works with v1.7.2 🎉) 4 | 5 | Simple Implementation of the [circuit breaker pattern](http://martinfowler.com/bliki/CircuitBreaker.html) in Crystal. 6 | 7 | ## What??!? 8 | 9 | > The basic idea behind the circuit breaker is very simple. You wrap a protected function call in a circuit breaker object, which monitors for failures. Once the failures reach a certain threshold, the circuit breaker trips, and all further calls to the circuit breaker return with an error, without the protected call being made at all. Usually you'll also want some kind of monitor alert if the circuit breaker trips. - Martin Fowler 10 | 11 | Given a certain error threshold, timeframe and timeout window, a breaker can be used to monitor criticial command executions. Circuit breakers are usually used to prevent unnecessary requests if a server ressource et al becomes unavailable. This protects the server from additional load and allows it to recover and relieves the client from requests that are doomed to fail. 12 | 13 | Wrap API calls inside a breaker, if the error rate in a given time frame surpasses a certain threshold, all subsequent calls will fail for a given duration. 14 | 15 | ## Installation 16 | 17 | Add to your shard.yml 18 | 19 | ```yaml 20 | dependencies: 21 | circuit_breaker: 22 | github: tpei/circuit_breaker 23 | branch: master 24 | ``` 25 | 26 | and then install the library into your project with 27 | 28 | ```bash 29 | $ crystal deps 30 | ``` 31 | 32 | ## Usage 33 | 34 | Create a new breaker: 35 | ```crystal 36 | require "circuit_breaker" 37 | 38 | breaker = CircuitBreaker.new( 39 | threshold: 5, # % of errors before you want to trip the circuit 40 | timewindow: 60, # in s: anything older will be ignored in error_rate 41 | reenable_after: 300 # after x seconds, the breaker will allow executions again 42 | ) 43 | ``` 44 | 45 | Then wrap whatever you like: 46 | ```crystal 47 | breaker.run do 48 | my_rest_call() 49 | end 50 | ``` 51 | 52 | ### Handling CircuitBreaker trips 53 | 54 | The Breaker will open and throw an CircuitOpenException for all subsequent calls, once the threshold is reached. You can of course catch these exceptions and do whatever you want :D 55 | ```crystal 56 | begin 57 | breaker.run do 58 | my_rest_call() 59 | end 60 | rescue exc : CircuitOpenException 61 | log "happens to the best of us..." 62 | 42 63 | end 64 | ``` 65 | 66 | After the given reenable time, the circuit will transition to "half open". This will completely reset the circuit if the next execution succeeds, but reopen the circuit and reset the timer if the next execution fails. 67 | 68 | ### Handling only certain error types 69 | 70 | If you are feeling really funky, you can also limit the exception classes to monitor. You might want to catch `RandomRestError`, but not `ArgumentError`, so do this: 71 | ```crystal 72 | breaker = CircuitBreaker.new( 73 | threshold: 5, 74 | timewindow: 60, 75 | reenable_after: 300, 76 | handled_errors: [RandomRestError.new] 77 | ) 78 | 79 | breaker.run 80 | raise ArgumentError.new("won't count towards the error rate") 81 | end 82 | ``` 83 | 84 | ### Ignoring certain error types 85 | 86 | Conversely, you can also add custom errors to ignore and count all others: 87 | ```crystal 88 | breaker = CircuitBreaker.new( 89 | threshold: 5, 90 | timewindow: 60, 91 | reenable_after: 300, 92 | ignored_errors: [ArgumentError.new] 93 | ) 94 | 95 | breaker.run 96 | raise ArgumentError.new("won't count towards the error rate") 97 | end 98 | ``` 99 | 100 | Unfortunately this both won't match against exception subclasses just yet, so at the moment you have to specify the exact class to monitor and can't just use `RestException` to match every subclass like `RestTimeoutException < RestException`... 101 | 102 | 103 | ## Thanks 104 | Special thanks goes to Pedro Belo on whose ruby circuit breaker implementation ([CB2](https://github.com/pedro/cb2)) this is loosely based. 105 | -------------------------------------------------------------------------------- /doc/CircuitBreaker.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | CircuitBreaker - github.com/TPei/circuit_breaker 9 | 10 | 11 | 12 |
13 | 16 | 17 | 20 | 21 | 29 | 30 |
31 | 32 |
33 |

34 | 35 | class CircuitBreaker 36 | 37 |

38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 |

Overview

46 | 47 |

Simple Implementation of the circuit breaker pattern in Crystal.

48 | 49 |

Given a certain error threshold, timeframe and timeout window, a breaker can be used to monitor criticial command executions. Circuit breakers are usually used to prevent unnecessary requests if a server ressource et al becomes unavailable. This protects the server from additional load and allows it to recover and relieves the client from requests that are doomed to fail.

50 | 51 |

Wrap API calls inside a breaker, if the error rate in a given time frame surpasses a certain threshold, all subsequent calls will fail for a given duration.

52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 |

Defined in:

67 | 68 | 69 | circuit_breaker.cr 70 | 71 |
72 | 73 | 74 | 75 | 76 | 77 | 78 |

Class Method Summary

79 | 89 | 90 | 91 | 92 |

Instance Method Summary

93 | 103 | 104 | 105 | 106 | 107 | 108 |
109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 |
125 | 126 | 127 |

Class Method Detail

128 | 129 |
130 |
131 | 132 | def self.new(threshold error_threshold, timewindow timeframe, reenable_after duration, handled_errors = [] of Exception, ignored_errors = [] of Exception) 133 | 134 | # 135 |
136 | 137 |

creates a CircuitBreaker instance with a specified error threshold, timeframe, breaker duration and optionally a number of ignored or handled errors

138 | 139 |
breaker = CircuitBreaker.new(
140 |   threshold: 5, # % of errors before you want to trip the circuit
141 |   timewindow: 60, # in s: anything older will be ignored in error_rate
142 |   reenable_after: 300 # after x seconds, the breaker will allow executions again
143 | )
144 | 145 |
146 |
147 | 148 | [View source] 149 | 150 |
151 |
152 | 153 | 154 | 155 | 156 |

Instance Method Detail

157 | 158 |
159 |
160 | 161 | def run(&block) 162 | 163 | # 164 |
165 | 166 |

get's passed a block to watch for errors 167 | every error thrown inside your block counts towards the error rate 168 | once the threshold is surpassed, it starts throwing CircuitOpenExceptions 169 | you can catch these rrors and implement some fallback behaviour

170 | 171 |
begin
172 |   breaker.run do
173 |     my_rest_call()
174 |   end
175 | rescue exc : CircuitOpenException
176 |   log "happens to the best of us..."
177 |   42
178 | end
179 | 180 |
181 |
182 | 183 | [View source] 184 | 185 |
186 |
187 | 188 | 189 | 190 | 191 | 192 |
193 | 194 | 195 | 196 | -------------------------------------------------------------------------------- /doc/css/style.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | position: relative; 3 | margin: 0; 4 | padding: 0; 5 | width: 100%; 6 | height: 100%; 7 | overflow: hidden; 8 | } 9 | 10 | body { 11 | font-family: "Avenir", "Tahoma", "Lucida Sans", "Lucida Grande", Verdana, Arial, sans-serif; 12 | color: #333; 13 | } 14 | 15 | a { 16 | color: #263F6C; 17 | } 18 | 19 | a:visited { 20 | color: #112750; 21 | } 22 | 23 | h1, h2, h3, h4, h5, h6 { 24 | margin: 35px 0 25px; 25 | color: #444444; 26 | } 27 | 28 | h1.type-name { 29 | color: #47266E; 30 | margin: 20px 0 30px; 31 | background-color: #F8F8F8; 32 | padding: 10px 12px; 33 | border: 1px solid #EBEBEB; 34 | border-radius: 2px; 35 | } 36 | 37 | h2 { 38 | border-bottom: 1px solid #E6E6E6; 39 | padding-bottom: 5px; 40 | } 41 | 42 | #types-list, #main-content { 43 | position: absolute; 44 | top: 0; 45 | bottom: 0; 46 | overflow: auto; 47 | } 48 | 49 | #types-list { 50 | left: 0; 51 | width: 20%; 52 | background-color: #2E1052; 53 | padding: 0 0 30px; 54 | box-shadow: inset -3px 0 4px rgba(0,0,0,.35); 55 | } 56 | 57 | #types-list #search-box { 58 | padding: 8px 9px; 59 | } 60 | 61 | #types-list input { 62 | display: block; 63 | box-sizing: border-box; 64 | margin: 0; 65 | padding: 5px; 66 | font: inherit; 67 | font-family: inherit; 68 | line-height: 1.2; 69 | width: 100%; 70 | border: 0; 71 | outline: 0; 72 | border-radius: 2px; 73 | box-shadow: 0px 3px 5px rgba(0,0,0,.25); 74 | transition: box-shadow .12s; 75 | } 76 | 77 | #types-list input:focus { 78 | box-shadow: 0px 5px 6px rgba(0,0,0,.5); 79 | } 80 | 81 | #types-list input::-webkit-input-placeholder { /* Chrome/Opera/Safari */ 82 | color: #C8C8C8; 83 | font-size: 14px; 84 | text-indent: 2px; 85 | } 86 | 87 | #types-list input::-moz-placeholder { /* Firefox 19+ */ 88 | color: #C8C8C8; 89 | font-size: 14px; 90 | text-indent: 2px; 91 | } 92 | 93 | #types-list input:-ms-input-placeholder { /* IE 10+ */ 94 | color: #C8C8C8; 95 | font-size: 14px; 96 | text-indent: 2px; 97 | } 98 | 99 | #types-list input:-moz-placeholder { /* Firefox 18- */ 100 | color: #C8C8C8; 101 | font-size: 14px; 102 | text-indent: 2px; 103 | } 104 | 105 | #types-list ul { 106 | margin: 0; 107 | padding: 0; 108 | list-style: none outside; 109 | } 110 | 111 | #types-list li { 112 | display: block; 113 | position: relative; 114 | } 115 | 116 | #types-list li.hide { 117 | display: none; 118 | } 119 | 120 | #types-list a { 121 | display: block; 122 | padding: 5px 15px 5px 30px; 123 | text-decoration: none; 124 | color: #F8F4FD; 125 | transition: color .14s; 126 | } 127 | 128 | #types-list a:focus { 129 | outline: 1px solid #D1B7F1; 130 | } 131 | 132 | #types-list .current > a, 133 | #types-list a:hover { 134 | color: #866BA6; 135 | } 136 | 137 | #types-list li ul { 138 | overflow: hidden; 139 | height: 0; 140 | max-height: 0; 141 | transition: 1s ease-in-out; 142 | } 143 | 144 | 145 | #types-list li.parent { 146 | padding-left: 30px; 147 | } 148 | 149 | #types-list li.parent::before { 150 | box-sizing: border-box; 151 | content: "▼"; 152 | display: block; 153 | width: 30px; 154 | height: 30px; 155 | position: absolute; 156 | top: 0; 157 | left: 0; 158 | text-align: center; 159 | color: white; 160 | font-size: 8px; 161 | line-height: 30px; 162 | transform: rotateZ(-90deg); 163 | cursor: pointer; 164 | transition: .2s linear; 165 | } 166 | 167 | 168 | #types-list li.parent > a { 169 | padding-left: 0; 170 | } 171 | 172 | #types-list li.parent.open::before { 173 | transform: rotateZ(0); 174 | } 175 | 176 | #types-list li.open > ul { 177 | height: auto; 178 | max-height: 1000em; 179 | } 180 | 181 | #main-content { 182 | padding: 0 30px 30px 30px; 183 | left: 20%; 184 | right: 0; 185 | } 186 | 187 | .kind { 188 | font-size: 60%; 189 | color: #866BA6; 190 | } 191 | 192 | .superclass-hierarchy { 193 | margin: -15px 0 30px 0; 194 | padding: 0; 195 | list-style: none outside; 196 | font-size: 80%; 197 | } 198 | 199 | .superclass-hierarchy .superclass { 200 | display: inline-block; 201 | margin: 0 7px 0 0; 202 | padding: 0; 203 | } 204 | 205 | .superclass-hierarchy .superclass + .superclass::before { 206 | content: "<"; 207 | margin-right: 7px; 208 | } 209 | 210 | .other-types-list li { 211 | display: inline-block; 212 | } 213 | 214 | .other-types-list, 215 | .list-summary { 216 | margin: 0 0 30px 0; 217 | padding: 0; 218 | list-style: none outside; 219 | } 220 | 221 | .entry-const { 222 | font-family: Consolas, 'Courier New', Courier, Monaco, monospace; 223 | } 224 | 225 | .entry-summary { 226 | padding-bottom: 4px; 227 | } 228 | 229 | .superclass-hierarchy .superclass a, 230 | .other-type a, 231 | .entry-summary .signature { 232 | padding: 4px 8px; 233 | margin-bottom: 4px; 234 | display: inline-block; 235 | background-color: #f8f8f8; 236 | color: #47266E; 237 | border: 1px solid #f0f0f0; 238 | text-decoration: none; 239 | border-radius: 3px; 240 | font-family: Consolas, 'Courier New', Courier, Monaco, monospace; 241 | transition: background .15s, border-color .15s; 242 | } 243 | 244 | .superclass-hierarchy .superclass a:hover, 245 | .other-type a:hover, 246 | .entry-summary .signature:hover { 247 | background: #D5CAE3; 248 | border-color: #624288; 249 | } 250 | 251 | .entry-summary .summary { 252 | padding-left: 32px; 253 | } 254 | 255 | .entry-summary .summary p { 256 | margin: 12px 0 16px; 257 | } 258 | 259 | .entry-summary a { 260 | text-decoration: none; 261 | } 262 | 263 | .entry-detail { 264 | padding: 30px 0; 265 | } 266 | 267 | .entry-detail .signature { 268 | position: relative; 269 | padding: 5px 15px; 270 | margin-bottom: 10px; 271 | display: block; 272 | border-radius: 5px; 273 | background-color: #f8f8f8; 274 | color: #47266E; 275 | border: 1px solid #f0f0f0; 276 | font-family: Consolas, 'Courier New', Courier, Monaco, monospace; 277 | transition: .2s ease-in-out; 278 | } 279 | 280 | .entry-detail:target .signature { 281 | background-color: #D5CAE3; 282 | border: 1px solid #624288; 283 | } 284 | 285 | .entry-detail .signature .method-permalink { 286 | position: absolute; 287 | top: 0; 288 | left: -35px; 289 | padding: 5px 15px; 290 | text-decoration: none; 291 | font-weight: bold; 292 | color: #624288; 293 | opacity: .4; 294 | transition: opacity .2s; 295 | } 296 | 297 | .entry-detail .signature .method-permalink:hover { 298 | opacity: 1; 299 | } 300 | 301 | .entry-detail:target .signature .method-permalink { 302 | opacity: 1; 303 | } 304 | 305 | .methods-inherited { 306 | padding-right: 10%; 307 | line-height: 1.5em; 308 | } 309 | 310 | .methods-inherited h3 { 311 | margin-bottom: 4px; 312 | } 313 | 314 | .methods-inherited a { 315 | display: inline-block; 316 | text-decoration: none; 317 | color: #47266E; 318 | } 319 | 320 | .methods-inherited a:hover { 321 | text-decoration: underline; 322 | color: #6C518B; 323 | } 324 | 325 | .methods-inherited .tooltip>span { 326 | background: #D5CAE3; 327 | padding: 4px 8px; 328 | border-radius: 3px; 329 | margin: -4px -8px; 330 | } 331 | 332 | .methods-inherited .tooltip * { 333 | color: #47266E; 334 | } 335 | 336 | pre { 337 | padding: 10px 20px; 338 | margin-top: 4px; 339 | border-radius: 3px; 340 | line-height: 1.45; 341 | overflow: auto; 342 | color: #333; 343 | background: #fdfdfd; 344 | font-size: 14px; 345 | border: 1px solid #eee; 346 | } 347 | 348 | code { 349 | font-family: Consolas, 'Courier New', Courier, Monaco, monospace; 350 | } 351 | 352 | span.flag { 353 | padding: 2px 4px 1px; 354 | border-radius: 3px; 355 | margin-right: 3px; 356 | font-size: 11px; 357 | border: 1px solid transparent; 358 | } 359 | 360 | span.flag.orange { 361 | background-color: #EE8737; 362 | color: #FCEBDD; 363 | border-color: #EB7317; 364 | } 365 | 366 | span.flag.yellow { 367 | background-color: #E4B91C; 368 | color: #FCF8E8; 369 | border-color: #B69115; 370 | } 371 | 372 | span.flag.green { 373 | background-color: #469C14; 374 | color: #E2F9D3; 375 | border-color: #34700E; 376 | } 377 | 378 | span.flag.red { 379 | background-color: #BF1919; 380 | color: #F9ECEC; 381 | border-color: #822C2C; 382 | } 383 | 384 | span.flag.purple { 385 | background-color: #2E1052; 386 | color: #ECE1F9; 387 | border-color: #1F0B37; 388 | } 389 | 390 | .tooltip>span { 391 | position: absolute; 392 | opacity: 0; 393 | display: none; 394 | pointer-events: none; 395 | } 396 | 397 | .tooltip:hover>span { 398 | display: inline-block; 399 | opacity: 1; 400 | } 401 | 402 | .c { 403 | color: #969896; 404 | } 405 | 406 | .n { 407 | color: #0086b3; 408 | } 409 | 410 | .t { 411 | color: #0086b3; 412 | } 413 | 414 | .s { 415 | color: #183691; 416 | } 417 | 418 | .i { 419 | color: #7f5030; 420 | } 421 | 422 | .k { 423 | color: #a71d5d; 424 | } 425 | 426 | .o { 427 | color: #a71d5d; 428 | } 429 | 430 | .m { 431 | color: #795da3; 432 | } 433 | -------------------------------------------------------------------------------- /doc/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | README - github.com/TPei/circuit_breaker 9 | 10 | 11 | 12 |
13 | 16 | 17 | 20 | 21 | 29 | 30 |
31 | 32 |
33 |

circuit_breaker Build Status

34 | 35 |

Simple Implementation of the circuit breaker pattern in Crystal.

36 | 37 |

Given a certain error threshold, timeframe and timeout window, a breaker can be used to monitor criticial command executions. Circuit breakers are usually used to prevent unnecessary requests if a server ressource et al becomes unavailable. This protects the server from additional load and allows it to recover and relieves the client from requests that are doomed to fail.

38 | 39 |

Wrap API calls inside a breaker, if the error rate in a given time frame surpasses a certain threshold, all subsequent calls will fail for a given duration.

40 | 41 |

Installation

42 | 43 |

Add to your shard.yml

44 | 45 |
dependencies:
 46 |   circuit_breaker:
 47 |     github: tpei/circuit_breaker
 48 |     branch: master
49 | 50 |

and then install the library into your project with

51 | 52 |
$ crystal deps
53 | 54 |

Usage

55 | 56 |

Create a new breaker:

57 | 58 |
require "circuit_breaker"
 59 | 
 60 | breaker = CircuitBreaker.new(
 61 |   threshold: 5, # % of errors before you want to trip the circuit
 62 |   timewindow: 60, # in s: anything older will be ignored in error_rate
 63 |   reenable_after: 300 # after x seconds, the breaker will allow executions again
 64 | )
65 | 66 |

Then wrap whatever you like:

67 | 68 |
breaker.run do
 69 |   my_rest_call()
 70 | end
71 | 72 |

The Breaker will open and throw an CircuitOpenException for all subsequent calls, once the threshold is reached. You can of course catch these exceptions and do whatever you want :D

73 | 74 |
begin
 75 |   breaker.run do
 76 |     my_rest_call()
 77 |   end
 78 | rescue exc : CircuitOpenException
 79 |   log "happens to the best of us..."
 80 |   42
 81 | end
82 | 83 |

After the given reenable time, the circuit will transition to "half open". This will completely reset the circuit if the next execution succeeds, but reopen the circuit and reset the timer if the next execution fails.

84 | 85 |

If you are feeling really funky, you can also hand in exception classes to monitor. You might want to catch RandomRestError, but not ArgumentError, so do this:

86 | 87 |
breaker = CircuitBreaker.new(
 88 |   threshold: 5,
 89 |   timewindow: 60,
 90 |   reenable_after: 300,
 91 |   handled_errors: [RandomRestError.new]
 92 | )
 93 | 
 94 | breaker.run
 95 |   raise ArgumentError.new("won't count towards the error rate")
 96 | end
97 | 98 |

Of course you can also add custom errors to ignore and count all others:

99 | 100 |
breaker = CircuitBreaker.new(
101 |   threshold: 5,
102 |   timewindow: 60,
103 |   reenable_after: 300,
104 |   ignored_errors: [ArgumentError.new]
105 | )
106 | 
107 | breaker.run
108 |   raise ArgumentError.new("won't count towards the error rate")
109 | end
110 | 111 |

Unfortunately this both won't match against exception subclasses just yet, so at the moment you have to specify the exact class to monitor and can't just use RestException to match every subclass like RestTimeoutException < RestException...

112 | 113 |

Thanks

114 | 115 |

Special thanks goes to Pedro Belo on whose ruby circuit breaker implementation (CB2) this is loosely based.

116 |
117 | 118 | 119 | -------------------------------------------------------------------------------- /doc/js/doc.js: -------------------------------------------------------------------------------- 1 | document.addEventListener('DOMContentLoaded', function() { 2 | var sessionStorage = window.sessionStorage; 3 | if(!sessionStorage) { 4 | sessionStorage = { 5 | setItem: function() {}, 6 | getItem: function() {}, 7 | removeItem: function() {} 8 | }; 9 | } 10 | 11 | var repositoryName = document.getElementById('repository-name').getAttribute('content'); 12 | var typesList = document.getElementById('types-list'); 13 | var searchInput = document.getElementById('search-input'); 14 | var parents = document.querySelectorAll('#types-list li.parent'); 15 | 16 | for(var i = 0; i < parents.length; i++) { 17 | var _parent = parents[i]; 18 | _parent.addEventListener('click', function(e) { 19 | e.stopPropagation(); 20 | 21 | if(e.target.tagName.toLowerCase() == 'li') { 22 | if(e.target.className.match(/open/)) { 23 | sessionStorage.removeItem(e.target.getAttribute('data-id')); 24 | e.target.className = e.target.className.replace(/ +open/g, ''); 25 | } else { 26 | sessionStorage.setItem(e.target.getAttribute('data-id'), '1'); 27 | if(e.target.className.indexOf('open') == -1) { 28 | e.target.className += ' open'; 29 | } 30 | } 31 | } 32 | }); 33 | 34 | if(sessionStorage.getItem(_parent.getAttribute('data-id')) == '1') { 35 | _parent.className += ' open'; 36 | } 37 | }; 38 | 39 | var childMatch = function(type, regexp){ 40 | var types = type.querySelectorAll("ul li"); 41 | for (var j = 0; j < types.length; j ++) { 42 | var t = types[j]; 43 | if(regexp.exec(t.getAttribute('data-name'))){ return true; }; 44 | }; 45 | return false; 46 | }; 47 | 48 | var searchTimeout; 49 | var performSearch = function() { 50 | clearTimeout(searchTimeout); 51 | searchTimeout = setTimeout(function() { 52 | var text = searchInput.value; 53 | var types = document.querySelectorAll('#types-list li'); 54 | var words = text.toLowerCase().split(/\s+/).filter(function(word) { 55 | return word.length > 0; 56 | }); 57 | var regexp = new RegExp(words.join('|')); 58 | 59 | for(var i = 0; i < types.length; i++) { 60 | var type = types[i]; 61 | if(words.length == 0 || regexp.exec(type.getAttribute('data-name')) || childMatch(type, regexp)) { 62 | type.className = type.className.replace(/ +hide/g, ''); 63 | var is_parent = new RegExp("parent").exec(type.className); 64 | var is_not_opened = !(new RegExp("open").exec(type.className)); 65 | if(childMatch(type,regexp) && is_parent && is_not_opened){ 66 | type.className += " open"; 67 | }; 68 | } else { 69 | if(type.className.indexOf('hide') == -1) { 70 | type.className += ' hide'; 71 | }; 72 | }; 73 | if(words.length == 0){ 74 | type.className = type.className.replace(/ +open/g, ''); 75 | }; 76 | } 77 | }, 200); 78 | }; 79 | if (searchInput.value.length > 0) { 80 | performSearch(); 81 | } 82 | searchInput.addEventListener('keyup', performSearch); 83 | searchInput.addEventListener('input', performSearch); 84 | 85 | typesList.onscroll = function() { 86 | var y = typesList.scrollTop; 87 | sessionStorage.setItem(repositoryName + '::types-list:scrollTop', y); 88 | }; 89 | 90 | var initialY = parseInt(sessionStorage.getItem(repositoryName + '::types-list:scrollTop') + "", 10); 91 | if(initialY > 0) { 92 | typesList.scrollTop = initialY; 93 | } 94 | 95 | var scrollToEntryFromLocationHash = function() { 96 | var hash = window.location.hash; 97 | if (hash) { 98 | var targetAnchor = unescape(hash.substr(1)); 99 | var targetEl = document.querySelectorAll('.entry-detail[id="' + targetAnchor + '"]'); 100 | 101 | if (targetEl && targetEl.length > 0) { 102 | targetEl[0].offsetParent.scrollTop = targetEl[0].offsetTop; 103 | } 104 | } 105 | }; 106 | window.addEventListener("hashchange", scrollToEntryFromLocationHash, false); 107 | scrollToEntryFromLocationHash(); 108 | }); 109 | -------------------------------------------------------------------------------- /shard.yml: -------------------------------------------------------------------------------- 1 | name: circuit_breaker 2 | version: 0.0.5 3 | 4 | authors: 5 | - tpei 6 | -------------------------------------------------------------------------------- /spec/circuit_breaker_spec.cr: -------------------------------------------------------------------------------- 1 | require "spec" 2 | require "../src/circuit_breaker.cr" 3 | 4 | describe "CircuitBreaker" do 5 | describe "#run" do 6 | it "returns original block value on success" do 7 | breaker = CircuitBreaker.new(threshold: 20, timewindow: 60, reenable_after: 2) 8 | 9 | breaker.run do 10 | 2 11 | end.should eq 2 12 | end 13 | 14 | it "passes on any raised exceptions" do 15 | breaker = CircuitBreaker.new(threshold: 20, timewindow: 60, reenable_after: 2) 16 | 17 | expect_raises MyError do 18 | breaker.run do 19 | raise MyError.new 20 | end 21 | end 22 | end 23 | 24 | it "throws a CircuitOpenException if the circuit is open" do 25 | breaker = CircuitBreaker.new(threshold: 20, timewindow: 60, reenable_after: 2) 26 | 27 | expect_raises MyError do 28 | breaker.run do 29 | raise MyError.new 30 | end 31 | end 32 | 33 | expect_raises CircuitOpenException do 34 | breaker.run do 35 | 2 36 | end 37 | end 38 | 39 | expect_raises CircuitOpenException do 40 | breaker.run do 41 | raise MyError.new 42 | end 43 | end 44 | end 45 | end 46 | 47 | describe "feature test" do 48 | it "reenables after a given timeframe" do 49 | breaker = CircuitBreaker.new(threshold: 20, timewindow: 60, reenable_after: 2) 50 | 51 | 10.times do 52 | breaker.run do 53 | "swag" 54 | end.should eq "swag" 55 | end 56 | 57 | 3.times do 58 | expect_raises ArgumentError do 59 | breaker.run do 60 | raise ArgumentError.new 61 | end 62 | end 63 | end 64 | 65 | 7.times do 66 | expect_raises CircuitOpenException do 67 | breaker.run do 68 | "swag" 69 | end 70 | end 71 | end 72 | 73 | sleep 2 74 | 75 | breaker.run do 76 | "swag" 77 | end.should eq "swag" 78 | end 79 | 80 | it "goes directly back to open if the first execution after reopening fails" do 81 | breaker = CircuitBreaker.new(threshold: 20, timewindow: 60, reenable_after: 2) 82 | 83 | 10.times do 84 | breaker.run do 85 | "swag" 86 | end.should eq "swag" 87 | end 88 | 89 | 3.times do 90 | expect_raises ArgumentError do 91 | breaker.run do 92 | raise ArgumentError.new 93 | end 94 | end 95 | end 96 | 97 | 7.times do 98 | expect_raises CircuitOpenException do 99 | breaker.run do 100 | "swag" 101 | end 102 | end 103 | end 104 | 105 | sleep 2.1 106 | 107 | expect_raises ArgumentError do 108 | breaker.run do 109 | raise ArgumentError.new 110 | end 111 | end 112 | 113 | expect_raises CircuitOpenException do 114 | breaker.run do 115 | "swag" 116 | end 117 | end 118 | end 119 | 120 | it "errors and executions only count in a given timeframe" do 121 | breaker = CircuitBreaker.new(threshold: 20, timewindow: 2, reenable_after: 60) 122 | 123 | 10.times do 124 | breaker.run do 125 | "swag" 126 | end.should eq "swag" 127 | end 128 | 2.times do 129 | expect_raises ArgumentError do 130 | breaker.run do 131 | raise ArgumentError.new 132 | end 133 | end 134 | end 135 | 136 | sleep 3 137 | 138 | 4.times do 139 | breaker.run do 140 | "swag" 141 | end.should eq "swag" 142 | end 143 | 144 | expect_raises ArgumentError do 145 | breaker.run do 146 | raise ArgumentError.new 147 | end 148 | end 149 | end 150 | 151 | it "if the breaker was given an array of Exception types, only those will be monitored" do 152 | breaker = CircuitBreaker.new(threshold: 20, timewindow: 2, reenable_after: 60, handled_errors: [MyError.new]) 153 | 154 | 10.times do 155 | expect_raises ArgumentError do 156 | breaker.run do 157 | raise ArgumentError.new 158 | end 159 | end 160 | end 161 | 162 | 3.times do 163 | expect_raises MyError do 164 | breaker.run do 165 | raise MyError.new 166 | end 167 | end 168 | end 169 | 170 | expect_raises CircuitOpenException do 171 | breaker.run do 172 | raise MyError.new 173 | end 174 | end 175 | end 176 | 177 | it "if the breaker was given an array of Exception types to ignore, those will not be monitored" do 178 | breaker = CircuitBreaker.new(threshold: 20, timewindow: 2, reenable_after: 60, ignored_errors: [ArgumentError.new]) 179 | 180 | 10.times do 181 | expect_raises ArgumentError do 182 | breaker.run do 183 | raise ArgumentError.new 184 | end 185 | end 186 | end 187 | 188 | 3.times do 189 | expect_raises MyError do 190 | breaker.run do 191 | raise MyError.new 192 | end 193 | end 194 | end 195 | 196 | expect_raises CircuitOpenException do 197 | breaker.run do 198 | raise MyError.new 199 | end 200 | end 201 | end 202 | end 203 | end 204 | 205 | class MyError < Exception 206 | end 207 | -------------------------------------------------------------------------------- /spec/circuit_state_spec.cr: -------------------------------------------------------------------------------- 1 | require "spec" 2 | require "../src/circuit_state.cr" 3 | 4 | describe "CircuitState" do 5 | describe "#trip" do 6 | it "transitions from :closed to :open" do 7 | cs = CircuitState.new 8 | cs.state.should eq :closed 9 | 10 | cs.trip 11 | cs.state.should eq :open 12 | end 13 | end 14 | 15 | describe "#attempt_reset" do 16 | it "transitions from :open to :half_open" do 17 | cs = CircuitState.new 18 | cs.trip 19 | cs.attempt_reset 20 | cs.state.should eq :half_open 21 | end 22 | end 23 | 24 | describe "#reset" do 25 | it "transitions from :half_open to :closed" do 26 | cs = CircuitState.new 27 | cs.state.should eq :closed 28 | 29 | cs.trip 30 | cs.attempt_reset 31 | cs.reset 32 | cs.state.should eq :closed 33 | end 34 | 35 | it "does not transition from :closed" do 36 | cs = CircuitState.new 37 | cs.state.should eq :closed 38 | 39 | expect_raises IllegalStateTransition do 40 | cs.reset 41 | end 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /spec/error_watcher_spec.cr: -------------------------------------------------------------------------------- 1 | require "spec" 2 | require "../src/error_watcher.cr" 3 | 4 | describe "ErrorWatcher" do 5 | describe "#add_failure" do 6 | it "adds a failure timestamp to @failures array" do 7 | end 8 | end 9 | 10 | describe "#error_rate" do 11 | it "calculates error rate correctly and cleans after time" do 12 | watcher = ErrorWatcher.new(Time::Span.new(hours: 0, minutes: 0, seconds: 1)) 13 | watcher.add_failure 14 | watcher.add_execution 15 | watcher.error_rate.should eq 100 16 | sleep 2 17 | watcher.error_rate.should eq 0 18 | end 19 | 20 | it "throws an error if there are more failures than executions" do 21 | watcher = ErrorWatcher.new(Time::Span.new(hours: 0, minutes: 0, seconds: 60)) 22 | watcher.add_failure 23 | expect_raises MoreErrorsThanExecutionsException do 24 | watcher.error_rate 25 | end 26 | end 27 | 28 | it "returns 0 if failures and executions are empty" do 29 | watcher = ErrorWatcher.new(Time::Span.new(hours: 0, minutes: 0, seconds: 60)) 30 | watcher.error_rate.should eq 0 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /src/circuit_breaker.cr: -------------------------------------------------------------------------------- 1 | require "./circuit_state" 2 | require "./error_watcher" 3 | 4 | # Simple Implementation of the circuit breaker pattern in Crystal. 5 | # 6 | # Given a certain error threshold, timeframe and timeout window, a breaker can be used to monitor criticial command executions. Circuit breakers are usually used to prevent unnecessary requests if a server ressource et al becomes unavailable. This protects the server from additional load and allows it to recover and relieves the client from requests that are doomed to fail. 7 | # 8 | # Wrap API calls inside a breaker, if the error rate in a given time frame surpasses a certain threshold, all subsequent calls will fail for a given duration. 9 | class CircuitBreaker 10 | @error_threshold : Int32 11 | @duration : Int32 12 | @reclose_time : Time 13 | 14 | # creates a CircuitBreaker instance with a specified error threshold, timeframe, breaker duration and optionally a number of ignored or handled errors 15 | # 16 | # ``` 17 | # breaker = CircuitBreaker.new( 18 | # threshold: 5, # % of errors before you want to trip the circuit 19 | # timewindow: 60, # in s: anything older will be ignored in error_rate 20 | # reenable_after: 300 # after x seconds, the breaker will allow executions again 21 | # ) 22 | # ``` 23 | def initialize(threshold @error_threshold, timewindow timeframe, reenable_after @duration, handled_errors = [] of Exception, ignored_errors = [] of Exception) 24 | @state = CircuitState.new 25 | @reclose_time = Time.local 26 | @error_watcher = ErrorWatcher.new(Time::Span.new(hours: 0, minutes: 0, seconds: timeframe)) 27 | 28 | # two-step initialization because of known crystal compiler bug 29 | @handled_errors = [] of Exception 30 | @handled_errors += handled_errors 31 | @ignored_errors = [] of Exception 32 | @ignored_errors += ignored_errors 33 | end 34 | 35 | # get's passed a block to watch for errors 36 | # every error thrown inside your block counts towards the error rate 37 | # once the threshold is surpassed, it starts throwing `CircuitOpenException`s 38 | # you can catch these rrors and implement some fallback behaviour 39 | # ``` 40 | # begin 41 | # breaker.run do 42 | # my_rest_call() 43 | # end 44 | # rescue exc : CircuitOpenException 45 | # log "happens to the best of us..." 46 | # 42 47 | # end 48 | # ``` 49 | def run(&block) 50 | # if open and not reclosable -> fail 51 | if open? 52 | raise CircuitOpenException.new("Circuit Breaker Open") 53 | end 54 | 55 | begin 56 | @error_watcher.add_execution 57 | return_value = yield 58 | handle_execution_success 59 | rescue exc 60 | if monitor? exc 61 | handle_execution_error 62 | end 63 | raise exc 64 | end 65 | 66 | return return_value 67 | end 68 | 69 | # --------------------------- 70 | # private methods 71 | # --------------------------- 72 | private def monitor?(exception : Exception) 73 | exception_type = exception.class 74 | errors = @handled_errors.map(&.class) 75 | ignored = @ignored_errors.map(&.class) 76 | (errors.includes?(exception_type) || errors.empty?) && !ignored.includes?(exception_type) 77 | end 78 | 79 | private def handle_execution_error 80 | @error_watcher.add_failure 81 | if error_rate >= @error_threshold || @state.state == :half_open 82 | open_circuit 83 | end 84 | end 85 | 86 | private def handle_execution_success 87 | if @state.state == :half_open 88 | reset 89 | end 90 | end 91 | 92 | private def open? 93 | @state.state == :open && !reclose? && !openable? 94 | end 95 | 96 | private def openable? 97 | if error_rate >= @error_threshold && @state.state != :open 98 | open_circuit 99 | true 100 | else 101 | false 102 | end 103 | end 104 | 105 | private def trip 106 | @state.trip 107 | 108 | @reclose_time = Time.local + Time::Span.new(hours: 0, minutes: 0, seconds: @duration) 109 | end 110 | 111 | private def reset 112 | @state.reset 113 | 114 | @reclose_time = Time.local 115 | @error_watcher.reset 116 | end 117 | 118 | private def error_rate 119 | @error_watcher.error_rate 120 | end 121 | 122 | private def reclose? 123 | if Time.local > @reclose_time 124 | @state.attempt_reset 125 | true 126 | else 127 | false 128 | end 129 | end 130 | 131 | private def open_circuit 132 | @state.trip 133 | @reclose_time = Time.local + Time::Span.new(hours: 0, minutes: 0, seconds: @duration) 134 | end 135 | end 136 | 137 | class CircuitOpenException < Exception 138 | end 139 | -------------------------------------------------------------------------------- /src/circuit_state.cr: -------------------------------------------------------------------------------- 1 | class CircuitState 2 | getter :state 3 | 4 | OPEN = :open 5 | CLOSED = :closed 6 | HALF_OPEN = :half_open 7 | 8 | ALLOWED_TRANSITIONS = { 9 | CLOSED => [OPEN], 10 | OPEN => [HALF_OPEN], 11 | HALF_OPEN => [OPEN, CLOSED] 12 | } 13 | 14 | def initialize 15 | @state = CLOSED 16 | end 17 | 18 | def trip 19 | transition_to OPEN 20 | end 21 | 22 | def attempt_reset 23 | transition_to HALF_OPEN 24 | end 25 | 26 | def reset 27 | transition_to CLOSED 28 | end 29 | 30 | private def transition_to(new_state) 31 | unless ALLOWED_TRANSITIONS[@state].includes? new_state 32 | raise IllegalStateTransition.new("From #{@state} to #{new_state}") 33 | end 34 | @state = new_state 35 | end 36 | end 37 | 38 | class IllegalStateTransition < Exception 39 | end 40 | -------------------------------------------------------------------------------- /src/error_watcher.cr: -------------------------------------------------------------------------------- 1 | class ErrorWatcher 2 | @failures = [] of Time 3 | @executions = [] of Time 4 | @timeframe : Time::Span 5 | 6 | def initialize(@timeframe) 7 | end 8 | 9 | def add_failure 10 | @failures << Time.local 11 | end 12 | 13 | def add_execution 14 | @executions << Time.local 15 | end 16 | 17 | def reset 18 | @failures = [] of Time 19 | @executions = [] of Time 20 | end 21 | 22 | def error_rate : Float64 23 | clean_old_records 24 | 25 | raise MoreErrorsThanExecutionsException.new if failure_count > execution_count 26 | return 0.to_f if @executions.size == 0 27 | 28 | failure_count / execution_count.to_f * 100 29 | end 30 | 31 | # --------------------------- 32 | # private methods 33 | # --------------------------- 34 | private def clean_old_records 35 | clean_old @failures 36 | clean_old @executions 37 | end 38 | 39 | private def clean_old(arr : Array(Time)) 40 | threshold = Time.local - @timeframe 41 | 42 | arr.reject! { |time| time < threshold } 43 | end 44 | 45 | private def failure_count 46 | @failures.size 47 | end 48 | 49 | private def execution_count 50 | @executions.size 51 | end 52 | 53 | end 54 | 55 | class MoreErrorsThanExecutionsException < Exception 56 | end 57 | --------------------------------------------------------------------------------