faulty 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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