Skip to content

Commit 69ab5b7

Browse files
committed
testing different circuit breaking scenarios
1 parent 550a46f commit 69ab5b7

16 files changed

+1237
-0
lines changed

experiments/Gemfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ source "https://rubygems.org"
44

55
# Graphing library for visualization
66
gem "gruff", "~> 0.23"
7+
gem "concurrent-ruby"
78

89
# Alternative: Use rubyplot for pure Ruby plotting (no ImageMagick dependency)
910
# gem 'rubyplot', '~> 0.1'

experiments/Gemfile.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
256 KB
Loading
190 KB
Loading

experiments/low_error_rate.png

181 KB
Loading
150 KB
Loading

experiments/oscillating_errors.png

248 KB
Loading

experiments/sustained_load.png

377 KB
Loading
208 KB
Loading
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
# frozen_string_literal: true
2+
3+
$LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
4+
5+
require "semian"
6+
require_relative "experimental_resource"
7+
8+
puts "Creating experimental resource with circuit breaker..."
9+
resource = Semian::Experiments::ExperimentalResource.new(
10+
name: "protected_service",
11+
endpoints_count: 50,
12+
min_latency: 0.01,
13+
max_latency: 0.2,
14+
distribution: {
15+
type: :log_normal,
16+
mean: 1,
17+
std_dev: 0.1,
18+
},
19+
error_rate: 0.01, # 1% baseline error rate
20+
timeout: 5, # 5 seconds timeout
21+
semian: {
22+
success_threshold: 2,
23+
error_threshold: 3,
24+
error_threshold_timeout: 20,
25+
error_timeout: 15,
26+
bulkhead: false,
27+
},
28+
)
29+
30+
outcomes = {}
31+
done = false
32+
circuit_opened_at_rate = nil
33+
34+
puts "Starting request thread (50 requests/second)..."
35+
Thread.new do
36+
until done
37+
sleep(0.02) # 50 requests per second
38+
current_sec = outcomes[Time.now.to_i] ||= {
39+
success: 0,
40+
circuit_open: 0,
41+
error: 0,
42+
}
43+
begin
44+
resource.request(rand(resource.endpoints_count))
45+
print "✓"
46+
current_sec[:success] += 1
47+
rescue Semian::Experiments::ExperimentalResource::CircuitOpenError => e
48+
print "⚡"
49+
current_sec[:circuit_open] += 1
50+
rescue Semian::Experiments::ExperimentalResource::RequestError, Semian::Experiments::ExperimentalResource::TimeoutError => e
51+
print "✗"
52+
current_sec[:error] += 1
53+
end
54+
end
55+
end
56+
57+
# Gradual error rate increase: 1% -> 6% in 0.5% increments
58+
error_rates = (1.0..6.0).step(0.5).to_a
59+
phase_duration = 20 # seconds per phase
60+
61+
puts "\n=== Gradual Error Rate Increase Test ==="
62+
puts "Starting at 1%, increasing by 0.5% every #{phase_duration} seconds until 6%"
63+
puts "Total test duration: #{error_rates.length * phase_duration} seconds (#{error_rates.length} phases)\n"
64+
65+
error_rates.each_with_index do |rate, index|
66+
rate_percent = rate / 100.0
67+
puts "\n--- Phase #{index + 1}: Error rate = #{rate}% ---"
68+
resource.set_error_rate(rate_percent)
69+
70+
phase_start = Time.now.to_i
71+
sleep phase_duration
72+
73+
# Check if circuit opened during this phase
74+
if circuit_opened_at_rate.nil?
75+
phase_data = outcomes.select { |time, _| time >= phase_start && time < phase_start + phase_duration }
76+
circuit_opens = phase_data.values.sum { |d| d[:circuit_open] }
77+
if circuit_opens > 0 && circuit_opened_at_rate.nil?
78+
circuit_opened_at_rate = rate
79+
puts "🔴 Circuit breaker opened at #{rate}% error rate!"
80+
end
81+
end
82+
end
83+
84+
done = true
85+
sleep 0.5 # Give the thread time to finish
86+
87+
puts "\n\n=== Test Complete ==="
88+
puts "\nGenerating analysis..."
89+
90+
# Calculate summary statistics
91+
total_success = outcomes.values.sum { |data| data[:success] }
92+
total_circuit_open = outcomes.values.sum { |data| data[:circuit_open] }
93+
total_error = outcomes.values.sum { |data| data[:error] }
94+
total_requests = total_success + total_circuit_open + total_error
95+
96+
puts "\n=== Summary Statistics ==="
97+
puts "Total Requests: #{total_requests}"
98+
puts " Successes: #{total_success} (#{(total_success.to_f / total_requests * 100).round(2)}%)"
99+
puts " Circuit Open: #{total_circuit_open} (#{(total_circuit_open.to_f / total_requests * 100).round(2)}%)"
100+
puts " Errors: #{total_error} (#{(total_error.to_f / total_requests * 100).round(2)}%)"
101+
102+
if circuit_opened_at_rate
103+
puts "\n🔴 Circuit breaker opened at: #{circuit_opened_at_rate}% error rate"
104+
else
105+
puts "\n🟢 Circuit breaker never opened during this test"
106+
end
107+
108+
# Phase-by-phase breakdown
109+
puts "\n=== Phase-by-Phase Breakdown ==="
110+
error_rates.each_with_index do |rate, index|
111+
phase_start_time = outcomes.keys[0] + (index * phase_duration)
112+
phase_data = outcomes.select { |time, _| time >= phase_start_time && time < phase_start_time + phase_duration }
113+
114+
phase_success = phase_data.values.sum { |d| d[:success] }
115+
phase_errors = phase_data.values.sum { |d| d[:error] }
116+
phase_circuit = phase_data.values.sum { |d| d[:circuit_open] }
117+
phase_total = phase_success + phase_errors + phase_circuit
118+
119+
actual_error_pct = phase_total > 0 ? ((phase_errors.to_f / phase_total) * 100).round(2) : 0
120+
121+
status = phase_circuit > 0 ? "⚡ CIRCUIT OPEN" : "✓"
122+
puts "Phase #{index + 1} (#{rate}%): #{status}"
123+
puts " Requests: #{phase_total} | Success: #{phase_success} | Errors: #{phase_errors} (#{actual_error_pct}%) | Circuit: #{phase_circuit}"
124+
end
125+
126+
puts "\nGenerating visualization..."
127+
128+
require "gruff"
129+
130+
graph = Gruff::Line.new(1400)
131+
graph.title = "Circuit Breaker: Gradual Error Increase (1% → 6%)"
132+
graph.x_axis_label = "Time (20-second intervals)"
133+
graph.y_axis_label = "Requests per Interval"
134+
135+
graph.hide_dots = false
136+
graph.line_width = 3
137+
138+
# Aggregate data into 20-second buckets for cleaner visualization
139+
bucketed_data = []
140+
error_rates.each_with_index do |rate, index|
141+
phase_start_time = outcomes.keys[0] + (index * phase_duration)
142+
phase_data = outcomes.select { |time, _| time >= phase_start_time && time < phase_start_time + phase_duration }
143+
144+
bucketed_data << {
145+
success: phase_data.values.sum { |d| d[:success] },
146+
circuit_open: phase_data.values.sum { |d| d[:circuit_open] },
147+
error: phase_data.values.sum { |d| d[:error] }
148+
}
149+
end
150+
151+
# Set x-axis labels to show error rates
152+
graph.labels = error_rates.each_with_index.to_h { |rate, i| [i, "#{rate}%"] }
153+
154+
graph.data("Success", bucketed_data.map { |d| d[:success] })
155+
graph.data("Circuit Open", bucketed_data.map { |d| d[:circuit_open] })
156+
graph.data("Error", bucketed_data.map { |d| d[:error] })
157+
158+
graph.write("gradual_error_increase.png")
159+
160+
puts "Graph saved to gradual_error_increase.png"
161+

0 commit comments

Comments
 (0)