///|
pub(all) struct CircuitBreakerConfig {
failure_threshold : Int
reset_timeout : Int
half_open_successes : Int
}
///|
pub(all) struct CircuitBreaker {
name : String
config : CircuitBreakerConfig
mut state : String
mut failures : Int
mut opened_tick : Int
mut trial_successes : Int
}
///|
pub(all) struct CircuitBreakerResult {
state : String
allowed : Int
rejected : Int
successes : Int
failures : Int
final_tick : Int
digest : UInt64
}
///|
pub fn circuit_breaker_config(
failure_threshold? : Int = 3,
reset_timeout? : Int = 5,
half_open_successes? : Int = 2,
) -> CircuitBreakerConfig {
{
failure_threshold: if failure_threshold < 1 {
1
} else {
failure_threshold
},
reset_timeout: if reset_timeout < 1 {
1
} else {
reset_timeout
},
half_open_successes: if half_open_successes < 1 {
1
} else {
half_open_successes
},
}
}
///|
pub fn CircuitBreaker::new(
name : String,
config? : CircuitBreakerConfig = circuit_breaker_config(),
) -> CircuitBreaker {
{
name,
config,
state: "closed",
failures: 0,
opened_tick: -1,
trial_successes: 0,
}
}
///|
pub fn CircuitBreaker::state(self : CircuitBreaker) -> String {
self.state
}
///|
pub fn CircuitBreaker::allow(self : CircuitBreaker, sim : Sim) -> Bool {
if self.state == "open" &&
sim.time() - self.opened_tick >= self.config.reset_timeout {
self.state = "half_open"
self.trial_successes = 0
sim.record(0, "circuit.transition", self.name + ":open->half_open")
}
if self.state == "open" {
sim.inc_counter("circuit_rejected")
sim.record(0, "circuit.reject", self.name)
false
} else {
sim.inc_counter("circuit_allowed")
true
}
}
///|
pub fn CircuitBreaker::record_success(self : CircuitBreaker, sim : Sim) -> Unit {
sim.inc_counter("circuit_success")
if self.state == "half_open" {
self.trial_successes += 1
if self.trial_successes >= self.config.half_open_successes {
self.state = "closed"
self.failures = 0
self.opened_tick = -1
sim.record(0, "circuit.transition", self.name + ":half_open->closed")
}
} else {
self.failures = 0
}
}
///|
pub fn CircuitBreaker::record_failure(self : CircuitBreaker, sim : Sim) -> Unit {
sim.inc_counter("circuit_failure")
self.failures += 1
if self.state == "half_open" {
self.open(sim)
} else if self.failures >= self.config.failure_threshold {
self.open(sim)
}
}
///|
fn CircuitBreaker::open(self : CircuitBreaker, sim : Sim) -> Unit {
self.state = "open"
self.opened_tick = sim.time()
self.trial_successes = 0
sim.record(0, "circuit.transition", self.name + ":open")
}
///|
pub fn run_circuit_breaker_model(
seed? : UInt64 = 606UL,
calls? : Int = 12,
) -> CircuitBreakerResult {
let sim = Sim::new(seed~)
let breaker = CircuitBreaker::new("service")
for call in 0.. TokenBucket {
let cap = if capacity < 1 { 1 } else { capacity }
{
name,
capacity: cap,
refill: if refill < 1 {
1
} else {
refill
},
interval: if interval < 1 {
1
} else {
interval
},
tokens: cap,
last_refill_tick: 0,
}
}
///|
pub fn TokenBucket::allow(
self : TokenBucket,
sim : Sim,
cost? : Int = 1,
) -> Bool {
self.refill_to(sim.time())
let normalized_cost = if cost < 1 { 1 } else { cost }
if self.tokens >= normalized_cost {
self.tokens -= normalized_cost
sim.inc_counter("bucket_allowed")
sim.record(0, "bucket.allow", self.name + ":" + self.tokens.to_string())
true
} else {
sim.inc_counter("bucket_rejected")
sim.record(0, "bucket.reject", self.name + ":" + self.tokens.to_string())
false
}
}
///|
pub fn TokenBucket::refill_to(self : TokenBucket, tick : Int) -> Unit {
if tick <= self.last_refill_tick {
return
}
let elapsed = tick - self.last_refill_tick
let periods = elapsed / self.interval
if periods > 0 {
self.tokens = min_int(self.capacity, self.tokens + periods * self.refill)
self.last_refill_tick += periods * self.interval
}
}
///|
fn min_int(a : Int, b : Int) -> Int {
if a < b {
a
} else {
b
}
}
///|
pub fn run_token_bucket_model(
seed? : UInt64 = 7070UL,
requests? : Int = 14,
) -> TokenBucketResult {
let sim = Sim::new(seed~)
let bucket = TokenBucket::new("api", capacity=4, refill=2, interval=3)
for request in 0.. ModelSummary {
{
name: "circuit_breaker",
digest: self.digest,
final_tick: self.final_tick,
events: self.allowed + self.rejected,
}
}
///|
pub fn TokenBucketResult::summary(self : TokenBucketResult) -> ModelSummary {
{
name: "token_bucket",
digest: self.digest,
final_tick: self.final_tick,
events: self.allowed + self.rejected,
}
}