Files
2026-07-22 16:51:23 +08:00

5.3 KiB

并发规范

Go 后端并发约定。

  • 每个 goroutine 必须有明确退出路径:外部用 context.Context 或 stop channel 通知,内部 select 响应;禁止 fire-and-forget,不允许 goroutine 泄漏。
  • 调用方能等待 goroutine 退出:sync.WaitGroupdone channel。
  • init() 中不启动 goroutine、不做 IO。
  • channel 容量只用 0 或 1,更大缓冲须注释说明理由。
  • 涉及阻塞或远程调用的函数第一个参数是 ctx context.Context,不把 ctx 存进 struct。

模式:服务级后台 goroutine 的落地形态(2026-07 起为既定写法)

面板内「异步旁路」(通知发送 Notifier.SendAsync、日志异步落库 SystemLogService.Record、定期清理 StartCleanup)统一三件套:

s.wg.Add(1)
go func() {
    defer s.wg.Done()
    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) // 自带超时,不复用请求 ctx
    defer cancel()
    if err := s.doWork(ctx); err != nil {
        log.Printf("xxx: %v", err) // 旁路失败只记日志,绝不影响主流程
    }
}()
  • 服务暴露 Wait()(内部 wg.Wait());常驻循环(如清理 ticker)额外接收 ctx,select 响应退出。
  • cmd/server 装配的 defer 顺序必须是「先停生产者、再等消费者」(LIFO):defer notifier.Wait() / defer systemLogs.Wait() 写在 defer tasks.Stop() / defer stopCleanup() 之前
  • 陷阱:cron.Stop() 返回的 context 要等(<-s.cron.Stop().Done()),否则仍在执行的任务尚未 wg.AddWait() 会提前返回,goroutine 泄漏且违反 WaitGroup 的 Add/Wait happens-before 约束。

HTTP 处理器不得同步执行长任务

问题:POST /tasks/:id/run 曾在处理器内同步跑完整个任务(AI 探测挂上模型验证后单次 24~90s),前端点击后长时间零反馈;且执行入口无并发防护,双击/与 cron 重叠会真跑两次。

约定(task.go TriggerTask 为范例):

  • 可能超过 1~2s 的执行,API 只做「异步触发」:校验存在 → 抢占执行权 → 挂 WaitGroup 的 goroutine 执行 → 立即 202;结果落任务日志/通知,由前端轮询呈现。
  • 在飞防重用 map[uint]bool + 专属 mutex(beginRun/endRun):手动重复触发返回哨兵错误(API 映射 409),cron 重叠触发静默跳过。执行权的获取必须在触发方同步完成,不能移进 goroutine(否则有双触发窗口)。
  • 后台执行 goroutine 一律纳入服务的 WaitGroup,Stop()cron.Stop() 之后追加 runWG.Wait() 收尾。

上游 HTTP 超时:流式/长调用禁用 http.Client.Timeout 总超时

问题(2026-07 responses 直通 60s 断流):http.Client.Timeout 覆盖单次请求全生命周期(发起→响应头→body 读完)。OCI SDK 默认 60s(common/client.go defaultTimeout),对 SSE 流式(读 body 无上限)与 multi-agent/搜索类慢模型(>60s 才回响应头)必然掐断;且超时被 switchable 判为可重试,还会误伤渠道熔断计数。

约定(genai_responses.go 为范例):

  • 非流式长调用:dispatcherWithTimeout 值拷贝 client 换总超时(保留 Transport,代理链路不受影响),预算走设置项(ai_upstream_wait_seconds,缺省 300s)。
  • 流式:总超时必须为 0;等待响应头预算用 callWithHeaderBudget(context.WithCancel + time.AfterFunc(wait, cancel),响应头到达即 timer.Stop()),返回 cancelReadCloser 保证流 Close 时取消派生 ctx。流建立后的生命周期交由请求方 ctx(客户端断开自动取消)。
  • SDK 的 Transport 是 OciHTTPTransportWrapper,不是 *http.Transport,设不了 ResponseHeaderTimeout——用 ctx 定时取消模拟,勿依赖类型断言 Transport。
  • 经租户出口的所有 HTTP 外呼(不只 SDK 调用,含 SAML 元数据抓取这类裸 http.Client)都必须复用该租户代理;代理配置非法时失败关闭报错,不得静默直连暴露服务器 IP(2026-07-22 审查 #7,federation.go metadataHTTPClient)。
  • 自建代理 Transport(proxyhttp.go)必须补阶段超时(Dial 30s / TLS 握手 10s,对齐 SDK 直连模板);http.Transport 零值这些字段=无超时,总超时一旦去掉就会裸奔。

出站连接复用与批量删除(2026-07 删桶超时复盘)

问题:每个 OCI 操作都新建 SDK client,代理路径连带每次 new 一个 http.Transport——连接零复用,每请求付整条 TCP+SOCKS5+TLS 握手(经代理 3+ RTT ≈ 1s+);且对象存储每操作先远程取一次 namespace,单条 PAR 删除被放大到 ~2.3s,几千条串行删除撑爆 30 分钟 purge 窗口。用完即弃的 Transport 未设 IdleConnTimeout(零值=客户端永不关),空闲连接堆到远端 ~65s 超时才断,代理侧稳态挂着几十条连接。

约定:

  • 代理出站 http.Client 一律经 proxyhttp.go 的包级 proxyClients 缓存(按 ProxySpec 值复用),不得在调用点新建 per-request Transport;共享实例只可包装(dispatcher wrap),不得改写其字段。连接池参数集中在 pooledTransport():MaxIdleConnsPerHost 须 ≥ 批量并发数。
  • 租户常量(对象存储 namespace 等)在 RealClientsync.Map 进程内缓存(照 limitDefs/shapes 模式),不逐操作远程取。
  • 批量逐条调用统一走 service 层 forEachConcurrently + bulkDeleteWorkers(16);对应的 SDK 删除请求带 bulkRetryPolicy(429/5xx 退避),并发提速与限流保护成对出现,二者缺一不可。