/// engine_circuit.mbt —— 熔断器三态状态机(R104,Nygard Release It! 2007 / Fowler / Azure / AWS 蒸馏落地)。
/// Closed(正常放行,窗口内失败计数)→ 达阈值 → Open(fail-fast 拒绝,微秒返回不耗资源)→
/// 恢复定时器到期 → Half-Open(放行探测请求)→ 探测成功 Closed / 探测失败回 Open。
/// 编排中间层定位:不真调外部服务,状态推进 + 决策(allow_call 指示调用方可否放行),持久化双后端。
///|
/// 上报一次失败并推进熔断状态机。窗口内失败达阈值 → Open(记 opened_at);Half-Open 探测失败 → 回 Open。
pub fn FistEngine::circuit_fail(
self : FistEngine,
ns? : String = "default",
circuit~ : String,
now_secs~ : Int,
window_secs? : Int = 60,
threshold? : Int = 3,
recovery_secs? : Int = 30,
) -> Json {
let row = self.store.cb_get(ns, circuit)
let (state, failures, w, thr, rec, opened_at, last_fail_ts) = match row {
None => ("closed", 0, window_secs, threshold, recovery_secs, 0, 0)
Some(r) => r
}
let (new_state, new_failures, new_opened) = if state == "open" {
// 已熔断:保持 Open(调用方按 allow_call=false 快速失败)
("open", failures, opened_at)
} else if state == "half_open" {
// Half-Open 探测失败 → 回 Open,重启恢复定时器
("open", thr, now_secs)
} else {
// Closed:窗口内计数
let nf = if now_secs - last_fail_ts > w { 1 } else { failures + 1 }
if nf >= thr {
("open", nf, now_secs)
} else {
("closed", nf, 0)
}
}
ignore(
self.store.cb_save(
ns,
circuit,
new_state,
new_failures,
w,
thr,
rec,
new_opened,
now_secs,
now_secs.to_string(),
),
)
cb_json(circuit, new_state, new_failures, w, thr, rec, new_opened, now_secs)
}
///|
/// 上报一次成功:Closed 复位计数;Half-Open 探测成功 → Closed;Open 保持。
pub fn FistEngine::circuit_succeed(
self : FistEngine,
ns? : String = "default",
circuit~ : String,
now_secs~ : Int,
) -> Json {
let row = self.store.cb_get(ns, circuit)
let (state, failures, w, thr, rec, opened_at, last_fail_ts) = match row {
None => ("closed", 0, 60, 3, 30, 0, 0)
Some(r) => r
}
let (new_state, new_failures, new_opened) = if state == "half_open" {
("closed", 0, 0)
} else if state == "open" {
("open", failures, opened_at)
} else {
("closed", 0, 0)
}
ignore(
self.store.cb_save(
ns,
circuit,
new_state,
new_failures,
w,
thr,
rec,
new_opened,
now_secs,
now_secs.to_string(),
),
)
cb_json(circuit, new_state, new_failures, w, thr, rec, new_opened, now_secs)
}
///|
/// 查询熔断状态(含 Half-Open 到期判定:Open 且 elapsed≥recovery → 转 Half-Open 放行探测)。
///
/// BUG-34:半开态必须**节流**——旧实现 `allow = state != "open"` 让恢复窗内所有调用全部放行,
/// 半开退化成"换个名字的 closed",正是要防的下游踩踏(工具描述一直承诺"放行探测请求,其余仍快速失败",
/// 承诺与实现相反)。
/// 配额计数复用已持久化的 `failures` 列(半开态下它的含义是"本恢复窗已放行探测数"):
/// 不扩 cb 表列 ⇒ 不动 native/js 双后端与既有迁移,也就不必把"跨进程计数"做成分布式预算
/// (上一轮 NOT-FIXED 的理由 ① 因此不成立)。
/// 探测丢失兜底:拿到配额却既不 succeed 也不 fail 时,超过一个 recovery 窗就把配额重置——
/// 否则一次被吞掉的探测会把半开态永久锁在快速失败(那是把旧 bug 换成新 bug)。
pub fn FistEngine::circuit_status(
self : FistEngine,
ns? : String = "default",
circuit~ : String,
now_secs~ : Int,
probe_limit? : Int = 1,
) -> Json {
let row = self.store.cb_get(ns, circuit)
let (state, failures, w, thr, rec, opened_at, last_ts) = match row {
None => ("closed", 0, 60, 3, 30, 0, 0)
Some(r) => r
}
// ① Open 到期 → Half-Open,并把"已放行探测数"清零
let (eff_state, eff_failures) = if state == "open" &&
opened_at > 0 &&
now_secs - opened_at >= rec {
ignore(
self.store.cb_save(
ns,
circuit,
"half_open",
0,
w,
thr,
rec,
opened_at,
now_secs,
now_secs.to_string(),
),
)
("half_open", 0)
} else {
(state, failures)
}
if eff_state != "half_open" {
cb_json(circuit, eff_state, eff_failures, w, thr, rec, opened_at, now_secs)
} else {
// ② 探测丢失(一个恢复窗内没有任何上报)⇒ 配额重置
let stale = rec > 0 && last_ts > 0 && now_secs - last_ts >= rec
let granted = if stale { 0 } else { eff_failures }
// probe_limit<=0 = 运维侧显式关掉节流(回到"半开全放行"的旧行为,要在返回体里看得见)
if probe_limit <= 0 || granted < probe_limit {
ignore(
self.store.cb_save(
ns,
circuit,
"half_open",
granted + 1,
w,
thr,
rec,
opened_at,
now_secs,
now_secs.to_string(),
),
)
cb_json(
circuit,
"half_open",
granted + 1,
w,
thr,
rec,
opened_at,
now_secs,
probe_limit~,
)
} else {
// 配额用尽:仍半开、仍记着 opened_at(等探测结果或下一个恢复窗),但对调用方快速失败
cb_json(
circuit,
"half_open",
granted,
w,
thr,
rec,
opened_at,
now_secs,
probe_limit~,
deny=true,
)
}
}
}
///|
fn cb_json(
circuit : String,
state : String,
failures : Int,
w : Int,
thr : Int,
rec : Int,
opened_at : Int,
now_secs : Int,
probe_limit? : Int = 1,
deny? : Bool = false,
) -> Json {
let allow = state != "open" && not(deny)
// 半开态下 failures 复用为"已放行探测数",另给显式字段,免得读侧按失败数解释
let probes = if state == "half_open" { failures } else { 0 }
let m = Map::new()
m.set("circuit", Json::string(circuit))
m.set("state", Json::string(state))
m.set("failures", Json::number(failures.to_double()))
m.set("window_secs", Json::number(w.to_double()))
m.set("threshold", Json::number(thr.to_double()))
m.set("recovery_secs", Json::number(rec.to_double()))
m.set("opened_at", Json::number(opened_at.to_double()))
m.set("allow_call", Json::boolean(allow))
m.set("probes_granted", Json::number(probes.to_double()))
m.set("probe_limit", Json::number(probe_limit.to_double()))
m.set(
"note",
Json::string(
"熔断器三态(Nygard Release It! 2007 / Fowler / Azure 蒸馏):Closed 放行 / Open fail-fast 拒绝 / Half-Open 只放行 probe_limit 次探测(默认 1),探测成功回 Closed、失败回 Open;半开态下 failures=probes_granted(已放行探测数,不是失败数),配额用尽即 allow_call=false,超过一个 recovery 窗无上报则重置。",
),
)
Json::object(m)
}