Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
8fbfeec
use lambda to implement dual circuit breaker
adriangudas Nov 25, 2025
f70a232
fix tests
adriangudas Nov 26, 2025
3840787
class method to allow setting a global selector for adaptive config
adriangudas Dec 1, 2025
4dd3dc2
remove unneeded methods that are no longer used
adriangudas Dec 1, 2025
8461f90
allow adaptive_circuit_breaker_selector to be unset
adriangudas Dec 2, 2025
5a7d114
make dynamic configuration call only on acquire
adriangudas Dec 2, 2025
db496a0
require resource to be provided
adriangudas Dec 2, 2025
3b22d16
fix: avoid an extra lookup to use_adaptive in metrics
adriangudas Dec 2, 2025
90e8144
various fixes
adriangudas Dec 2, 2025
7eaeb97
nit: cleanup
adriangudas Dec 2, 2025
757ec08
fix logging to both circuit breakers at the same time
adriangudas Dec 2, 2025
be42433
use logic from both acquire methods in dual_circuit_breaker
adriangudas Dec 4, 2025
4e34d7b
fix last error tracking test by using properly scoped exceptions vari…
adriangudas Dec 4, 2025
afd0314
fix demos
adriangudas Dec 5, 2025
0b1879d
remove unused attr_reader for name
adriangudas Dec 8, 2025
1fb0caa
nit: typo fix
adriangudas Dec 8, 2025
b2bea2d
nit: remove TODO comment
adriangudas Dec 8, 2025
c12f983
Merge branch 'pid-take-2' into adriangudas/adaptive-semian-feature-fl…
adriangudas Dec 8, 2025
245eaaf
don't return if variables are nil (dangerous)
adriangudas Dec 8, 2025
11ed33a
address PR comments
adriangudas Dec 9, 2025
30921b8
make active_circuit_breaker readable for tests
adriangudas Dec 9, 2025
c1d450e
improve track both breakers test with assert_equal
adriangudas Dec 9, 2025
1cdb33d
replace "legacy" with "classic", notify on change in cb type
kris-gaudel Dec 9, 2025
523cff7
replace legacy with classic
kris-gaudel Dec 9, 2025
6740875
notify on change in cb type, remove verbose comments, and refactor `h…
kris-gaudel Dec 9, 2025
5d5b75e
metrics functions for dcb example
kris-gaudel Dec 9, 2025
d4000b5
have error handling parity between classic and adaptive
AbdulRahmanAlHamali Dec 10, 2025
88d3fb1
fix exceptions parameter passing
kris-gaudel Dec 10, 2025
0d348b1
reference as instance variable
kris-gaudel Dec 10, 2025
9342a19
use acquire directly from circuit breakers
AbdulRahmanAlHamali Dec 10, 2025
8d56a3e
Merge branch 'adriangudas/adaptive-semian-feature-flag-gate' of https…
AbdulRahmanAlHamali Dec 10, 2025
714c105
fix dcb test and remove verbose content
kris-gaudel Dec 10, 2025
0d97b5e
fix double counting of errors
kris-gaudel Dec 10, 2025
22eb598
undo the move of method to public
AbdulRahmanAlHamali Dec 15, 2025
5c9d9d0
make active_breaker_type public
AbdulRahmanAlHamali Dec 16, 2025
f23b7fa
rerun experiments
AbdulRahmanAlHamali Dec 16, 2025
25be568
add back-reference for protected resource object to circuit breaker s…
Aguasvivas22 Dec 16, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Gemfile.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

174 changes: 174 additions & 0 deletions examples/dual_circuit_breaker_demo.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
#!/usr/bin/env ruby
# frozen_string_literal: true

require "bundler/setup"
require "semian"

# Example: Dual Circuit Breaker Demo
# This demonstrates how to use both legacy and adaptive circuit breakers
# simultaneously, switching between them at runtime based on a callable.

# Simulate a feature flag that can be toggled
module ExperimentFlags
@enabled = false

def self.enable_adaptive!
@enabled = true
end

def self.disable_adaptive!
@enabled = false
end

def self.use_adaptive_circuit_breaker?
@enabled
end
end

# Helper function to print state of all Semian objects between each phase
def print_semian_state
puts "\n=== Semian Resources State ===\n"
Semian.resources.values.each do |resource|
puts "Resource: #{resource.name}"

# Bulkhead info
if resource.bulkhead
puts " Bulkhead: tickets=#{resource.tickets}, count=#{resource.count}"
else
puts " Bulkhead: disabled"
end

# Circuit breaker info
cb = resource.circuit_breaker
if cb.nil?
puts " Circuit Breaker: disabled"
elsif cb.is_a?(Semian::DualCircuitBreaker)
puts " Circuit Breaker: DualCircuitBreaker"
metrics = cb.metrics
puts " Active: #{metrics[:active]}"
puts " Classic: state=#{metrics[:classic][:state]}, open=#{metrics[:classic][:open]}, half_open=#{metrics[:classic][:half_open]}"
puts " Adaptive: rejection_rate=#{metrics[:adaptive][:rejection_rate]}, error_rate=#{metrics[:adaptive][:error_rate]}"
elsif cb.is_a?(Semian::AdaptiveCircuitBreaker)
puts " Circuit Breaker: AdaptiveCircuitBreaker"
puts " open=#{cb.open?}, closed=#{cb.closed?}, half_open=#{cb.half_open?}"
else
puts " Circuit Breaker: Legacy"
puts " state=#{cb.state&.value}, open=#{cb.open?}, closed=#{cb.closed?}, half_open=#{cb.half_open?}"
puts " last_error=#{cb.last_error&.class}"
end
puts ""
end
puts "=== END STATE OUTPUT ===\n\n"
end

# Register a resource with dual circuit breaker mode
resource = Semian.register(
:my_service,
# Enable dual circuit breaker mode
dual_circuit_breaker: true,

# Legacy circuit breaker parameters (required)
success_threshold: 2,
error_threshold: 3,
error_timeout: 10,

# Adaptive circuit breaker parameters (optional, has defaults)
seed_error_rate: 0.01,

# Common parameters
tickets: 5,
timeout: 0.5,
exceptions: [RuntimeError],
)

Semian::DualCircuitBreaker.adaptive_circuit_breaker_selector(->(_resource) { ExperimentFlags.use_adaptive_circuit_breaker? })

puts "=== Dual Circuit Breaker Demo ===\n\n"

# Helper to simulate service calls
def simulate_call(success: true)
if success
"Success!"
else
raise "Service error"
end
end

# Test with legacy circuit breaker (use_adaptive returns false)
puts "Phase 1: Using LEGACY circuit breaker (use_adaptive=false)"
puts "The first 3 requests will succeed, the rest will fail."
puts "-" * 50

ExperimentFlags.disable_adaptive!

10.times do |i|
result = Semian[:my_service].acquire do
simulate_call(success: i < 3) # First 3 succeed, rest fail
end
puts " Request #{i + 1}: #{result}"
rescue => e
puts " Request #{i + 1}: Failed - #{e.class.name}: #{e.message}"
end

print_semian_state

# Reset both circuit breakers
puts "\n" + "=" * 50
puts "Resetting circuit breakers..."
resource.circuit_breaker.reset

# Test with adaptive circuit breaker (use_adaptive returns true)
puts "\nPhase 2: Using ADAPTIVE circuit breaker (use_adaptive=true)"
puts "The first 3 requests will succeed, then the rest will be failures."
puts "The adaptive circuit breaker is not expected to open yet."
puts "-" * 50

ExperimentFlags.enable_adaptive!

10.times do |i|
begin
result = Semian[:my_service].acquire do
simulate_call(success: i < 3) # First 3 succeed, rest fail
end
puts " Request #{i + 1}: #{result}"
rescue => e
puts " Request #{i + 1}: Failed - #{e.class.name}: #{e.message}"
end
sleep 0.05 # Small delay to see adaptive behavior
end

print_semian_state

# Demonstrate dynamic switching
puts "\n" + "=" * 50
puts "Phase 3: Dynamic switching between circuit breakers"
puts "-" * 50

5.times do |i|
# Toggle every 2 requests
if i.even?
ExperimentFlags.disable_adaptive!
puts " Switched to LEGACY"
else
ExperimentFlags.enable_adaptive!
puts " Switched to ADAPTIVE"
end

begin
result = Semian[:my_service].acquire do
simulate_call(success: true)
end
puts " Request #{i + 1}: #{result}"
rescue => e
puts " Request #{i + 1}: Failed - #{e.class.name}"
end
end

puts "\n=== Demo Complete ===\n"
puts "Both circuit breakers tracked all requests, but only the active one"
puts "was used for decision-making based on the adaptive_circuit_breaker_selector callable."

print_semian_state

# Cleanup
Semian.destroy(:my_service)
166 changes: 166 additions & 0 deletions examples/net_http/08_dual_circuit_breaker.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
#!/usr/bin/env ruby
# frozen_string_literal: true

require "bundler/setup"
require "semian"
require "semian/net_http"
require "net/http"

# Example: Dual Circuit Breaker with Net::HTTP
# This shows how to run both legacy and adaptive circuit breakers in parallel
# and switch between them at runtime using a callable that determines which to use.

# Simulate a feature flag system (in production, this would be your actual feature flag service)
module ExperimentFlags
@enabled = false

def self.enable_adaptive!
@enabled = true
end

def self.disable_adaptive!
@enabled = false
end

def self.use_adaptive_circuit_breaker?
@enabled
end
end

# Helper function to print state of all Semian objects between each phase
def print_semian_state
puts "\n=== Semian Resources State ===\n"
Semian.resources.values.each do |resource|
puts "Resource: #{resource.name}"

# Bulkhead info
if resource.bulkhead
puts " Bulkhead: tickets=#{resource.tickets}, count=#{resource.count}"
else
puts " Bulkhead: disabled"
end

# Circuit breaker info
cb = resource.circuit_breaker
if cb.nil?
puts " Circuit Breaker: disabled"
elsif cb.is_a?(Semian::DualCircuitBreaker)
puts " Circuit Breaker: DualCircuitBreaker"
metrics = cb.metrics
puts " Active: #{metrics[:active]}"
puts " Classic: state=#{metrics[:classic][:state]}, open=#{metrics[:classic][:open]}, half_open=#{metrics[:classic][:half_open]}"
puts " Adaptive: rejection_rate=#{metrics[:adaptive][:rejection_rate]}, error_rate=#{metrics[:adaptive][:error_rate]}"
elsif cb.is_a?(Semian::AdaptiveCircuitBreaker)
puts " Circuit Breaker: AdaptiveCircuitBreaker"
puts " open=#{cb.open?}, closed=#{cb.closed?}, half_open=#{cb.half_open?}"
else
puts " Circuit Breaker: Legacy"
puts " state=#{cb.state&.value}, open=#{cb.open?}, closed=#{cb.closed?}, half_open=#{cb.half_open?}"
puts " last_error=#{cb.last_error&.class}"
end
puts ""
end
puts "=== END STATE OUTPUT ===\n\n"
end

Semian::DualCircuitBreaker.adaptive_circuit_breaker_selector(->(_resource) { ExperimentFlags.use_adaptive_circuit_breaker? })

# Configure Semian with dual circuit breaker
Semian::NetHTTP.semian_configuration = proc do |host, port|
# Example: only enable for specific host
if host == "shopify-debug.com"
{
# Enable dual circuit breaker mode
dual_circuit_breaker: true,

# Legacy circuit breaker settings (required)
success_threshold: 2,
error_threshold: 3,
error_timeout: 5,

# Adaptive circuit breaker settings (uses defaults if not specified)
seed_error_rate: 0.01,

# Bulkhead settings
tickets: 3,
timeout: 1,

# Exceptions to track
exceptions: [Net::HTTPServerException],
}
end
end

puts "=== Dual Circuit Breaker with Net::HTTP ===\n\n"

# Helper to make HTTP requests
def make_request(uri_str)
uri = URI(uri_str)
Net::HTTP.start(uri.host, uri.port) do |http|
request = Net::HTTP::Get.new(uri)
response = http.request(request)
response.code
end
rescue => e
e.class.name
end

print_semian_state

# Phase 1: Use legacy circuit breaker
puts "Phase 1: Using LEGACY circuit breaker"
puts "-" * 50
ExperimentFlags.disable_adaptive!

3.times do |i|
puts "Request #{i + 1} (using legacy)..."
result = make_request("http://shopify-debug.com/")
puts " Result: #{result}\n\n"
sleep 3
end

print_semian_state

# Phase 2: Switch to adaptive circuit breaker
puts "Phase 2: Switching to ADAPTIVE circuit breaker"
puts "-" * 50
ExperimentFlags.enable_adaptive!

3.times do |i|
puts "Request #{i + 1} (using adaptive)..."
result = make_request("http://shopify-debug.com/")
puts " Result: #{result}\n\n"
sleep 3
end

print_semian_state

# Phase 3: Demonstrate dynamic switching
puts "Phase 3: Dynamic switching during runtime"
puts "-" * 50

5.times do |i|
# Toggle between legacy and adaptive
if i.even?
ExperimentFlags.disable_adaptive!
active = "LEGACY"
else
ExperimentFlags.enable_adaptive!
active = "ADAPTIVE"
end

puts "Request #{i + 1} (using #{active})..."
result = make_request("http://shopify-debug.com/")
puts " Result: #{result}\n\n"
sleep 3
end

print_semian_state

puts "=== Demo Complete ===\n\n"
puts "Key Benefits of Dual Circuit Breaker:"
puts " 1. Both breakers track all requests independently"
puts " 2. Can switch between breakers without losing state"
puts " 3. Can compare behavior of both approaches with same traffic"
puts " 4. Enables gradual rollout with instant rollback capability"
puts " 5. Perfect for A/B testing circuit breaker strategies"
Loading
Loading