ruby
70 lines · 9 steps
How a circuit breaker guards failing calls
A thread-safe state machine that stops hammering a failing dependency and probes for recovery.
Explained by
highlit
1class CircuitBreaker
2 class OpenCircuitError < StandardError; end
3
4 def initialize(failure_threshold: 5, reset_timeout: 30, half_open_max: 1)
5 @failure_threshold = failure_threshold
6 @reset_timeout = reset_timeout
7 @half_open_max = half_open_max
8 @state = :closed
9 @failures = 0
10 @opened_at = nil
11 @half_open_calls = 0
12 @mutex = Mutex.new
13 end
14
15 def call
16 @mutex.synchronize { transition_from_open if open? && reset_timeout_elapsed? }
17
18 raise OpenCircuitError, "circuit is open" if reject_call?
19
20 begin
21 result = yield
22 record_success
23 result
24 rescue StandardError => e
25 record_failure
26 raise e
27 end
28 end
29
30 private
31
32 def open?
33 @state == :open
34 end
35
36 def reject_call?
37 @mutex.synchronize do
38 return true if open?
39 @half_open_calls += 1 if @state == :half_open
40 @state == :half_open && @half_open_calls > @half_open_max
41 end
42 end
43
44 def reset_timeout_elapsed?
45 @opened_at && (Time.now - @opened_at) >= @reset_timeout
46 end
47
48 def transition_from_open
49 @state = :half_open
50 @half_open_calls = 0
51 end
52
53 def record_success
54 @mutex.synchronize do
55 @failures = 0
56 @state = :closed
57 @opened_at = nil
58 end
59 end
60
61 def record_failure
62 @mutex.synchronize do
63 @failures += 1
64 if @state == :half_open || @failures >= @failure_threshold
65 @state = :open
66 @opened_at = Time.now
67 end
68 end
69 end
70end
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1A circuit breaker trades a fast failure for a slow one, protecting a struggling dependency from being overwhelmed.
- 2The half-open state acts as a controlled probe, letting a limited number of trial calls decide whether the circuit reopens or fully closes.
- 3Guarding every shared-state mutation with a mutex keeps the breaker correct when many threads call it at once.
Related explainers
ruby
require "csv" class CsvExporter def initialize(records, columns: nil)
Turning records into CSV in Ruby
csv
data-export
serialization
Intermediate
7 steps
javascript
async function uploadInBatches(records, uploadFn, { batchSize = 100, concurrency = 3 } = {}) { const batches = []; for (let i = 0; i < records.length; i += batchSize) { batches.push(records.slice(i, i + batchSize));
Uploading records with bounded concurrency
concurrency
worker-pool
async-await
Advanced
8 steps
ruby
class ProjectsController < ApplicationController before_action :set_project, only: %i[show update destroy] def create
Scoping a Rails API controller to Current.account
multitenancy
strong-parameters
nested-attributes
Intermediate
7 steps
go
package middleware import ( "crypto/sha256"
Deduplicating concurrent requests in Gin
singleflight
request-coalescing
middleware
Advanced
8 steps
ruby
class SearchController < ApplicationController def index @query = params[:q].to_s.strip end
Live search suggestions in a Rails controller
controllers
sql-injection
query-building
Intermediate
6 steps
java
@Configuration @EnableBatchProcessing public class CustomerImportJobConfig {
How a chunk-based CSV import job works in Spring
batch-processing
etl
csv-parsing
Intermediate
9 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/how-a-circuit-breaker-guards-failing-calls-explained-ruby-b14c/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.