user_msg_queue_service.go 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312
  1. package service
  2. import (
  3. "context"
  4. cryptorand "crypto/rand"
  5. "encoding/hex"
  6. "fmt"
  7. "math"
  8. "math/rand/v2"
  9. "sync"
  10. "time"
  11. "github.com/Wei-Shaw/sub2api/internal/config"
  12. "github.com/Wei-Shaw/sub2api/internal/pkg/logger"
  13. "github.com/tidwall/gjson"
  14. )
  15. // UserMsgQueueCache 用户消息串行队列 Redis 缓存接口
  16. type UserMsgQueueCache interface {
  17. // AcquireLock 尝试获取账号级串行锁
  18. AcquireLock(ctx context.Context, accountID int64, requestID string, lockTtlMs int) (acquired bool, err error)
  19. // ReleaseLock 释放锁并记录完成时间
  20. ReleaseLock(ctx context.Context, accountID int64, requestID string) (released bool, err error)
  21. // GetLastCompletedMs 获取上次完成时间(毫秒时间戳,Redis TIME 源)
  22. GetLastCompletedMs(ctx context.Context, accountID int64) (int64, error)
  23. // GetCurrentTimeMs 获取 Redis 服务器当前时间(毫秒),与 ReleaseLock 记录的时间源一致
  24. GetCurrentTimeMs(ctx context.Context) (int64, error)
  25. // ReconcileExpiredLockCandidates 处理锁索引中的到期候选,按真实 PTTL 清理或刷新索引
  26. ReconcileExpiredLockCandidates(ctx context.Context, maxCount int) (cleaned int, err error)
  27. }
  28. // QueueLockResult 锁获取结果
  29. type QueueLockResult struct {
  30. Acquired bool
  31. RequestID string
  32. }
  33. // UserMessageQueueService 用户消息串行队列服务
  34. // 对真实用户消息实施账号级串行化 + RPM 自适应延迟
  35. type UserMessageQueueService struct {
  36. cache UserMsgQueueCache
  37. rpmCache RPMCache
  38. cfg *config.UserMessageQueueConfig
  39. stopCh chan struct{} // graceful shutdown
  40. stopOnce sync.Once // 确保 Stop() 并发安全
  41. }
  42. // NewUserMessageQueueService 创建用户消息串行队列服务
  43. func NewUserMessageQueueService(cache UserMsgQueueCache, rpmCache RPMCache, cfg *config.UserMessageQueueConfig) *UserMessageQueueService {
  44. return &UserMessageQueueService{
  45. cache: cache,
  46. rpmCache: rpmCache,
  47. cfg: cfg,
  48. stopCh: make(chan struct{}),
  49. }
  50. }
  51. // IsRealUserMessage 检测是否为真实用户消息(非 tool_result)
  52. // 与 claude-relay-service 的检测逻辑一致:
  53. // 1. messages 非空
  54. // 2. 最后一条消息 role == "user"
  55. // 3. 最后一条消息 content(如果是数组)中不含 type:"tool_result" / "tool_use_result"
  56. func IsRealUserMessage(parsed *ParsedRequest) bool {
  57. if parsed == nil {
  58. return false
  59. }
  60. messagesRaw := parsed.MessagesRaw()
  61. if len(messagesRaw) == 0 {
  62. return false
  63. }
  64. messages := gjson.ParseBytes(messagesRaw)
  65. if !messages.IsArray() {
  66. return false
  67. }
  68. lastMsg := gjson.Result{}
  69. messages.ForEach(func(_, msg gjson.Result) bool {
  70. lastMsg = msg
  71. return true
  72. })
  73. if !lastMsg.Exists() || !lastMsg.IsObject() {
  74. return false
  75. }
  76. if lastMsg.Get("role").String() != "user" {
  77. return false
  78. }
  79. content := lastMsg.Get("content")
  80. if !content.Exists() {
  81. return true
  82. }
  83. if !content.IsArray() {
  84. return true
  85. }
  86. isReal := true
  87. content.ForEach(func(_, item gjson.Result) bool {
  88. itemType := item.Get("type").String()
  89. if itemType == "tool_result" || itemType == "tool_use_result" {
  90. isReal = false
  91. return false
  92. }
  93. return true
  94. })
  95. return isReal
  96. }
  97. // TryAcquire 尝试立即获取串行锁
  98. func (s *UserMessageQueueService) TryAcquire(ctx context.Context, accountID int64) (*QueueLockResult, error) {
  99. if s.cache == nil {
  100. return &QueueLockResult{Acquired: true}, nil // fail-open
  101. }
  102. requestID := generateUMQRequestID()
  103. lockTTL := s.cfg.LockTTLMs
  104. if lockTTL <= 0 {
  105. lockTTL = 120000
  106. }
  107. acquired, err := s.cache.AcquireLock(ctx, accountID, requestID, lockTTL)
  108. if err != nil {
  109. logger.LegacyPrintf("service.umq", "AcquireLock failed for account %d: %v", accountID, err)
  110. return &QueueLockResult{Acquired: true}, nil // fail-open
  111. }
  112. return &QueueLockResult{
  113. Acquired: acquired,
  114. RequestID: requestID,
  115. }, nil
  116. }
  117. // Release 释放串行锁
  118. func (s *UserMessageQueueService) Release(ctx context.Context, accountID int64, requestID string) error {
  119. if s.cache == nil || requestID == "" {
  120. return nil
  121. }
  122. released, err := s.cache.ReleaseLock(ctx, accountID, requestID)
  123. if err != nil {
  124. logger.LegacyPrintf("service.umq", "ReleaseLock failed for account %d: %v", accountID, err)
  125. return err
  126. }
  127. if !released {
  128. logger.LegacyPrintf("service.umq", "ReleaseLock no-op for account %d (requestID mismatch or expired)", accountID)
  129. }
  130. return nil
  131. }
  132. // EnforceDelay 根据 RPM 负载执行自适应延迟
  133. // 使用 Redis TIME 确保与 releaseLockScript 记录的时间源一致
  134. func (s *UserMessageQueueService) EnforceDelay(ctx context.Context, accountID int64, baseRPM int) error {
  135. if s.cache == nil {
  136. return nil
  137. }
  138. // 先检查历史记录:没有历史则无需延迟,避免不必要的 RPM 查询
  139. lastMs, err := s.cache.GetLastCompletedMs(ctx, accountID)
  140. if err != nil {
  141. logger.LegacyPrintf("service.umq", "GetLastCompletedMs failed for account %d: %v", accountID, err)
  142. return nil // fail-open
  143. }
  144. if lastMs == 0 {
  145. return nil // 没有历史记录,无需延迟
  146. }
  147. delay := s.CalculateRPMAwareDelay(ctx, accountID, baseRPM)
  148. if delay <= 0 {
  149. return nil
  150. }
  151. // 获取 Redis 当前时间(与 lastMs 同源,避免时钟偏差)
  152. nowMs, err := s.cache.GetCurrentTimeMs(ctx)
  153. if err != nil {
  154. logger.LegacyPrintf("service.umq", "GetCurrentTimeMs failed: %v", err)
  155. return nil // fail-open
  156. }
  157. elapsed := time.Duration(nowMs-lastMs) * time.Millisecond
  158. if elapsed < 0 {
  159. // 时钟异常(Redis 故障转移等),fail-open
  160. return nil
  161. }
  162. remaining := delay - elapsed
  163. if remaining <= 0 {
  164. return nil
  165. }
  166. // 执行延迟
  167. timer := time.NewTimer(remaining)
  168. defer timer.Stop()
  169. select {
  170. case <-ctx.Done():
  171. return ctx.Err()
  172. case <-timer.C:
  173. return nil
  174. }
  175. }
  176. // CalculateRPMAwareDelay 根据当前 RPM 负载计算自适应延迟
  177. // ratio = currentRPM / baseRPM
  178. // ratio < 0.5 → MinDelay
  179. // 0.5 ≤ ratio < 0.8 → 线性插值 MinDelay..MaxDelay
  180. // ratio ≥ 0.8 → MaxDelay
  181. // 返回值包含 ±15% 随机抖动(anti-detection + 避免惊群效应)
  182. func (s *UserMessageQueueService) CalculateRPMAwareDelay(ctx context.Context, accountID int64, baseRPM int) time.Duration {
  183. minDelay := time.Duration(s.cfg.MinDelayMs) * time.Millisecond
  184. maxDelay := time.Duration(s.cfg.MaxDelayMs) * time.Millisecond
  185. if minDelay <= 0 {
  186. minDelay = 200 * time.Millisecond
  187. }
  188. if maxDelay <= 0 {
  189. maxDelay = 2000 * time.Millisecond
  190. }
  191. // 防止配置错误:minDelay > maxDelay 时交换
  192. if minDelay > maxDelay {
  193. minDelay, maxDelay = maxDelay, minDelay
  194. }
  195. var baseDelay time.Duration
  196. if baseRPM <= 0 || s.rpmCache == nil {
  197. baseDelay = minDelay
  198. } else {
  199. currentRPM, err := s.rpmCache.GetRPM(ctx, accountID)
  200. if err != nil {
  201. logger.LegacyPrintf("service.umq", "GetRPM failed for account %d: %v", accountID, err)
  202. baseDelay = minDelay // fail-open
  203. } else {
  204. ratio := float64(currentRPM) / float64(baseRPM)
  205. if ratio < 0.5 {
  206. baseDelay = minDelay
  207. } else if ratio >= 0.8 {
  208. baseDelay = maxDelay
  209. } else {
  210. // 线性插值: 0.5 → minDelay, 0.8 → maxDelay
  211. t := (ratio - 0.5) / 0.3
  212. interpolated := float64(minDelay) + t*(float64(maxDelay)-float64(minDelay))
  213. baseDelay = time.Duration(math.Round(interpolated))
  214. }
  215. }
  216. }
  217. // ±15% 随机抖动
  218. return applyJitter(baseDelay, 0.15)
  219. }
  220. // StartCleanupWorker 启动孤儿锁清理 worker。
  221. // worker 只处理锁索引中的到期候选,真正删除前由 cache 层再次校验锁 PTTL。
  222. func (s *UserMessageQueueService) StartCleanupWorker(interval time.Duration) {
  223. if s == nil || s.cache == nil || interval <= 0 {
  224. return
  225. }
  226. runCleanup := func() {
  227. ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
  228. defer cancel()
  229. // 每轮限制处理数量,避免清理任务在大量过期候选时长时间占用 Redis。
  230. cleaned, err := s.cache.ReconcileExpiredLockCandidates(ctx, 1000)
  231. if err != nil {
  232. logger.LegacyPrintf("service.umq", "Cleanup reconcile failed: %v", err)
  233. return
  234. }
  235. if cleaned > 0 {
  236. logger.LegacyPrintf("service.umq", "Cleanup completed: released %d orphaned locks", cleaned)
  237. }
  238. }
  239. go func() {
  240. ticker := time.NewTicker(interval)
  241. defer ticker.Stop()
  242. for {
  243. select {
  244. case <-s.stopCh:
  245. return
  246. case <-ticker.C:
  247. runCleanup()
  248. }
  249. }
  250. }()
  251. }
  252. // Stop 停止后台 cleanup worker
  253. func (s *UserMessageQueueService) Stop() {
  254. if s != nil && s.stopCh != nil {
  255. s.stopOnce.Do(func() {
  256. close(s.stopCh)
  257. })
  258. }
  259. }
  260. // applyJitter 对延迟值施加 ±jitterPct 的随机抖动
  261. // 使用 math/rand/v2(Go 1.22+ 自动使用 crypto/rand 种子),与 nextBackoff 一致
  262. // 例如 applyJitter(200ms, 0.15) 返回 170ms ~ 230ms
  263. func applyJitter(d time.Duration, jitterPct float64) time.Duration {
  264. if d <= 0 || jitterPct <= 0 {
  265. return d
  266. }
  267. // [-jitterPct, +jitterPct]
  268. jitter := (rand.Float64()*2 - 1) * jitterPct
  269. return time.Duration(float64(d) * (1 + jitter))
  270. }
  271. // generateUMQRequestID 生成唯一请求 ID(与 generateRequestID 一致的 fallback 模式)
  272. func generateUMQRequestID() string {
  273. b := make([]byte, 16)
  274. if _, err := cryptorand.Read(b); err != nil {
  275. return fmt.Sprintf("%x", time.Now().UnixNano())
  276. }
  277. return hex.EncodeToString(b)
  278. }