faulty 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 67b20b18497c36c64f2a3dc7d16de761af0c22c0a38d93f3101534a5ac85e26d
4
- data.tar.gz: 3d880b9a143939ab8ef4a22393c4bb8fa2b724d0bb02986bf52ac06ac3e5cf0b
3
+ metadata.gz: 55bf688c3615a59f51103633db0902abf3458324945ab855894e5975981868f5
4
+ data.tar.gz: dab412aad317a79364782a85c2a6a184e55fa2214cd966655d74eeab69e248c0
5
5
  SHA512:
6
- metadata.gz: 379c17805a9c647d71fac74e5190b6c077ef55c6d42b797f68dff008e602b005053a86c0c41572c761f32df25436b918e68f61049adf613e3a61a1a779d0364d
7
- data.tar.gz: a7ff7c19ec6759282ec0f17ffe72f2a56aa5a41d83221ca53040f676951f8346fa2fa83fbe20c71a4ef843be9d2dfbbe0d23380d94342173e8bf41632d74ad12
6
+ metadata.gz: 7527a23df0c042ea317e99b8fafdf8c8f96ac5f9f68323ba4c4a2901f8e32dc7ca2b7c62b6b7197e446985eab6901f85dd41c6463407ae3632d86e022626d733
7
+ data.tar.gz: 7e3d48b82905b0ad2303449a532e85736a0681fa4dcb8a5f9ee64e6ffd10c76fdc760ac97aa9dd882f7a019d335440f9d91f0ba1efbb92f978c5e9e43c5b977a
data/.gitignore CHANGED
@@ -2,6 +2,7 @@
2
2
  /Gemfile.lock
3
3
  /vendor/
4
4
  /.ruby-version
5
+ /*.gem
5
6
 
6
7
  /coverage/
7
8
  /doc/
@@ -29,6 +29,9 @@ Layout/LineLength:
29
29
  Layout/MultilineMethodCallIndentation:
30
30
  EnforcedStyle: indented
31
31
 
32
+ Layout/RescueEnsureAlignment:
33
+ Enabled: false
34
+
32
35
  Lint/RaiseException:
33
36
  Enabled: true
34
37
 
@@ -59,6 +62,9 @@ Metrics/BlockLength:
59
62
  Metrics/MethodLength:
60
63
  Max: 30
61
64
 
65
+ Naming/MethodParameterName:
66
+ MinNameLength: 1
67
+
62
68
  Style/Documentation:
63
69
  Enabled: false
64
70
 
@@ -6,6 +6,8 @@ rvm:
6
6
  - 2.5
7
7
  - 2.6
8
8
  - 2.7
9
+ - jruby
10
+ - truffleruby
9
11
 
10
12
  env:
11
13
  global:
@@ -22,8 +24,8 @@ before_script:
22
24
  - ./cc-test-reporter before-build
23
25
 
24
26
  script:
25
- - bin/rubocop
26
- - bin/rspec --format doc
27
+ - bundle exec rubocop
28
+ - bundle exec rspec --format doc
27
29
  - bin/check-version
28
30
 
29
31
  after_script:
@@ -0,0 +1,34 @@
1
+ ## Release v0.2.0
2
+
3
+ * Remove Scopes and replace them with Faulty instances #9
4
+
5
+ ### Breaking Changes
6
+
7
+ * `Faulty::Scope` has been removed. Use `Faulty.new` instead.
8
+ * `Faulty` is now a class, not a module
9
+
10
+ ## Release v0.1.5
11
+
12
+ * Fix redis storage to expire state key when using CAS #8
13
+
14
+ ## Release v0.1.4
15
+
16
+ * Improve spec coverage for supporting classes #6
17
+ * Fix redis bug where concurrent CAS requests could crash #7
18
+
19
+ ## Release v0.1.3
20
+
21
+ * Fix bug where memory storage would delete the newest entries #5
22
+ * Add HoneybadgerListener for error reporting #4
23
+
24
+ ## Release v0.1.2
25
+
26
+ * Fix Storage::FaultTolerantProxy open and reopen methods #2
27
+
28
+ ## Release v0.1.1
29
+
30
+ * Fix a crash when Storage::FaultTolerantProxy created a status stub #1
31
+
32
+ ## Release v0.1.0
33
+
34
+ Initial public release
data/Gemfile CHANGED
@@ -3,3 +3,20 @@
3
3
  source 'https://rubygems.org'
4
4
 
5
5
  gemspec
6
+
7
+ # We add non-essential gems like debugging tools and CI dependencies
8
+ # here. This also allows us to use conditional dependencies that depend on the
9
+ # platform
10
+
11
+ not_jruby = %i[ruby mingw x64_mingw].freeze
12
+
13
+ gem 'activesupport', '>= 4.2'
14
+ gem 'bundler', '>= 1.17', '< 3'
15
+ gem 'byebug', platforms: not_jruby
16
+ gem 'irb', '~> 1.0'
17
+ gem 'redcarpet', '~> 3.5', platforms: not_jruby
18
+ gem 'rspec_junit_formatter', '~> 0.4'
19
+ # For now, code climate doesn't support simplecov 0.18
20
+ # https://github.com/codeclimate/test-reporter/issues/413
21
+ gem 'simplecov', '>= 0.17.1', '< 0.18'
22
+ gem 'yard', '~> 0.9.25', platforms: not_jruby
data/README.md CHANGED
@@ -67,6 +67,51 @@ end
67
67
  For a full list of configuration options, see the
68
68
  [Global Configuration](#global-configuration) section.
69
69
 
70
+ ## What is this for?
71
+
72
+ Circuit breakers are a fault-tolerance tool for creating separation between your
73
+ application and external dependencies. For example, your application may call an
74
+ external API to send a text message:
75
+
76
+ ```ruby
77
+ TextApi.send(message)
78
+ ```
79
+
80
+ In normal operation, this API call is very fast. However what if the texting
81
+ service started hanging? Your application would quickly use up a lot of
82
+ resources waiting for requests to return from the service. You could consider
83
+ adding a timeout to your request:
84
+
85
+ ```ruby
86
+ TextApi.send(message, timeout: 5)
87
+ ```
88
+
89
+ Now your application will terminate requests after 5 seconds, but that could
90
+ still add up to a lot of resources if you call this thousands of times. Circuit
91
+ breakers solve this problem.
92
+
93
+ ```ruby
94
+ Faulty.circuit(:text_api).run do
95
+ TextApi.send(message, timeout: 5)
96
+ end
97
+ ```
98
+
99
+ Now, when the text API hangs, the first few will run and start timing out. This
100
+ will trip the circuit. After the circuit trips
101
+ (see [How it Works](#how-it-works)), calls to the text API will be paused for
102
+ the configured cool down period. Your application resources are not overwhelmed.
103
+
104
+ You are free to implement a fallback or error handling however you wish, for
105
+ example, in this case, you might add the text message to a failure queue:
106
+
107
+ ```ruby
108
+ Faulty.circuit(:text_api).run do
109
+ TextApi.send(message, timeout: 5)
110
+ rescue Faulty::CircuitError => e
111
+ FailureQueue.enqueue(message)
112
+ end
113
+ ```
114
+
70
115
  ## Basic Usage
71
116
 
72
117
  To create a circuit, call `Faulty.circuit`. This can be done as you use the
@@ -137,9 +182,9 @@ Faulty's implementation are:
137
182
  - Event-based monitoring
138
183
 
139
184
  Following the principals of the circuit-breaker pattern, the block given to
140
- `run` or `try_run` will always be executed as long as long as it never raises an
141
- error. If the block _does_ raise an error, then the circuit keeps track of the
142
- number of runs and the failure rate.
185
+ `run` or `try_run` will always be executed as long as it never raises an error.
186
+ If the block _does_ raise an error, then the circuit keeps track of the number
187
+ of runs and the failure rate.
143
188
 
144
189
  Once both thresholds are breached, the circuit is opened. Once open, the
145
190
  circuit starts the cool-down period. Any executions within that cool-down are
@@ -158,11 +203,12 @@ In addition to the classic circuit breaker design, Faulty implements caching
158
203
  that is integrated with the circuit state. See [Caching](#caching) for more
159
204
  detail.
160
205
 
161
- ## Global Configuration
206
+ ## Configuration
162
207
 
163
- `Faulty.init` can set the following global configuration options. This example
164
- illustrates the default values. It is also possible to define multiple
165
- non-global configuration scopes (see [Scopes](#scopes)).
208
+ Faulty can be configured with the following configuration options. This example
209
+ illustrates the default values. In the first example, we configure Faulty
210
+ globally. The second example shows the same configuration using an instance of
211
+ Faulty instead of global configuration.
166
212
 
167
213
  ```ruby
168
214
  Faulty.init do |config|
@@ -188,6 +234,25 @@ Faulty.init do |config|
188
234
  end
189
235
  ```
190
236
 
237
+ Here is the same configuration using an instance of `Faulty`. This is a more
238
+ object-oriented approach.
239
+
240
+ ```ruby
241
+ faulty = Faulty.new do |config|
242
+ config.cache = Faulty::Cache::Default.new
243
+ config.storage = Faulty::Storage::Memory.new
244
+ config.listeners = [Faulty::Events::LogListener.new]
245
+ config.notifier = Faulty::Events::Notifier.new(config.listeners)
246
+ end
247
+ ```
248
+
249
+ Most of the examples in this README use the global Faulty class methods, but
250
+ they work the same way when using an instance. Just substitute your instance
251
+ instead of `Faulty`. There is no preferred way to use Faulty. Choose whichever
252
+ configuration mechanism works best for your application. Also see
253
+ [Multiple Configurations](#multiple-configurations) if your application needs
254
+ to set different options in different scenarios.
255
+
191
256
  For all Faulty APIs that have configuration, you can also pass in an options
192
257
  hash. For example, `Faulty.init` could be called like this:
193
258
 
@@ -199,9 +264,9 @@ Faulty.init(cache: Faulty::Cache::Null.new)
199
264
 
200
265
  A circuit can be created with the following configuration options. Those options
201
266
  are only set once, synchronized across threads, and will persist in-memory until
202
- the process exits. If you're using [scopes](#scopes), the options are retained
203
- within the context of each scope. All options given after the first call to
204
- `Faulty.circuit` (or `Scope.circuit` are ignored.
267
+ the process exits. If you're using [multiple configurations](#multiple-configurations),
268
+ the options are retained within the context of each instance. All options given
269
+ after the first call to `Faulty.circuit` (or `Faulty#circuit`) are ignored.
205
270
 
206
271
  This is because the circuit objects themselves are internally memoized, and are
207
272
  read-only once created.
@@ -262,8 +327,8 @@ Faulty.circuit(:api, cache_expires_in: 1800)
262
327
 
263
328
  Faulty integrates caching into it's circuits in a way that is particularly
264
329
  suited to fault-tolerance. To make use of caching, you must specify the `cache`
265
- configuration option when initializing Faulty or creating a scope. If you're
266
- using Rails, this is automatically set to the Rails cache.
330
+ configuration option when initializing Faulty or creating a new Faulty instance.
331
+ If you're using Rails, this is automatically set to the Rails cache.
267
332
 
268
333
  Once your cache is configured, you can use the `cache` parameter when running
269
334
  a circuit to specify a cache key:
@@ -338,12 +403,17 @@ events. The full list of events is available from `Faulty::Events::EVENTS`.
338
403
  closed. Payload: `circuit`
339
404
  - `circuit_success` - A circuit execution was successful. Payload: `circuit`,
340
405
  `status`
341
- - `storage_failure` - A storage backend raised an error. Payload `circuit`,
342
- `action`, `error`
406
+ - `storage_failure` - A storage backend raised an error. Payload `circuit` (can
407
+ be nil), `action`, `error`
343
408
 
344
409
  By default events are logged using `Faulty::Events::LogListener`, but that can
345
410
  be replaced, or additional listeners can be added.
346
411
 
412
+ ### CallbackListener
413
+
414
+ The callback listener is useful for ad-hoc handling of events. You can specify
415
+ an event handler by calling a method on the callback handler by the same name.
416
+
347
417
  ```ruby
348
418
  Faulty.init do |config|
349
419
  # Replace the default listener with a custom callback listener
@@ -356,6 +426,17 @@ Faulty.init do |config|
356
426
  end
357
427
  ```
358
428
 
429
+ ### Other Built-in Listeners
430
+
431
+ In addition to the log and callback listeners, Faulty intends to implement
432
+ built-in service-specific handlers to make it easy to integrate with monitoring
433
+ and reporting software.
434
+
435
+ - `Faulty::Events::HoneybadgerListener`: Reports circuit and backend errors to
436
+ the Honeybadger error reporting service.
437
+
438
+ ### Custom Listeners
439
+
359
440
  You can implement your own listener by following the documentation in
360
441
  `Faulty::Events::ListenerInterface`. For example:
361
442
 
@@ -405,7 +486,7 @@ Faulty.init do |config|
405
486
  storage.key_prefix = 'faulty'
406
487
 
407
488
  # A string to separate the parts of the redis key
408
- storage.key_separator: ':'
489
+ storage.key_separator = ':'
409
490
 
410
491
  # The maximum number of circuit runs that will be stored
411
492
  storage.max_sample_size = 100
@@ -416,11 +497,11 @@ Faulty.init do |config|
416
497
  end
417
498
  ```
418
499
 
419
- ### Listing Circuits
500
+ ## Listing Circuits
420
501
 
421
502
  For monitoring or debugging, you may need to retrieve a list of all circuit
422
- names. This is possible with `Faulty.list_circuits` (or the equivalent method on
423
- your [scope](#scopes)).
503
+ names. This is possible with `Faulty.list_circuits` (or `Faulty#list_circuits`
504
+ if you're using an instance).
424
505
 
425
506
  You can get a list of all circuit statuses by mapping those names to their
426
507
  status objects. Be careful though, since this could cause performance issues for
@@ -432,73 +513,103 @@ statuses = Faulty.list_circuits.map do |name|
432
513
  end
433
514
  ```
434
515
 
435
- ## Scopes
516
+ ## Locking Circuits
517
+
518
+ It is possible to lock a circuit open or closed. A circuit that is locked open
519
+ will never execute its block, and always raise an `Faulty::OpenCircuitError`.
520
+ This is useful in cases where you need to manually disable a dependency
521
+ entirely. If a cached value is available, that will be returned from the circuit
522
+ until it expires, even outside its refresh period.
523
+
524
+ ```ruby
525
+ Faulty.circuit(:broken_api).lock_open!
526
+ ```
527
+
528
+ A circuit that is locked closed will never trip. This is useful in cases where a
529
+ circuit is continuously tripping incorrectly. If a cached value is available, it
530
+ will have the same behavior as an unlocked circuit.
531
+
532
+ ```ruby
533
+ Faulty.circuit(:false_positive).lock_closed!
534
+ ```
535
+
536
+ To remove a lock of either type:
537
+
538
+ ```ruby
539
+ Faulty.circuit(:fixed).unlock!
540
+ ```
541
+
542
+ Locking or unlocking a circuit has no concurrency guarantees, so it's not
543
+ recommended to lock or unlock circuits from production code. Instead, locks are
544
+ intended as an emergency tool for troubleshooting and debugging.
545
+
546
+ ## Multiple Configurations
436
547
 
437
548
  It is possible to have multiple configurations of Faulty running within the same
438
- process. The most common configuration is to simply use `Faulty.init` to
549
+ process. The most common setup is to simply use `Faulty.init` to
439
550
  configure Faulty globally, however it is possible to have additional
440
- configurations using scopes.
551
+ configurations.
441
552
 
442
- ### The default scope
553
+ ### The default instance
443
554
 
444
- When you call `Faulty.init`, you are actually creating the default scope. You
445
- can access this scope directly by calling `Faulty.default`.
555
+ When you call `Faulty.init`, you are actually creating the default instance of
556
+ `Faulty`. You can access this instance directly by calling `Faulty.default`.
446
557
 
447
558
  ```ruby
448
- # We create the default scope
559
+ # We create the default instance
449
560
  Faulty.init
450
561
 
451
- # Access the default scope
452
- scope = Faulty.default
562
+ # Access the default instance
563
+ faulty = Faulty.default
453
564
 
454
- # Alternatively, access the scope by name
455
- scope = Faulty[:default]
565
+ # Alternatively, access the instance by name
566
+ faulty = Faulty[:default]
456
567
  ```
457
568
 
458
- You can rename the default scope if desired:
569
+ You can rename the default instance if desired:
459
570
 
460
571
  ```ruby
461
572
  Faulty.init(:custom_default)
462
573
 
463
- scope = Faulty.default
464
- scope = Faulty[:custom_default]
574
+ instance = Faulty.default
575
+ instance = Faulty[:custom_default]
465
576
  ```
466
577
 
467
- ### Multiple Scopes
578
+ ### Multiple Instances
468
579
 
469
- If you want multiple scopes, but want global, thread-safe access to
580
+ If you want multiple instance, but want global, thread-safe access to
470
581
  them, you can use `Faulty.register`:
471
582
 
472
583
  ```ruby
473
- api_scope = Faulty::Scope.new do |config|
584
+ api_faulty = Faulty.new do |config|
474
585
  # This accepts the same options as Faulty.init
475
586
  end
476
587
 
477
- Faulty.register(:api, api_scope)
588
+ Faulty.register(:api, api_faulty)
478
589
 
479
- # Now access the scope globally
590
+ # Now access the instance globally
480
591
  Faulty[:api]
481
592
  ```
482
593
 
483
594
  When you call `Faulty.circuit`, that's the same as calling
484
- `Faulty.default.circuit`, so you can apply the same API to any other Faulty
485
- scope:
595
+ `Faulty.default.circuit`, so you can apply the same principal to any other
596
+ registered Faulty instance:
486
597
 
487
598
  ```ruby
488
599
  Faulty[:api].circuit(:api_circuit).run { 'ok' }
489
600
  ```
490
601
 
491
- ### Standalone Scopes
602
+ ### Standalone Instances
492
603
 
493
- If you choose, you can use Faulty scopes without registering them globally. This
494
- could be useful if you prefer dependency injection over global state.
604
+ If you choose, you can use Faulty instances without registering them globally.
605
+ This is more object-oriented and is necessary if you use dependency injection.
495
606
 
496
607
  ```ruby
497
- faulty = Faulty::Scope.new
608
+ faulty = Faulty.new
498
609
  faulty.circuit(:standalone_circuit)
499
610
  ```
500
611
 
501
- Calling `circuit` on the scope still has the same memoization behavior that
612
+ Calling `#circuit` on the instance still has the same memoization behavior that
502
613
  `Faulty.circuit` has, so subsequent calls to the same circuit will return a
503
614
  memoized circuit object.
504
615
 
@@ -545,15 +656,34 @@ would be useful for other users.
545
656
  Faulty has its own opinions about how to implement a circuit breaker in Ruby,
546
657
  but there are and have been many other options:
547
658
 
548
- - [circuitbox](https://github.com/yammer/circuitbox)
549
- - [circuit_breaker-ruby](https://github.com/scripbox/circuit_breaker-ruby)
550
- - [stoplight](https://github.com/orgsync/stoplight) (currently unmaintained)
659
+ ### Currently Active
660
+
661
+ - [semian](https://github.com/Shopify/semian): A resiliency toolkit that
662
+ includes circuit breakers. It uses adapters to auto-wire circuits, and it has
663
+ only host-local storage by design.
664
+ - [circuitbox](https://github.com/yammer/circuitbox): Similar in function to
665
+ Faulty, but with a different API. It uses Moneta to abstract circuit storage
666
+ to allow any key-value store.
667
+
668
+ ### Previous Work
669
+
670
+ - [circuit_breaker-ruby](https://github.com/scripbox/circuit_breaker-ruby) (no
671
+ recent activity)
672
+ - [stoplight](https://github.com/orgsync/stoplight) (unmaintained)
551
673
  - [circuit_breaker](https://github.com/wooga/circuit_breaker) (archived)
552
674
  - [simple_circuit_breaker](https://github.com/soundcloud/simple_circuit_breaker)
553
675
  (unmaintained)
554
676
  - [breaker](https://github.com/ahawkins/breaker) (unmaintained)
555
677
  - [circuit_b](https://github.com/alg/circuit_b) (unmaintained)
556
678
 
679
+ ### Faulty's Unique Features
680
+
681
+ - Simple API but configurable for advanced users
682
+ - Pluggable storage backends (circuitbox also has this)
683
+ - Global, or object-oriented configuration with multiple instances
684
+ - Integrated caching support tailored for fault-tolerance
685
+ - Manually lock circuits open or closed
686
+
557
687
  [api docs]: https://www.rubydoc.info/github/ParentSquare/faulty/master
558
688
  [martin fowler]: https://www.martinfowler.com/bliki/CircuitBreaker.html
559
689
  [hystrix]: https://github.com/Netflix/Hystrix/wiki/How-it-Works