package backthink import "time" // 本文件是自排闸的**只读快照**出口(design/12 §N C.5/C.2 的读侧,切片 C2)。 // // 为什么需要它:面板此前**看不到**「本窗口还能自排几次」—— 那个数字只出现在 // 被拒绝那一轮的审计文案里(`backthink.self_wake.capped` 的 result),也就是 // 「已经被拒一次」之后才第一次可见。C.5 的闸本来就维护了一份**只为上报**的镜像 // (selfWakeGate.granted),本文件把它接出来,判定仍然只由 limiter 做 // (§C.5:「判定始终由 limiter 做;两者用同一个时间源与同一个窗口」)。 // // 🔴 本文件**不做**任何写入、不碰滑窗件(`session.RateLimiter` 至今没有用量读口, // 属别人的文件面):镜像在 selfWakeGate 里已经有了,读它即可。 // SelfWakeStatus 是自排闸的只读快照:本窗口已用多少、上限多少、窗口多长, // 以及容量 1 的待跑槽是否被占用。 // // 它**不参与任何判定**(判定始终由滑窗与待跑槽做),因此读它永远不改变闸的状态。 type SelfWakeStatus struct { // Running 报告自排是否真的能排:未 Start / 已 Stop ⇒ false(此刻 WakeNow // 会返回 ErrSelfWakeUnavailable)。面板据此显示「未运行」而不是「0 次」。 Running bool // Pending 报告容量 1 的待跑槽是否已占用(有一轮自排排着队还没跑)。 Pending bool // Used 是本窗口已用额度,与收据 `SelfWakeReceipt.Used` **同一口径** // (同一次取数:过期的记录先被丢弃,再计数)。 Used int // Limit 与 WindowSec 是**进程生效**的上限与窗口(已 normalize;`≤0` 的那份 // 配置不会走到这里,见 cfg.go 的 normalize 注释)。 Limit int WindowSec int // DefaultLimit / DefaultWindowSec 是代码默认值;LimitIsDefault / WindowIsDefault // 报告生效值**恰好等于**默认值。 // // ⚠️ 它**不是**「配置文件里没写这个键」的断言(与 config-facts 的 isDefault // 同一条诚实口径:网关不读配置文件,也无从区分「配了 4」与「没配」)。 DefaultLimit int DefaultWindowSec int LimitIsDefault bool WindowIsDefault bool } // SelfWakeStatus 取一张自排闸快照。第二个返回值为 false 表示**闸不在** // (只有零值 / 未经 `New` 构造的 Backthink 会这样)—— 调用方**不得**把零值 // 当成事实(不伪造「0 / 0」)。 func (b *Backthink) SelfWakeStatus() (SelfWakeStatus, bool) { gate, running := b.wakeGate() if gate == nil { return SelfWakeStatus{}, false } gate.mu.Lock() // usedLocked 顺手丢弃过期记录,因此这里读到的与判定用的是同一个口径。 used := gate.usedLocked() pending := gate.pending != nil limit, window := gate.limit, gate.window gate.mu.Unlock() return SelfWakeStatus{ Running: running, Pending: pending, Used: used, Limit: limit, WindowSec: int(window / time.Second), DefaultLimit: defaultSelfWakeLimit, DefaultWindowSec: int(defaultSelfWakeWindow / time.Second), LimitIsDefault: limit == defaultSelfWakeLimit, WindowIsDefault: window == defaultSelfWakeWindow, }, true }