concurrency_service_test.go 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618
  1. //go:build unit
  2. package service
  3. import (
  4. "context"
  5. "errors"
  6. "strconv"
  7. "strings"
  8. "sync/atomic"
  9. "testing"
  10. "time"
  11. "github.com/stretchr/testify/require"
  12. )
  13. // stubConcurrencyCacheForTest 用于并发服务单元测试的缓存桩
  14. type stubConcurrencyCacheForTest struct {
  15. acquireResult bool
  16. acquireErr error
  17. releaseErr error
  18. concurrency int
  19. concurrencyErr error
  20. waitAllowed bool
  21. waitErr error
  22. waitCount int
  23. waitCountErr error
  24. loadBatch map[int64]*AccountLoadInfo
  25. loadBatchErr error
  26. usersLoadBatch map[int64]*UserLoadInfo
  27. usersLoadErr error
  28. cleanupErr error
  29. apiKeyTrackErr error
  30. apiKeyReleaseErr error
  31. apiKeyConcurrency map[int64]int
  32. apiKeyConcurrencyErr error
  33. // 记录调用
  34. releasedAccountIDs []int64
  35. releasedRequestIDs []string
  36. loadBatchCalls atomic.Int64
  37. trackedAPIKeyIDs []int64
  38. trackedAPIKeyRequestIDs []string
  39. releasedAPIKeyIDs []int64
  40. releasedAPIKeyRequestIDs []string
  41. }
  42. type ingressLeaseCacheForTest struct {
  43. stubConcurrencyCacheForTest
  44. acquireIngressResult bool
  45. acquireIngressErr error
  46. acquireIngressFn func(context.Context, int64, int, string) (bool, error)
  47. refreshIngressResult bool
  48. refreshIngressErr error
  49. refreshIngressFn func(context.Context, int64, string) (bool, error)
  50. releaseIngressErr error
  51. releaseIngressFn func(context.Context, int64, string) error
  52. acquireIngressCalls int
  53. refreshIngressCalls int
  54. releaseIngressCalls int
  55. }
  56. func (c *ingressLeaseCacheForTest) AcquireOpenAIWSIngressLease(ctx context.Context, apiKeyID int64, maxConnections int, leaseID string) (bool, error) {
  57. c.acquireIngressCalls++
  58. if c.acquireIngressFn != nil {
  59. return c.acquireIngressFn(ctx, apiKeyID, maxConnections, leaseID)
  60. }
  61. return c.acquireIngressResult, c.acquireIngressErr
  62. }
  63. func (c *ingressLeaseCacheForTest) RefreshOpenAIWSIngressLease(ctx context.Context, apiKeyID int64, leaseID string) (bool, error) {
  64. c.refreshIngressCalls++
  65. if c.refreshIngressFn != nil {
  66. return c.refreshIngressFn(ctx, apiKeyID, leaseID)
  67. }
  68. return c.refreshIngressResult, c.refreshIngressErr
  69. }
  70. func (c *ingressLeaseCacheForTest) ReleaseOpenAIWSIngressLease(ctx context.Context, apiKeyID int64, leaseID string) error {
  71. c.releaseIngressCalls++
  72. if c.releaseIngressFn != nil {
  73. return c.releaseIngressFn(ctx, apiKeyID, leaseID)
  74. }
  75. return c.releaseIngressErr
  76. }
  77. var _ ConcurrencyCache = (*stubConcurrencyCacheForTest)(nil)
  78. var _ OpenAIWSIngressLeaseCache = (*ingressLeaseCacheForTest)(nil)
  79. func (c *stubConcurrencyCacheForTest) AcquireAccountSlot(_ context.Context, _ int64, _ int, _ string) (bool, error) {
  80. return c.acquireResult, c.acquireErr
  81. }
  82. func (c *stubConcurrencyCacheForTest) ReleaseAccountSlot(_ context.Context, accountID int64, requestID string) error {
  83. c.releasedAccountIDs = append(c.releasedAccountIDs, accountID)
  84. c.releasedRequestIDs = append(c.releasedRequestIDs, requestID)
  85. return c.releaseErr
  86. }
  87. func (c *stubConcurrencyCacheForTest) GetAccountConcurrency(_ context.Context, _ int64) (int, error) {
  88. return c.concurrency, c.concurrencyErr
  89. }
  90. func (c *stubConcurrencyCacheForTest) GetAccountConcurrencyBatch(_ context.Context, accountIDs []int64) (map[int64]int, error) {
  91. result := make(map[int64]int, len(accountIDs))
  92. for _, accountID := range accountIDs {
  93. if c.concurrencyErr != nil {
  94. return nil, c.concurrencyErr
  95. }
  96. result[accountID] = c.concurrency
  97. }
  98. return result, nil
  99. }
  100. func (c *stubConcurrencyCacheForTest) IncrementAccountWaitCount(_ context.Context, _ int64, _ int) (bool, error) {
  101. return c.waitAllowed, c.waitErr
  102. }
  103. func (c *stubConcurrencyCacheForTest) DecrementAccountWaitCount(_ context.Context, _ int64) error {
  104. return nil
  105. }
  106. func (c *stubConcurrencyCacheForTest) GetAccountWaitingCount(_ context.Context, _ int64) (int, error) {
  107. return c.waitCount, c.waitCountErr
  108. }
  109. func (c *stubConcurrencyCacheForTest) AcquireUserSlot(_ context.Context, _ int64, _ int, _ string) (bool, error) {
  110. return c.acquireResult, c.acquireErr
  111. }
  112. func (c *stubConcurrencyCacheForTest) ReleaseUserSlot(_ context.Context, _ int64, _ string) error {
  113. return c.releaseErr
  114. }
  115. func (c *stubConcurrencyCacheForTest) GetUserConcurrency(_ context.Context, _ int64) (int, error) {
  116. return c.concurrency, c.concurrencyErr
  117. }
  118. func (c *stubConcurrencyCacheForTest) TrackAPIKeySlot(_ context.Context, apiKeyID int64, requestID string) error {
  119. c.trackedAPIKeyIDs = append(c.trackedAPIKeyIDs, apiKeyID)
  120. c.trackedAPIKeyRequestIDs = append(c.trackedAPIKeyRequestIDs, requestID)
  121. return c.apiKeyTrackErr
  122. }
  123. func (c *stubConcurrencyCacheForTest) ReleaseAPIKeySlot(_ context.Context, apiKeyID int64, requestID string) error {
  124. c.releasedAPIKeyIDs = append(c.releasedAPIKeyIDs, apiKeyID)
  125. c.releasedAPIKeyRequestIDs = append(c.releasedAPIKeyRequestIDs, requestID)
  126. return c.apiKeyReleaseErr
  127. }
  128. func (c *stubConcurrencyCacheForTest) GetAPIKeyConcurrencyBatch(_ context.Context, apiKeyIDs []int64) (map[int64]int, error) {
  129. if c.apiKeyConcurrencyErr != nil {
  130. return nil, c.apiKeyConcurrencyErr
  131. }
  132. result := make(map[int64]int, len(apiKeyIDs))
  133. for _, apiKeyID := range apiKeyIDs {
  134. result[apiKeyID] = c.apiKeyConcurrency[apiKeyID]
  135. }
  136. return result, nil
  137. }
  138. func (c *stubConcurrencyCacheForTest) IncrementWaitCount(_ context.Context, _ int64, _ int) (bool, error) {
  139. return c.waitAllowed, c.waitErr
  140. }
  141. func (c *stubConcurrencyCacheForTest) DecrementWaitCount(_ context.Context, _ int64) error {
  142. return nil
  143. }
  144. func (c *stubConcurrencyCacheForTest) GetAccountsLoadBatch(_ context.Context, _ []AccountWithConcurrency) (map[int64]*AccountLoadInfo, error) {
  145. c.loadBatchCalls.Add(1)
  146. return c.loadBatch, c.loadBatchErr
  147. }
  148. func (c *stubConcurrencyCacheForTest) GetUsersLoadBatch(_ context.Context, _ []UserWithConcurrency) (map[int64]*UserLoadInfo, error) {
  149. return c.usersLoadBatch, c.usersLoadErr
  150. }
  151. func (c *stubConcurrencyCacheForTest) CleanupExpiredAccountSlots(_ context.Context, _ int64) error {
  152. return c.cleanupErr
  153. }
  154. func (c *stubConcurrencyCacheForTest) CleanupExpiredAccountSlotKeys(_ context.Context) error {
  155. return c.cleanupErr
  156. }
  157. func (c *stubConcurrencyCacheForTest) CleanupStaleProcessSlots(_ context.Context, _ string) error {
  158. return c.cleanupErr
  159. }
  160. type trackingConcurrencyCache struct {
  161. stubConcurrencyCacheForTest
  162. cleanupPrefix string
  163. }
  164. func (c *trackingConcurrencyCache) CleanupStaleProcessSlots(_ context.Context, prefix string) error {
  165. c.cleanupPrefix = prefix
  166. return c.cleanupErr
  167. }
  168. func TestCleanupStaleProcessSlots_NilCache(t *testing.T) {
  169. svc := &ConcurrencyService{cache: nil}
  170. require.NoError(t, svc.CleanupStaleProcessSlots(context.Background()))
  171. }
  172. func TestCleanupStaleProcessSlots_DelegatesPrefix(t *testing.T) {
  173. cache := &trackingConcurrencyCache{}
  174. svc := NewConcurrencyService(cache)
  175. require.NoError(t, svc.CleanupStaleProcessSlots(context.Background()))
  176. require.Equal(t, RequestIDPrefix(), cache.cleanupPrefix)
  177. }
  178. func TestAcquireAccountSlot_Success(t *testing.T) {
  179. cache := &stubConcurrencyCacheForTest{acquireResult: true}
  180. svc := NewConcurrencyService(cache)
  181. result, err := svc.AcquireAccountSlot(context.Background(), 1, 5)
  182. require.NoError(t, err)
  183. require.True(t, result.Acquired)
  184. require.NotNil(t, result.ReleaseFunc)
  185. }
  186. func TestAcquireAccountSlot_Failure(t *testing.T) {
  187. cache := &stubConcurrencyCacheForTest{acquireResult: false}
  188. svc := NewConcurrencyService(cache)
  189. result, err := svc.AcquireAccountSlot(context.Background(), 1, 5)
  190. require.NoError(t, err)
  191. require.False(t, result.Acquired)
  192. require.Nil(t, result.ReleaseFunc)
  193. }
  194. func TestAcquireAccountSlot_UnlimitedConcurrency(t *testing.T) {
  195. svc := NewConcurrencyService(&stubConcurrencyCacheForTest{})
  196. for _, maxConcurrency := range []int{0, -1} {
  197. result, err := svc.AcquireAccountSlot(context.Background(), 1, maxConcurrency)
  198. require.NoError(t, err)
  199. require.True(t, result.Acquired, "maxConcurrency=%d 应无限制通过", maxConcurrency)
  200. require.NotNil(t, result.ReleaseFunc, "ReleaseFunc 应为 no-op 函数")
  201. }
  202. }
  203. func TestAcquireAccountSlot_CacheError(t *testing.T) {
  204. cache := &stubConcurrencyCacheForTest{acquireErr: errors.New("redis down")}
  205. svc := NewConcurrencyService(cache)
  206. result, err := svc.AcquireAccountSlot(context.Background(), 1, 5)
  207. require.Error(t, err)
  208. require.Nil(t, result)
  209. }
  210. func TestAcquireAccountSlot_ReleaseDecrements(t *testing.T) {
  211. cache := &stubConcurrencyCacheForTest{acquireResult: true}
  212. svc := NewConcurrencyService(cache)
  213. result, err := svc.AcquireAccountSlot(context.Background(), 42, 5)
  214. require.NoError(t, err)
  215. require.True(t, result.Acquired)
  216. // 调用 ReleaseFunc 应释放槽位
  217. result.ReleaseFunc()
  218. require.Len(t, cache.releasedAccountIDs, 1)
  219. require.Equal(t, int64(42), cache.releasedAccountIDs[0])
  220. require.Len(t, cache.releasedRequestIDs, 1)
  221. require.NotEmpty(t, cache.releasedRequestIDs[0], "requestID 不应为空")
  222. }
  223. func TestAcquireUserSlot_IndependentFromAccount(t *testing.T) {
  224. cache := &stubConcurrencyCacheForTest{acquireResult: true}
  225. svc := NewConcurrencyService(cache)
  226. // 用户槽位获取应独立于账户槽位
  227. result, err := svc.AcquireUserSlot(context.Background(), 100, 3)
  228. require.NoError(t, err)
  229. require.True(t, result.Acquired)
  230. require.NotNil(t, result.ReleaseFunc)
  231. }
  232. func TestAcquireUserSlot_UnlimitedConcurrency(t *testing.T) {
  233. svc := NewConcurrencyService(&stubConcurrencyCacheForTest{})
  234. result, err := svc.AcquireUserSlot(context.Background(), 1, 0)
  235. require.NoError(t, err)
  236. require.True(t, result.Acquired)
  237. }
  238. func TestTrackAPIKeySlot_ReleaseDecrements(t *testing.T) {
  239. cache := &stubConcurrencyCacheForTest{}
  240. svc := NewConcurrencyService(cache)
  241. release := svc.TrackAPIKeySlot(context.Background(), 88)
  242. require.NotNil(t, release)
  243. require.Equal(t, []int64{88}, cache.trackedAPIKeyIDs)
  244. require.Len(t, cache.trackedAPIKeyRequestIDs, 1)
  245. require.NotEmpty(t, cache.trackedAPIKeyRequestIDs[0])
  246. release()
  247. require.Equal(t, []int64{88}, cache.releasedAPIKeyIDs)
  248. require.Equal(t, cache.trackedAPIKeyRequestIDs, cache.releasedAPIKeyRequestIDs)
  249. }
  250. func TestTrackAPIKeySlot_FailOpen(t *testing.T) {
  251. cache := &stubConcurrencyCacheForTest{apiKeyTrackErr: errors.New("redis down")}
  252. svc := NewConcurrencyService(cache)
  253. release := svc.TrackAPIKeySlot(context.Background(), 88)
  254. require.NotNil(t, release)
  255. require.Equal(t, []int64{88}, cache.trackedAPIKeyIDs)
  256. require.NotPanics(t, release)
  257. require.Empty(t, cache.releasedAPIKeyIDs)
  258. }
  259. func TestGetAPIKeyConcurrencyBatch_Fallbacks(t *testing.T) {
  260. t.Run("nil cache returns zeroes", func(t *testing.T) {
  261. svc := &ConcurrencyService{cache: nil}
  262. counts, err := svc.GetAPIKeyConcurrencyBatch(context.Background(), []int64{1, 2})
  263. require.NoError(t, err)
  264. require.Equal(t, map[int64]int{1: 0, 2: 0}, counts)
  265. })
  266. t.Run("redis error returns zeroes", func(t *testing.T) {
  267. cache := &stubConcurrencyCacheForTest{apiKeyConcurrencyErr: errors.New("redis down")}
  268. svc := NewConcurrencyService(cache)
  269. counts, err := svc.GetAPIKeyConcurrencyBatch(context.Background(), []int64{1, 2})
  270. require.NoError(t, err)
  271. require.Equal(t, map[int64]int{1: 0, 2: 0}, counts)
  272. })
  273. t.Run("success returns counts", func(t *testing.T) {
  274. cache := &stubConcurrencyCacheForTest{apiKeyConcurrency: map[int64]int{1: 3, 2: 0}}
  275. svc := NewConcurrencyService(cache)
  276. counts, err := svc.GetAPIKeyConcurrencyBatch(context.Background(), []int64{1, 2})
  277. require.NoError(t, err)
  278. require.Equal(t, map[int64]int{1: 3, 2: 0}, counts)
  279. })
  280. }
  281. func TestAcquireOpenAIWSIngressLease(t *testing.T) {
  282. t.Run("zero value release is safe", func(t *testing.T) {
  283. var lease OpenAIWSIngressLease
  284. require.NotPanics(t, lease.Release)
  285. })
  286. t.Run("disabled", func(t *testing.T) {
  287. cache := &ingressLeaseCacheForTest{}
  288. lease, acquired, err := NewConcurrencyService(cache).AcquireOpenAIWSIngressLease(nil, 1, 0)
  289. require.NoError(t, err)
  290. require.True(t, acquired)
  291. require.Nil(t, lease)
  292. require.Zero(t, cache.acquireIngressCalls)
  293. })
  294. t.Run("unsupported cache fails closed", func(t *testing.T) {
  295. lease, acquired, err := NewConcurrencyService(&stubConcurrencyCacheForTest{}).AcquireOpenAIWSIngressLease(context.Background(), 1, 1)
  296. require.Error(t, err)
  297. require.False(t, acquired)
  298. require.Nil(t, lease)
  299. })
  300. t.Run("capacity rejected", func(t *testing.T) {
  301. cache := &ingressLeaseCacheForTest{acquireIngressResult: false}
  302. lease, acquired, err := NewConcurrencyService(cache).AcquireOpenAIWSIngressLease(context.Background(), 1, 1)
  303. require.NoError(t, err)
  304. require.False(t, acquired)
  305. require.Nil(t, lease)
  306. })
  307. t.Run("release returns capacity", func(t *testing.T) {
  308. cache := &ingressLeaseCacheForTest{acquireIngressResult: true, refreshIngressResult: true}
  309. lease, acquired, err := NewConcurrencyService(cache).AcquireOpenAIWSIngressLease(nil, 1, 1)
  310. require.NoError(t, err)
  311. require.True(t, acquired)
  312. require.NotNil(t, lease)
  313. lease.Release()
  314. lease.Release()
  315. require.Equal(t, 1, cache.releaseIngressCalls)
  316. })
  317. }
  318. func TestOpenAIWSIngressLeaseRefreshLoss(t *testing.T) {
  319. t.Run("missing lease is lost immediately", func(t *testing.T) {
  320. cache := &ingressLeaseCacheForTest{refreshIngressResult: false}
  321. lease := &OpenAIWSIngressLease{cache: cache, apiKeyID: 1, leaseID: "missing"}
  322. _, lost := lease.refresh(time.Now())
  323. require.True(t, lost)
  324. require.Equal(t, 1, cache.refreshIngressCalls)
  325. })
  326. t.Run("persistent redis errors lose lease after ttl", func(t *testing.T) {
  327. cache := &ingressLeaseCacheForTest{refreshIngressErr: errors.New("redis unavailable")}
  328. lease := &OpenAIWSIngressLease{cache: cache, apiKeyID: 1, leaseID: "unconfirmed"}
  329. _, lost := lease.refresh(time.Now().Add(-openAIWSIngressLeaseTTL))
  330. require.True(t, lost)
  331. require.Equal(t, 1, cache.refreshIngressCalls)
  332. })
  333. }
  334. func TestOpenAIWSIngressLeaseReleaseWaitsForInFlightRefresh(t *testing.T) {
  335. refreshStarted := make(chan struct{})
  336. allowRefresh := make(chan struct{})
  337. cache := &ingressLeaseCacheForTest{
  338. refreshIngressFn: func(context.Context, int64, string) (bool, error) {
  339. close(refreshStarted)
  340. <-allowRefresh
  341. return true, nil
  342. },
  343. }
  344. ctx, cancel := context.WithCancelCause(context.Background())
  345. lease := &OpenAIWSIngressLease{
  346. ctx: ctx,
  347. cancel: cancel,
  348. cache: cache,
  349. apiKeyID: 1,
  350. leaseID: "in-flight-refresh",
  351. stopCh: make(chan struct{}),
  352. refreshDone: make(chan struct{}),
  353. }
  354. go func() {
  355. defer close(lease.refreshDone)
  356. _, _ = lease.refresh(time.Now())
  357. }()
  358. <-refreshStarted
  359. released := make(chan struct{})
  360. go func() {
  361. lease.Release()
  362. close(released)
  363. }()
  364. select {
  365. case <-released:
  366. t.Fatal("release returned before the in-flight refresh completed")
  367. case <-time.After(20 * time.Millisecond):
  368. }
  369. require.Zero(t, cache.releaseIngressCalls)
  370. close(allowRefresh)
  371. select {
  372. case <-released:
  373. case <-time.After(time.Second):
  374. t.Fatal("release did not complete after the refresh returned")
  375. }
  376. require.Equal(t, 1, cache.releaseIngressCalls)
  377. }
  378. func TestGenerateRequestID_UsesStablePrefixAndMonotonicCounter(t *testing.T) {
  379. id1 := generateRequestID()
  380. id2 := generateRequestID()
  381. require.NotEmpty(t, id1)
  382. require.NotEmpty(t, id2)
  383. p1 := strings.Split(id1, "-")
  384. p2 := strings.Split(id2, "-")
  385. require.Len(t, p1, 2)
  386. require.Len(t, p2, 2)
  387. require.Equal(t, p1[0], p2[0], "同一进程前缀应保持一致")
  388. n1, err := strconv.ParseUint(p1[1], 36, 64)
  389. require.NoError(t, err)
  390. n2, err := strconv.ParseUint(p2[1], 36, 64)
  391. require.NoError(t, err)
  392. require.Equal(t, n1+1, n2, "计数器应单调递增")
  393. }
  394. func TestGetAccountsLoadBatch_ReturnsCorrectData(t *testing.T) {
  395. expected := map[int64]*AccountLoadInfo{
  396. 1: {AccountID: 1, CurrentConcurrency: 3, WaitingCount: 0, LoadRate: 60},
  397. 2: {AccountID: 2, CurrentConcurrency: 5, WaitingCount: 2, LoadRate: 100},
  398. }
  399. cache := &stubConcurrencyCacheForTest{loadBatch: expected}
  400. svc := NewConcurrencyService(cache)
  401. accounts := []AccountWithConcurrency{
  402. {ID: 1, MaxConcurrency: 5},
  403. {ID: 2, MaxConcurrency: 5},
  404. }
  405. result, err := svc.GetAccountsLoadBatch(context.Background(), accounts)
  406. require.NoError(t, err)
  407. require.Equal(t, expected, result)
  408. }
  409. func TestGetAccountsLoadBatch_NilCache(t *testing.T) {
  410. svc := &ConcurrencyService{cache: nil}
  411. result, err := svc.GetAccountsLoadBatch(context.Background(), nil)
  412. require.NoError(t, err)
  413. require.Empty(t, result)
  414. }
  415. func TestGetAccountsLoadBatch_UsesShortTTLCache(t *testing.T) {
  416. cache := &stubConcurrencyCacheForTest{
  417. loadBatch: map[int64]*AccountLoadInfo{
  418. 1: {AccountID: 1, CurrentConcurrency: 1, LoadRate: 20},
  419. },
  420. }
  421. svc := NewConcurrencyService(cache)
  422. svc.SetAccountLoadBatchCacheTTL(time.Second)
  423. accounts := []AccountWithConcurrency{{ID: 1, MaxConcurrency: 5}}
  424. first, err := svc.GetAccountsLoadBatch(context.Background(), accounts)
  425. require.NoError(t, err)
  426. require.Equal(t, 1, first[int64(1)].CurrentConcurrency)
  427. cache.loadBatch[1] = &AccountLoadInfo{AccountID: 1, CurrentConcurrency: 4, LoadRate: 80}
  428. second, err := svc.GetAccountsLoadBatch(context.Background(), accounts)
  429. require.NoError(t, err)
  430. require.Equal(t, 1, second[int64(1)].CurrentConcurrency)
  431. require.Equal(t, int64(1), cache.loadBatchCalls.Load())
  432. }
  433. func TestGetAccountsLoadBatchFresh_BypassesShortTTLCache(t *testing.T) {
  434. cache := &stubConcurrencyCacheForTest{
  435. loadBatch: map[int64]*AccountLoadInfo{
  436. 1: {AccountID: 1, CurrentConcurrency: 1, LoadRate: 20},
  437. },
  438. }
  439. svc := NewConcurrencyService(cache)
  440. svc.SetAccountLoadBatchCacheTTL(time.Second)
  441. accounts := []AccountWithConcurrency{{ID: 1, MaxConcurrency: 5}}
  442. _, err := svc.GetAccountsLoadBatch(context.Background(), accounts)
  443. require.NoError(t, err)
  444. cache.loadBatch[1] = &AccountLoadInfo{AccountID: 1, CurrentConcurrency: 4, LoadRate: 80}
  445. fresh, err := svc.GetAccountsLoadBatchFresh(context.Background(), accounts)
  446. require.NoError(t, err)
  447. require.Equal(t, 4, fresh[int64(1)].CurrentConcurrency)
  448. require.Equal(t, int64(2), cache.loadBatchCalls.Load())
  449. }
  450. func TestIncrementWaitCount_Success(t *testing.T) {
  451. cache := &stubConcurrencyCacheForTest{waitAllowed: true}
  452. svc := NewConcurrencyService(cache)
  453. allowed, err := svc.IncrementWaitCount(context.Background(), 1, 25)
  454. require.NoError(t, err)
  455. require.True(t, allowed)
  456. }
  457. func TestIncrementWaitCount_QueueFull(t *testing.T) {
  458. cache := &stubConcurrencyCacheForTest{waitAllowed: false}
  459. svc := NewConcurrencyService(cache)
  460. allowed, err := svc.IncrementWaitCount(context.Background(), 1, 25)
  461. require.NoError(t, err)
  462. require.False(t, allowed)
  463. }
  464. func TestIncrementWaitCount_FailOpen(t *testing.T) {
  465. // Redis 错误时应 fail-open(允许请求通过)
  466. cache := &stubConcurrencyCacheForTest{waitErr: errors.New("redis timeout")}
  467. svc := NewConcurrencyService(cache)
  468. allowed, err := svc.IncrementWaitCount(context.Background(), 1, 25)
  469. require.NoError(t, err, "Redis 错误不应传播")
  470. require.True(t, allowed, "Redis 错误时应 fail-open")
  471. }
  472. func TestIncrementWaitCount_NilCache(t *testing.T) {
  473. svc := &ConcurrencyService{cache: nil}
  474. allowed, err := svc.IncrementWaitCount(context.Background(), 1, 25)
  475. require.NoError(t, err)
  476. require.True(t, allowed, "nil cache 应 fail-open")
  477. }
  478. func TestCalculateMaxWait(t *testing.T) {
  479. tests := []struct {
  480. concurrency int
  481. expected int
  482. }{
  483. {5, 25}, // 5 + 20
  484. {1, 21}, // 1 + 20
  485. {0, 21}, // min(1) + 20
  486. {-1, 21}, // min(1) + 20
  487. {10, 30}, // 10 + 20
  488. }
  489. for _, tt := range tests {
  490. result := CalculateMaxWait(tt.concurrency)
  491. require.Equal(t, tt.expected, result, "CalculateMaxWait(%d)", tt.concurrency)
  492. }
  493. }
  494. func TestGetAccountWaitingCount(t *testing.T) {
  495. cache := &stubConcurrencyCacheForTest{waitCount: 5}
  496. svc := NewConcurrencyService(cache)
  497. count, err := svc.GetAccountWaitingCount(context.Background(), 1)
  498. require.NoError(t, err)
  499. require.Equal(t, 5, count)
  500. }
  501. func TestGetAccountWaitingCount_NilCache(t *testing.T) {
  502. svc := &ConcurrencyService{cache: nil}
  503. count, err := svc.GetAccountWaitingCount(context.Background(), 1)
  504. require.NoError(t, err)
  505. require.Equal(t, 0, count)
  506. }
  507. func TestGetAccountConcurrencyBatch(t *testing.T) {
  508. cache := &stubConcurrencyCacheForTest{concurrency: 3}
  509. svc := NewConcurrencyService(cache)
  510. result, err := svc.GetAccountConcurrencyBatch(context.Background(), []int64{1, 2, 3})
  511. require.NoError(t, err)
  512. require.Len(t, result, 3)
  513. for _, id := range []int64{1, 2, 3} {
  514. require.Equal(t, 3, result[id])
  515. }
  516. }
  517. func TestIncrementAccountWaitCount_FailOpen(t *testing.T) {
  518. cache := &stubConcurrencyCacheForTest{waitErr: errors.New("redis error")}
  519. svc := NewConcurrencyService(cache)
  520. allowed, err := svc.IncrementAccountWaitCount(context.Background(), 1, 10)
  521. require.NoError(t, err, "Redis 错误不应传播")
  522. require.True(t, allowed, "Redis 错误时应 fail-open")
  523. }
  524. func TestIncrementAccountWaitCount_NilCache(t *testing.T) {
  525. svc := &ConcurrencyService{cache: nil}
  526. allowed, err := svc.IncrementAccountWaitCount(context.Background(), 1, 10)
  527. require.NoError(t, err)
  528. require.True(t, allowed)
  529. }