Files
Agent-System-Backend/internal/runmgr/admission.go
T
weichen adcd8fefd2 Fence U3 account writes and stabilize directory cursors
Why: Account disable could be followed by late Backend business writes, API key creation could implicitly create users, account bootstrap locks could invert, and directory cursors were process-local or leaked out-of-scope changes.

Evidence: Local go test ./... and isolated PostgreSQL package suites passed on the staged candidate; exact committed-head SELF replay and stage 6 human review remain pending.

Impact: Adds final account transactions and external-effect checks across HTTP, Host, Bus and run manager; tightens API key issuance and persists versioned cursor signing. Delegated RDM rename still needs the separately proposed real-operator contract, so A1 and stage 6 are not complete.

Refs: Documents U3 r1, approved A1-A4 amendment fb3a821, pending delegated-projection amendment 5783940, Dev feature/iam-u3-account-directory-recovery.
2026-10-14 07:33:54 +00:00

130 lines
3.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package runmgr
import (
"context"
"errors"
"log"
"sync"
"agent-system-backend/internal/store"
)
// 协作存储 U3 切片 2:执行许可门(R8)。预算经 budgetFn 动态读取(设置
// runtime.execBudgetGlobal / runtime.execBudgetPerUser,默认全零=不限,本地档
// 行为与历史一致)。许可的共享权威在 store.admission_leases(PG 咨询锁下
// 原子清点);本门只负责有界排队(接纳数)与让出/重取(父等待子代理或
// HITL 时让出执行许可、保留会话所有权——避免额度占尽时父子互等)。
// ErrAdmissionQueueFull 待执行队列已满(接纳被拒;HTTP 层映射 429)。
var ErrAdmissionQueueFull = errors.New("runmgr: execution queue full")
// admissionGate 每进程一个;释放时广播唤醒全部等待者(无公平队列,Golang
// cond 语义;公平分配留 U7 压测后按实测加)。
type admissionGate struct {
mu sync.Mutex
broadcast chan struct{} // 释放时 close+重建
waiters int
budgetFn func() store.ExecutionBudget
queueMax int
}
func newAdmissionGate(budgetFn func() store.ExecutionBudget, queueMax int) *admissionGate {
if budgetFn == nil {
budgetFn = func() store.ExecutionBudget { return store.ExecutionBudget{} }
}
if queueMax <= 0 {
queueMax = 200 // 方案容量合同的默认队列上限(待实测调整)
}
return &admissionGate{broadcast: make(chan struct{}), budgetFn: budgetFn, queueMax: queueMax}
}
// budget 加锁读取当前预算(与 SetExecutionBudgetFn 并发安全)。
func (g *admissionGate) budget() store.ExecutionBudget {
if g == nil {
return store.ExecutionBudget{} // 直接构造的 Manager(测试):预算关闭
}
g.mu.Lock()
defer g.mu.Unlock()
return g.budgetFn()
}
// QueueDepth 返回当前排队数(诊断/验收)。
func (g *admissionGate) QueueDepth() int {
if g == nil {
return 0
}
g.mu.Lock()
defer g.mu.Unlock()
return g.waiters
}
// QueueFull 非阻塞探测:排队是否已满(API 边缘在落库前拒绝,429 且不写数据)。
func (g *admissionGate) QueueFull() bool {
budget := g.budget()
if budget.Off() {
return false
}
g.mu.Lock()
defer g.mu.Unlock()
return g.waiters >= g.queueMax
}
// acquire 阻塞获取执行许可(ctx 取消则放弃排队)。预算关闭时零开销直通。
func (g *admissionGate) acquire(ctx context.Context, st *store.Store, runID, owner string, accountGuard ...bool) error {
if g == nil {
return nil
}
budget := g.budget()
if budget.Off() {
return ctx.Err()
}
for {
var err error
if len(accountGuard) > 0 && accountGuard[0] {
allowImplicit := len(accountGuard) > 1 && accountGuard[1]
err = st.TryAcquireExecutionSlotAccountChecked(owner, runID, budget, allowImplicit)
} else {
err = st.TryAcquireExecutionSlot(runID, owner, budget)
}
if err == nil {
return nil
} else if !errors.Is(err, store.ErrBudgetExceeded) {
return err // 存储故障:不放行(fail-closed,宁可拒绝执行)
}
g.mu.Lock()
if g.waiters >= g.queueMax {
g.mu.Unlock()
return ErrAdmissionQueueFull
}
g.waiters++
ch := g.broadcast
g.mu.Unlock()
select {
case <-ch:
g.mu.Lock()
g.waiters--
g.mu.Unlock()
case <-ctx.Done():
g.mu.Lock()
g.waiters--
g.mu.Unlock()
return ctx.Err()
}
}
}
// release 释放执行许可并广播(幂等;预算关闭时零开销)。
func (g *admissionGate) release(st *store.Store, runID string) {
if g == nil || g.budget().Off() {
return
}
if err := st.ReleaseExecutionSlot(runID); err != nil {
// 释放失败只影响账本整洁(下次复活即修正),不中断运行。
log.Printf("[runmgr] admission release %s: %v", runID, err)
}
g.mu.Lock()
close(g.broadcast)
g.broadcast = make(chan struct{})
g.mu.Unlock()
}