gateway_helper.go 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475
  1. package handler
  2. import (
  3. "context"
  4. "encoding/json"
  5. "fmt"
  6. "math/rand/v2"
  7. "net/http"
  8. "strings"
  9. "sync"
  10. "time"
  11. middleware2 "github.com/Wei-Shaw/sub2api/internal/server/middleware"
  12. "github.com/Wei-Shaw/sub2api/internal/service"
  13. "github.com/gin-gonic/gin"
  14. )
  15. const gatewayStreamHeartbeatBytesKey = "gateway_stream_heartbeat_bytes"
  16. func recordGatewayStreamHeartbeat(c *gin.Context, written int) {
  17. if c == nil || written <= 0 {
  18. return
  19. }
  20. total, _ := c.Get(gatewayStreamHeartbeatBytesKey)
  21. bytes, _ := total.(int)
  22. c.Set(gatewayStreamHeartbeatBytesKey, bytes+written)
  23. }
  24. func gatewayStreamHasOnlyHeartbeats(c *gin.Context) bool {
  25. if c == nil || c.Writer == nil {
  26. return false
  27. }
  28. value, ok := c.Get(gatewayStreamHeartbeatBytesKey)
  29. if !ok {
  30. return false
  31. }
  32. heartbeatBytes, _ := value.(int)
  33. return heartbeatBytes > 0 && c.Writer.Size() == heartbeatBytes
  34. }
  35. // claudeCodeValidator is a singleton validator for Claude Code client detection
  36. var claudeCodeValidator = service.NewClaudeCodeValidator()
  37. // SetClaudeCodeClientContext 检查请求是否来自 Claude Code 客户端,并设置到 context 中
  38. // 返回更新后的 context
  39. func SetClaudeCodeClientContext(c *gin.Context, body []byte, parsedReq *service.ParsedRequest) {
  40. if c == nil || c.Request == nil {
  41. return
  42. }
  43. ua := c.GetHeader("User-Agent")
  44. // Fast path:非 Claude CLI UA 直接判定 false,避免热路径二次 JSON 反序列化。
  45. if !claudeCodeValidator.ValidateUserAgent(ua) {
  46. ctx := service.SetClaudeCodeClient(c.Request.Context(), false)
  47. c.Request = c.Request.WithContext(ctx)
  48. return
  49. }
  50. isClaudeCode := false
  51. if !strings.Contains(c.Request.URL.Path, "messages") {
  52. // 与 Validate 行为一致:非 messages 路径 UA 命中即可视为 Claude Code 客户端。
  53. isClaudeCode = true
  54. } else {
  55. // 仅在确认为 Claude CLI 且 messages 路径时再做 body 解析。
  56. bodyMap := claudeCodeBodyMapFromParsedRequest(parsedReq)
  57. if bodyMap == nil && len(body) > 0 {
  58. _ = json.Unmarshal(body, &bodyMap)
  59. }
  60. isClaudeCode = claudeCodeValidator.Validate(c.Request, bodyMap)
  61. }
  62. // 更新 request context
  63. ctx := service.SetClaudeCodeClient(c.Request.Context(), isClaudeCode)
  64. // 仅在确认为 Claude Code 客户端时提取版本号写入 context
  65. if isClaudeCode {
  66. if version := claudeCodeValidator.ExtractVersion(ua); version != "" {
  67. ctx = service.SetClaudeCodeVersion(ctx, version)
  68. }
  69. }
  70. c.Request = c.Request.WithContext(ctx)
  71. }
  72. func claudeCodeBodyMapFromParsedRequest(parsedReq *service.ParsedRequest) map[string]any {
  73. if parsedReq == nil {
  74. return nil
  75. }
  76. bodyMap := map[string]any{
  77. "model": parsedReq.Model,
  78. }
  79. // 探测识别(max_tokens=1)需要看到该字段,复用已解析请求时一并带上。
  80. if parsedReq.MaxTokens > 0 {
  81. bodyMap["max_tokens"] = parsedReq.MaxTokens
  82. }
  83. if parsedReq.HasSystem {
  84. if system, ok := parsedReq.SystemValue(); ok {
  85. bodyMap["system"] = system
  86. } else {
  87. bodyMap["system"] = nil
  88. }
  89. }
  90. if parsedReq.MetadataUserID != "" {
  91. bodyMap["metadata"] = map[string]any{"user_id": parsedReq.MetadataUserID}
  92. }
  93. return bodyMap
  94. }
  95. // 并发槽位等待相关常量
  96. //
  97. // 性能优化说明:
  98. // 原实现使用固定间隔(100ms)轮询并发槽位,存在以下问题:
  99. // 1. 高并发时频繁轮询增加 Redis 压力
  100. // 2. 固定间隔可能导致多个请求同时重试(惊群效应)
  101. //
  102. // 新实现使用指数退避 + 抖动算法:
  103. // 1. 初始退避 100ms,每次乘以 1.5,最大 2s
  104. // 2. 添加 ±20% 的随机抖动,分散重试时间点
  105. // 3. 减少 Redis 压力,避免惊群效应
  106. const (
  107. // maxConcurrencyWait 等待并发槽位的最大时间
  108. maxConcurrencyWait = 30 * time.Second
  109. // defaultPingInterval 流式响应等待时发送 ping 的默认间隔
  110. defaultPingInterval = 10 * time.Second
  111. // initialBackoff 初始退避时间
  112. initialBackoff = 100 * time.Millisecond
  113. // backoffMultiplier 退避时间乘数(指数退避)
  114. backoffMultiplier = 1.5
  115. // maxBackoff 最大退避时间
  116. maxBackoff = 2 * time.Second
  117. )
  118. // SSEPingFormat defines the format of SSE ping events for different platforms
  119. type SSEPingFormat string
  120. const (
  121. // SSEPingFormatClaude is the Claude/Anthropic SSE ping format
  122. SSEPingFormatClaude SSEPingFormat = "data: {\"type\": \"ping\"}\n\n"
  123. // SSEPingFormatNone indicates no ping should be sent (e.g., OpenAI has no ping spec)
  124. SSEPingFormatNone SSEPingFormat = ""
  125. // SSEPingFormatComment is an SSE comment ping for OpenAI/Codex CLI clients
  126. SSEPingFormatComment SSEPingFormat = ":\n\n"
  127. )
  128. // ConcurrencyError represents a concurrency limit error with context
  129. type ConcurrencyError struct {
  130. SlotType string
  131. IsTimeout bool
  132. }
  133. func (e *ConcurrencyError) Error() string {
  134. if e.IsTimeout {
  135. return fmt.Sprintf("timeout waiting for %s concurrency slot", e.SlotType)
  136. }
  137. return fmt.Sprintf("%s concurrency limit reached", e.SlotType)
  138. }
  139. type WaitQueueFullError struct {
  140. SlotType string
  141. }
  142. func (e *WaitQueueFullError) Error() string {
  143. return "Too many pending requests, please retry later"
  144. }
  145. // ConcurrencyHelper provides common concurrency slot management for gateway handlers
  146. type ConcurrencyHelper struct {
  147. concurrencyService *service.ConcurrencyService
  148. pingFormat SSEPingFormat
  149. pingInterval time.Duration
  150. }
  151. // NewConcurrencyHelper creates a new ConcurrencyHelper
  152. func NewConcurrencyHelper(concurrencyService *service.ConcurrencyService, pingFormat SSEPingFormat, pingInterval time.Duration) *ConcurrencyHelper {
  153. if pingInterval <= 0 {
  154. pingInterval = defaultPingInterval
  155. }
  156. return &ConcurrencyHelper{
  157. concurrencyService: concurrencyService,
  158. pingFormat: pingFormat,
  159. pingInterval: pingInterval,
  160. }
  161. }
  162. // wrapReleaseOnDone ensures release runs at most once and still triggers on context cancellation.
  163. // 用于避免客户端断开或上游超时导致的并发槽位泄漏。
  164. // 优化:基于 context.AfterFunc 注册回调,避免每请求额外守护 goroutine。
  165. func wrapReleaseOnDone(ctx context.Context, releaseFunc func()) func() {
  166. if releaseFunc == nil {
  167. return nil
  168. }
  169. var once sync.Once
  170. releaseOnce := func() {
  171. once.Do(releaseFunc)
  172. }
  173. stop := context.AfterFunc(ctx, releaseOnce)
  174. return func() {
  175. _ = stop()
  176. releaseOnce()
  177. }
  178. }
  179. // IncrementWaitCount increments the wait count for a user
  180. func (h *ConcurrencyHelper) IncrementWaitCount(ctx context.Context, userID int64, maxWait int) (bool, error) {
  181. return h.concurrencyService.IncrementWaitCount(ctx, userID, maxWait)
  182. }
  183. // DecrementWaitCount decrements the wait count for a user
  184. func (h *ConcurrencyHelper) DecrementWaitCount(ctx context.Context, userID int64) {
  185. h.concurrencyService.DecrementWaitCount(ctx, userID)
  186. }
  187. // IncrementAccountWaitCount increments the wait count for an account
  188. func (h *ConcurrencyHelper) IncrementAccountWaitCount(ctx context.Context, accountID int64, maxWait int) (bool, error) {
  189. return h.concurrencyService.IncrementAccountWaitCount(ctx, accountID, maxWait)
  190. }
  191. // DecrementAccountWaitCount decrements the wait count for an account
  192. func (h *ConcurrencyHelper) DecrementAccountWaitCount(ctx context.Context, accountID int64) {
  193. h.concurrencyService.DecrementAccountWaitCount(ctx, accountID)
  194. }
  195. // TryAcquireUserSlot 尝试立即获取用户并发槽位。
  196. // 返回值: (releaseFunc, acquired, error)
  197. func (h *ConcurrencyHelper) TryAcquireUserSlot(ctx context.Context, userID int64, maxConcurrency int) (func(), bool, error) {
  198. result, err := h.concurrencyService.AcquireUserSlot(ctx, userID, maxConcurrency)
  199. if err != nil {
  200. return nil, false, err
  201. }
  202. if !result.Acquired {
  203. return nil, false, nil
  204. }
  205. return result.ReleaseFunc, true, nil
  206. }
  207. func (h *ConcurrencyHelper) TryAcquireUserSlotForAPIKey(ctx context.Context, userID int64, maxConcurrency int, apiKeyID int64) (func(), bool, error) {
  208. releaseFunc, acquired, err := h.TryAcquireUserSlot(ctx, userID, maxConcurrency)
  209. if err != nil || !acquired {
  210. return releaseFunc, acquired, err
  211. }
  212. return h.withAPIKeySlot(ctx, apiKeyID, releaseFunc), true, nil
  213. }
  214. // AcquireOpenAIWSIngressLease bounds the whole client WebSocket lifecycle,
  215. // independently from per-turn user and account slots.
  216. func (h *ConcurrencyHelper) AcquireOpenAIWSIngressLease(ctx context.Context, apiKeyID int64, maxConnections int) (*service.OpenAIWSIngressLease, bool, error) {
  217. if h == nil || h.concurrencyService == nil {
  218. return nil, false, fmt.Errorf("concurrency service is unavailable")
  219. }
  220. return h.concurrencyService.AcquireOpenAIWSIngressLease(ctx, apiKeyID, maxConnections)
  221. }
  222. // TryAcquireAccountSlot 尝试立即获取账号并发槽位。
  223. // 返回值: (releaseFunc, acquired, error)
  224. func (h *ConcurrencyHelper) TryAcquireAccountSlot(ctx context.Context, accountID int64, maxConcurrency int) (func(), bool, error) {
  225. result, err := h.concurrencyService.AcquireAccountSlot(ctx, accountID, maxConcurrency)
  226. if err != nil {
  227. return nil, false, err
  228. }
  229. if !result.Acquired {
  230. return nil, false, nil
  231. }
  232. return result.ReleaseFunc, true, nil
  233. }
  234. // AcquireUserSlotWithWait acquires a user concurrency slot, waiting if necessary.
  235. // For streaming requests, sends ping events during the wait.
  236. // streamStarted is updated if streaming response has begun.
  237. func (h *ConcurrencyHelper) AcquireUserSlotWithWait(c *gin.Context, userID int64, maxConcurrency int, isStream bool, streamStarted *bool) (func(), error) {
  238. return h.acquireUserSlotWithWaitTimeout(c, userID, maxConcurrency, maxConcurrencyWait, isStream, streamStarted)
  239. }
  240. func (h *ConcurrencyHelper) acquireUserSlotWithWaitTimeout(c *gin.Context, userID int64, maxConcurrency int, timeout time.Duration, isStream bool, streamStarted *bool) (func(), error) {
  241. ctx := c.Request.Context()
  242. // Try to acquire immediately
  243. releaseFunc, acquired, err := h.TryAcquireUserSlot(ctx, userID, maxConcurrency)
  244. if err != nil {
  245. return nil, err
  246. }
  247. if acquired {
  248. return h.withAPIKeySlotFromGin(c, releaseFunc), nil
  249. }
  250. queueLimit := service.CalculateMaxWait(maxConcurrency) - maxConcurrency
  251. if queueLimit < 1 {
  252. queueLimit = 1
  253. }
  254. canWait, err := h.IncrementWaitCount(ctx, userID, queueLimit)
  255. if err != nil {
  256. return nil, err
  257. }
  258. if !canWait {
  259. return nil, &WaitQueueFullError{SlotType: "user"}
  260. }
  261. defer h.DecrementWaitCount(ctx, userID)
  262. // Need to wait - handle streaming ping if needed
  263. releaseFunc, err = h.waitForSlotWithPingTimeout(c, "user", userID, maxConcurrency, timeout, isStream, streamStarted, false)
  264. if err != nil {
  265. return nil, err
  266. }
  267. return h.withAPIKeySlotFromGin(c, releaseFunc), nil
  268. }
  269. func (h *ConcurrencyHelper) withAPIKeySlotFromGin(c *gin.Context, releaseFunc func()) func() {
  270. if c == nil {
  271. return releaseFunc
  272. }
  273. apiKey, ok := middleware2.GetAPIKeyFromContext(c)
  274. if !ok || apiKey == nil {
  275. return releaseFunc
  276. }
  277. return h.withAPIKeySlot(c.Request.Context(), apiKey.ID, releaseFunc)
  278. }
  279. func (h *ConcurrencyHelper) withAPIKeySlot(ctx context.Context, apiKeyID int64, releaseFunc func()) func() {
  280. if h == nil || h.concurrencyService == nil || apiKeyID <= 0 {
  281. return releaseFunc
  282. }
  283. apiKeyReleaseFunc := h.concurrencyService.TrackAPIKeySlot(ctx, apiKeyID)
  284. return func() {
  285. if releaseFunc != nil {
  286. releaseFunc()
  287. }
  288. if apiKeyReleaseFunc != nil {
  289. apiKeyReleaseFunc()
  290. }
  291. }
  292. }
  293. // AcquireAccountSlotWithWait acquires an account concurrency slot, waiting if necessary.
  294. // For streaming requests, sends ping events during the wait.
  295. // streamStarted is updated if streaming response has begun.
  296. func (h *ConcurrencyHelper) AcquireAccountSlotWithWait(c *gin.Context, accountID int64, maxConcurrency int, isStream bool, streamStarted *bool) (func(), error) {
  297. ctx := c.Request.Context()
  298. // Try to acquire immediately
  299. releaseFunc, acquired, err := h.TryAcquireAccountSlot(ctx, accountID, maxConcurrency)
  300. if err != nil {
  301. return nil, err
  302. }
  303. if acquired {
  304. return releaseFunc, nil
  305. }
  306. // Need to wait - handle streaming ping if needed
  307. return h.waitForSlotWithPing(c, "account", accountID, maxConcurrency, isStream, streamStarted)
  308. }
  309. // waitForSlotWithPing waits for a concurrency slot, sending ping events for streaming requests.
  310. // streamStarted pointer is updated when streaming begins (for proper error handling by caller).
  311. func (h *ConcurrencyHelper) waitForSlotWithPing(c *gin.Context, slotType string, id int64, maxConcurrency int, isStream bool, streamStarted *bool) (func(), error) {
  312. return h.waitForSlotWithPingTimeout(c, slotType, id, maxConcurrency, maxConcurrencyWait, isStream, streamStarted, false)
  313. }
  314. // waitForSlotWithPingTimeout waits for a concurrency slot with a custom timeout.
  315. func (h *ConcurrencyHelper) waitForSlotWithPingTimeout(c *gin.Context, slotType string, id int64, maxConcurrency int, timeout time.Duration, isStream bool, streamStarted *bool, tryImmediate bool) (func(), error) {
  316. ctx, cancel := context.WithTimeout(c.Request.Context(), timeout)
  317. defer cancel()
  318. acquireSlot := func() (*service.AcquireResult, error) {
  319. if slotType == "user" {
  320. return h.concurrencyService.AcquireUserSlot(ctx, id, maxConcurrency)
  321. }
  322. return h.concurrencyService.AcquireAccountSlot(ctx, id, maxConcurrency)
  323. }
  324. if tryImmediate {
  325. result, err := acquireSlot()
  326. if err != nil {
  327. return nil, err
  328. }
  329. if result.Acquired {
  330. return result.ReleaseFunc, nil
  331. }
  332. }
  333. // Determine if ping is needed (streaming + ping format defined)
  334. needPing := isStream && h.pingFormat != ""
  335. var flusher http.Flusher
  336. if needPing {
  337. var ok bool
  338. flusher, ok = c.Writer.(http.Flusher)
  339. if !ok {
  340. return nil, fmt.Errorf("streaming not supported")
  341. }
  342. }
  343. // Only create ping ticker if ping is needed
  344. var pingCh <-chan time.Time
  345. if needPing {
  346. pingTicker := time.NewTicker(h.pingInterval)
  347. defer pingTicker.Stop()
  348. pingCh = pingTicker.C
  349. }
  350. backoff := initialBackoff
  351. timer := time.NewTimer(backoff)
  352. defer timer.Stop()
  353. for {
  354. select {
  355. case <-ctx.Done():
  356. if parentErr := c.Request.Context().Err(); parentErr != nil {
  357. return nil, parentErr
  358. }
  359. return nil, &ConcurrencyError{
  360. SlotType: slotType,
  361. IsTimeout: true,
  362. }
  363. case <-pingCh:
  364. // Send ping to keep connection alive
  365. if !*streamStarted {
  366. c.Header("Content-Type", "text/event-stream")
  367. c.Header("Cache-Control", "no-cache")
  368. c.Header("Connection", "keep-alive")
  369. c.Header("X-Accel-Buffering", "no")
  370. *streamStarted = true
  371. }
  372. written, err := fmt.Fprint(c.Writer, string(h.pingFormat))
  373. if err != nil {
  374. return nil, err
  375. }
  376. recordGatewayStreamHeartbeat(c, written)
  377. flusher.Flush()
  378. case <-timer.C:
  379. // Try to acquire slot
  380. result, err := acquireSlot()
  381. if err != nil {
  382. return nil, err
  383. }
  384. if result.Acquired {
  385. return result.ReleaseFunc, nil
  386. }
  387. backoff = nextBackoff(backoff)
  388. timer.Reset(backoff)
  389. }
  390. }
  391. }
  392. // AcquireAccountSlotWithWaitTimeout acquires an account slot with a custom timeout (keeps SSE ping).
  393. func (h *ConcurrencyHelper) AcquireAccountSlotWithWaitTimeout(c *gin.Context, accountID int64, maxConcurrency int, timeout time.Duration, isStream bool, streamStarted *bool) (func(), error) {
  394. return h.waitForSlotWithPingTimeout(c, "account", accountID, maxConcurrency, timeout, isStream, streamStarted, true)
  395. }
  396. // nextBackoff 计算下一次退避时间
  397. // 性能优化:使用指数退避 + 随机抖动,避免惊群效应
  398. // current: 当前退避时间
  399. // 返回值:下一次退避时间(100ms ~ 2s 之间)
  400. func nextBackoff(current time.Duration) time.Duration {
  401. // 指数退避:当前时间 * 1.5
  402. next := time.Duration(float64(current) * backoffMultiplier)
  403. if next > maxBackoff {
  404. next = maxBackoff
  405. }
  406. // 添加 ±20% 的随机抖动(jitter 范围 0.8 ~ 1.2)
  407. // 抖动可以分散多个请求的重试时间点,避免同时冲击 Redis
  408. jitter := 0.8 + rand.Float64()*0.4
  409. jittered := time.Duration(float64(next) * jitter)
  410. if jittered < initialBackoff {
  411. return initialBackoff
  412. }
  413. if jittered > maxBackoff {
  414. return maxBackoff
  415. }
  416. return jittered
  417. }