concurrency_cache_integration_test.go 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879
  1. //go:build integration
  2. package repository
  3. import (
  4. "context"
  5. "errors"
  6. "fmt"
  7. "strconv"
  8. "testing"
  9. "time"
  10. "github.com/Wei-Shaw/sub2api/internal/service"
  11. "github.com/redis/go-redis/v9"
  12. "github.com/stretchr/testify/require"
  13. "github.com/stretchr/testify/suite"
  14. )
  15. // 测试用 TTL 配置(15 分钟,与默认值一致)
  16. const testSlotTTLMinutes = 15
  17. // 测试用 TTL Duration,用于 TTL 断言
  18. var testSlotTTL = time.Duration(testSlotTTLMinutes) * time.Minute
  19. type ConcurrencyCacheSuite struct {
  20. IntegrationRedisSuite
  21. cache service.ConcurrencyCache
  22. rawCache *concurrencyCache
  23. }
  24. func TestConcurrencyCacheSuite(t *testing.T) {
  25. suite.Run(t, new(ConcurrencyCacheSuite))
  26. }
  27. func (s *ConcurrencyCacheSuite) SetupTest() {
  28. s.IntegrationRedisSuite.SetupTest()
  29. s.rawCache = NewConcurrencyCache(s.rdb, testSlotTTLMinutes, int(testSlotTTL.Seconds())).(*concurrencyCache)
  30. s.cache = s.rawCache
  31. }
  32. type apiKeyConcurrencyCacheForTest interface {
  33. TrackAPIKeySlot(ctx context.Context, apiKeyID int64, requestID string) error
  34. ReleaseAPIKeySlot(ctx context.Context, apiKeyID int64, requestID string) error
  35. GetAPIKeyConcurrencyBatch(ctx context.Context, apiKeyIDs []int64) (map[int64]int, error)
  36. }
  37. func (s *ConcurrencyCacheSuite) apiKeyConcurrencyCache() apiKeyConcurrencyCacheForTest {
  38. cache, ok := s.cache.(apiKeyConcurrencyCacheForTest)
  39. require.True(s.T(), ok)
  40. return cache
  41. }
  42. func (s *ConcurrencyCacheSuite) TestOpenAIWSIngressAPIKeySlot_HardLimitRefreshAndRelease() {
  43. apiKeyID := int64(9011)
  44. firstLeaseID := "ingress-first"
  45. secondLeaseID := "ingress-second"
  46. ok, err := s.rawCache.AcquireOpenAIWSIngressLease(s.ctx, apiKeyID, 1, firstLeaseID)
  47. require.NoError(s.T(), err)
  48. require.True(s.T(), ok)
  49. ok, err = s.rawCache.AcquireOpenAIWSIngressLease(s.ctx, apiKeyID, 1, secondLeaseID)
  50. require.NoError(s.T(), err)
  51. require.False(s.T(), ok, "a second live session must not exceed the API key limit")
  52. ok, err = s.rawCache.RefreshOpenAIWSIngressLease(s.ctx, apiKeyID, firstLeaseID)
  53. require.NoError(s.T(), err)
  54. require.True(s.T(), ok, "the current owner must be able to refresh its lease")
  55. require.NoError(s.T(), s.rawCache.ReleaseOpenAIWSIngressLease(s.ctx, apiKeyID, firstLeaseID))
  56. ok, err = s.rawCache.AcquireOpenAIWSIngressLease(s.ctx, apiKeyID, 1, secondLeaseID)
  57. require.NoError(s.T(), err)
  58. require.True(s.T(), ok, "released capacity must become available immediately")
  59. }
  60. func (s *ConcurrencyCacheSuite) TestOpenAIWSIngressAPIKeySlot_ReapsCrashedLeaseWithoutDeletingLiveOtherInstance() {
  61. apiKeyID := int64(9012)
  62. key := openAIWSIngressLeaseKey(apiKeyID)
  63. now, err := s.rawCache.redisUnixSeconds(s.ctx)
  64. require.NoError(s.T(), err)
  65. require.NoError(s.T(), s.rdb.ZAdd(s.ctx, key,
  66. redis.Z{Score: float64(now - openAIWSIngressLeaseTTLSeconds - 1), Member: "crashed-instance"},
  67. redis.Z{Score: float64(now), Member: "live-other-instance"},
  68. ).Err())
  69. require.NoError(s.T(), s.rdb.Expire(s.ctx, key, time.Duration(openAIWSIngressLeaseTTLSeconds)*time.Second).Err())
  70. ok, err := s.rawCache.AcquireOpenAIWSIngressLease(s.ctx, apiKeyID, 2, "new-instance")
  71. require.NoError(s.T(), err)
  72. require.True(s.T(), ok, "the crashed member should be reaped before enforcing the limit")
  73. _, err = s.rdb.ZScore(s.ctx, key, "crashed-instance").Result()
  74. require.ErrorIs(s.T(), err, redis.Nil)
  75. _, err = s.rdb.ZScore(s.ctx, key, "live-other-instance").Result()
  76. require.NoError(s.T(), err, "a live lease owned by another instance must be preserved")
  77. count, err := s.rdb.ZCard(s.ctx, key).Result()
  78. require.NoError(s.T(), err)
  79. require.Equal(s.T(), int64(2), count)
  80. }
  81. func (s *ConcurrencyCacheSuite) TestLiveLease_CountsTowardRegularAccountAndUserLimits() {
  82. liveCache, ok := s.cache.(service.LiveConcurrencyCache)
  83. require.True(s.T(), ok)
  84. accountID := int64(9101)
  85. userID := int64(9102)
  86. apiKeyID := int64(9103)
  87. acquired, err := liveCache.AcquireLiveLease(
  88. s.ctx,
  89. accountID,
  90. 1,
  91. userID,
  92. 1,
  93. apiKeyID,
  94. "live-integration",
  95. false,
  96. )
  97. require.NoError(s.T(), err)
  98. require.True(s.T(), acquired)
  99. regularAccount, err := s.cache.AcquireAccountSlot(s.ctx, accountID, 1, "regular-account")
  100. require.NoError(s.T(), err)
  101. require.False(s.T(), regularAccount)
  102. regularUser, err := s.cache.AcquireUserSlot(s.ctx, userID, 1, "regular-user")
  103. require.NoError(s.T(), err)
  104. require.False(s.T(), regularUser)
  105. refreshed, err := liveCache.RefreshLiveLease(s.ctx, accountID, userID, apiKeyID, "live-integration")
  106. require.NoError(s.T(), err)
  107. require.True(s.T(), refreshed)
  108. require.NoError(s.T(), liveCache.ReleaseLiveLease(s.ctx, accountID, userID, apiKeyID, "live-integration"))
  109. regularAccount, err = s.cache.AcquireAccountSlot(s.ctx, accountID, 1, "regular-account")
  110. require.NoError(s.T(), err)
  111. require.True(s.T(), regularAccount)
  112. }
  113. func (s *ConcurrencyCacheSuite) TestAccountSlot_AcquireAndRelease() {
  114. accountID := int64(10)
  115. reqID1, reqID2, reqID3 := "req1", "req2", "req3"
  116. ok, err := s.cache.AcquireAccountSlot(s.ctx, accountID, 2, reqID1)
  117. require.NoError(s.T(), err, "AcquireAccountSlot 1")
  118. require.True(s.T(), ok)
  119. ok, err = s.cache.AcquireAccountSlot(s.ctx, accountID, 2, reqID2)
  120. require.NoError(s.T(), err, "AcquireAccountSlot 2")
  121. require.True(s.T(), ok)
  122. ok, err = s.cache.AcquireAccountSlot(s.ctx, accountID, 2, reqID3)
  123. require.NoError(s.T(), err, "AcquireAccountSlot 3")
  124. require.False(s.T(), ok, "expected third acquire to fail")
  125. cur, err := s.cache.GetAccountConcurrency(s.ctx, accountID)
  126. require.NoError(s.T(), err, "GetAccountConcurrency")
  127. require.Equal(s.T(), 2, cur, "concurrency mismatch")
  128. require.NoError(s.T(), s.cache.ReleaseAccountSlot(s.ctx, accountID, reqID1), "ReleaseAccountSlot")
  129. cur, err = s.cache.GetAccountConcurrency(s.ctx, accountID)
  130. require.NoError(s.T(), err, "GetAccountConcurrency after release")
  131. require.Equal(s.T(), 1, cur, "expected 1 after release")
  132. }
  133. func (s *ConcurrencyCacheSuite) TestAccountActiveIndex_AcquireAndRelease() {
  134. accountID := int64(610)
  135. member := strconv.FormatInt(accountID, 10)
  136. reqID := "active-index-req"
  137. now, err := s.rawCache.redisUnixSeconds(s.ctx)
  138. require.NoError(s.T(), err)
  139. ok, err := s.cache.AcquireAccountSlot(s.ctx, accountID, 2, reqID)
  140. require.NoError(s.T(), err)
  141. require.True(s.T(), ok)
  142. score, err := s.rdb.ZScore(s.ctx, accountActiveIndexKey, member).Result()
  143. require.NoError(s.T(), err)
  144. require.Greater(s.T(), int64(score), now, "index score should be a future expiry")
  145. require.NoError(s.T(), s.cache.ReleaseAccountSlot(s.ctx, accountID, reqID))
  146. _, err = s.rdb.ZScore(s.ctx, accountActiveIndexKey, member).Result()
  147. require.ErrorIs(s.T(), err, redis.Nil, "index member should be removed after load drops to zero")
  148. }
  149. func (s *ConcurrencyCacheSuite) TestAccountActiveIndex_WaitLifecycle() {
  150. accountID := int64(611)
  151. member := strconv.FormatInt(accountID, 10)
  152. ok, err := s.cache.IncrementAccountWaitCount(s.ctx, accountID, 2)
  153. require.NoError(s.T(), err)
  154. require.True(s.T(), ok)
  155. _, err = s.rdb.ZScore(s.ctx, accountActiveIndexKey, member).Result()
  156. require.NoError(s.T(), err, "wait increment should register index member")
  157. require.NoError(s.T(), s.cache.DecrementAccountWaitCount(s.ctx, accountID))
  158. _, err = s.rdb.ZScore(s.ctx, accountActiveIndexKey, member).Result()
  159. require.ErrorIs(s.T(), err, redis.Nil, "index member should be removed after wait drops to zero")
  160. }
  161. func (s *ConcurrencyCacheSuite) TestUserActiveIndex_AcquireAndRelease() {
  162. userID := int64(612)
  163. member := strconv.FormatInt(userID, 10)
  164. reqID := "user-active-index-req"
  165. ok, err := s.cache.AcquireUserSlot(s.ctx, userID, 2, reqID)
  166. require.NoError(s.T(), err)
  167. require.True(s.T(), ok)
  168. _, err = s.rdb.ZScore(s.ctx, userActiveIndexKey, member).Result()
  169. require.NoError(s.T(), err, "acquire should register user index member")
  170. require.NoError(s.T(), s.cache.ReleaseUserSlot(s.ctx, userID, reqID))
  171. _, err = s.rdb.ZScore(s.ctx, userActiveIndexKey, member).Result()
  172. require.ErrorIs(s.T(), err, redis.Nil, "user index member should be removed after release")
  173. }
  174. func (s *ConcurrencyCacheSuite) TestAccountSlot_TTL() {
  175. accountID := int64(11)
  176. reqID := "req_ttl_test"
  177. slotKey := fmt.Sprintf("%s%d", accountSlotKeyPrefix, accountID)
  178. ok, err := s.cache.AcquireAccountSlot(s.ctx, accountID, 5, reqID)
  179. require.NoError(s.T(), err, "AcquireAccountSlot")
  180. require.True(s.T(), ok)
  181. ttl, err := s.rdb.TTL(s.ctx, slotKey).Result()
  182. require.NoError(s.T(), err, "TTL")
  183. s.AssertTTLWithin(ttl, 1*time.Second, testSlotTTL)
  184. }
  185. func (s *ConcurrencyCacheSuite) TestAccountSlot_DuplicateReqID() {
  186. accountID := int64(12)
  187. reqID := "dup-req"
  188. ok, err := s.cache.AcquireAccountSlot(s.ctx, accountID, 2, reqID)
  189. require.NoError(s.T(), err)
  190. require.True(s.T(), ok)
  191. // Acquiring with same reqID should be idempotent
  192. ok, err = s.cache.AcquireAccountSlot(s.ctx, accountID, 2, reqID)
  193. require.NoError(s.T(), err)
  194. require.True(s.T(), ok)
  195. cur, err := s.cache.GetAccountConcurrency(s.ctx, accountID)
  196. require.NoError(s.T(), err)
  197. require.Equal(s.T(), 1, cur, "expected concurrency=1 (idempotent)")
  198. }
  199. func (s *ConcurrencyCacheSuite) TestAccountSlot_ReleaseIdempotent() {
  200. accountID := int64(13)
  201. reqID := "release-test"
  202. ok, err := s.cache.AcquireAccountSlot(s.ctx, accountID, 1, reqID)
  203. require.NoError(s.T(), err)
  204. require.True(s.T(), ok)
  205. require.NoError(s.T(), s.cache.ReleaseAccountSlot(s.ctx, accountID, reqID), "ReleaseAccountSlot")
  206. // Releasing again should not error
  207. require.NoError(s.T(), s.cache.ReleaseAccountSlot(s.ctx, accountID, reqID), "ReleaseAccountSlot again")
  208. // Releasing non-existent should not error
  209. require.NoError(s.T(), s.cache.ReleaseAccountSlot(s.ctx, accountID, "non-existent"), "ReleaseAccountSlot non-existent")
  210. cur, err := s.cache.GetAccountConcurrency(s.ctx, accountID)
  211. require.NoError(s.T(), err)
  212. require.Equal(s.T(), 0, cur)
  213. }
  214. func (s *ConcurrencyCacheSuite) TestAccountSlot_MaxZero() {
  215. accountID := int64(14)
  216. reqID := "max-zero-test"
  217. ok, err := s.cache.AcquireAccountSlot(s.ctx, accountID, 0, reqID)
  218. require.NoError(s.T(), err)
  219. require.False(s.T(), ok, "expected acquire to fail with max=0")
  220. }
  221. func (s *ConcurrencyCacheSuite) TestUserSlot_AcquireAndRelease() {
  222. userID := int64(42)
  223. reqID1, reqID2 := "req1", "req2"
  224. ok, err := s.cache.AcquireUserSlot(s.ctx, userID, 1, reqID1)
  225. require.NoError(s.T(), err, "AcquireUserSlot")
  226. require.True(s.T(), ok)
  227. ok, err = s.cache.AcquireUserSlot(s.ctx, userID, 1, reqID2)
  228. require.NoError(s.T(), err, "AcquireUserSlot 2")
  229. require.False(s.T(), ok, "expected second acquire to fail at max=1")
  230. cur, err := s.cache.GetUserConcurrency(s.ctx, userID)
  231. require.NoError(s.T(), err, "GetUserConcurrency")
  232. require.Equal(s.T(), 1, cur, "expected concurrency=1")
  233. require.NoError(s.T(), s.cache.ReleaseUserSlot(s.ctx, userID, reqID1), "ReleaseUserSlot")
  234. // Releasing a non-existent slot should not error
  235. require.NoError(s.T(), s.cache.ReleaseUserSlot(s.ctx, userID, "non-existent"), "ReleaseUserSlot non-existent")
  236. cur, err = s.cache.GetUserConcurrency(s.ctx, userID)
  237. require.NoError(s.T(), err, "GetUserConcurrency after release")
  238. require.Equal(s.T(), 0, cur, "expected concurrency=0 after release")
  239. }
  240. func (s *ConcurrencyCacheSuite) TestUserSlot_TTL() {
  241. userID := int64(200)
  242. reqID := "req_ttl_test"
  243. slotKey := fmt.Sprintf("%s%d", userSlotKeyPrefix, userID)
  244. ok, err := s.cache.AcquireUserSlot(s.ctx, userID, 5, reqID)
  245. require.NoError(s.T(), err, "AcquireUserSlot")
  246. require.True(s.T(), ok)
  247. ttl, err := s.rdb.TTL(s.ctx, slotKey).Result()
  248. require.NoError(s.T(), err, "TTL")
  249. s.AssertTTLWithin(ttl, 1*time.Second, testSlotTTL)
  250. }
  251. func (s *ConcurrencyCacheSuite) TestAPIKeySlot_TrackReleaseAndBatchCount() {
  252. cache := s.apiKeyConcurrencyCache()
  253. apiKeyID := int64(300)
  254. emptyAPIKeyID := int64(301)
  255. slotKey := fmt.Sprintf("%s%d", apiKeySlotKeyPrefix, apiKeyID)
  256. require.NoError(s.T(), cache.TrackAPIKeySlot(s.ctx, apiKeyID, "req1"))
  257. require.NoError(s.T(), cache.TrackAPIKeySlot(s.ctx, apiKeyID, "req2"))
  258. counts, err := cache.GetAPIKeyConcurrencyBatch(s.ctx, []int64{apiKeyID, emptyAPIKeyID})
  259. require.NoError(s.T(), err)
  260. require.Equal(s.T(), map[int64]int{apiKeyID: 2, emptyAPIKeyID: 0}, counts)
  261. ttl, err := s.rdb.TTL(s.ctx, slotKey).Result()
  262. require.NoError(s.T(), err, "TTL")
  263. s.AssertTTLWithin(ttl, 1*time.Second, testSlotTTL)
  264. require.NoError(s.T(), cache.ReleaseAPIKeySlot(s.ctx, apiKeyID, "req1"))
  265. counts, err = cache.GetAPIKeyConcurrencyBatch(s.ctx, []int64{apiKeyID})
  266. require.NoError(s.T(), err)
  267. require.Equal(s.T(), 1, counts[apiKeyID])
  268. require.NoError(s.T(), cache.ReleaseAPIKeySlot(s.ctx, apiKeyID, "req2"))
  269. counts, err = cache.GetAPIKeyConcurrencyBatch(s.ctx, []int64{apiKeyID})
  270. require.NoError(s.T(), err)
  271. require.Equal(s.T(), 0, counts[apiKeyID])
  272. }
  273. func (s *ConcurrencyCacheSuite) TestWaitQueue_IncrementAndDecrement() {
  274. userID := int64(20)
  275. waitKey := fmt.Sprintf("%s%d", waitQueueKeyPrefix, userID)
  276. ok, err := s.cache.IncrementWaitCount(s.ctx, userID, 2)
  277. require.NoError(s.T(), err, "IncrementWaitCount 1")
  278. require.True(s.T(), ok)
  279. ok, err = s.cache.IncrementWaitCount(s.ctx, userID, 2)
  280. require.NoError(s.T(), err, "IncrementWaitCount 2")
  281. require.True(s.T(), ok)
  282. ok, err = s.cache.IncrementWaitCount(s.ctx, userID, 2)
  283. require.NoError(s.T(), err, "IncrementWaitCount 3")
  284. require.False(s.T(), ok, "expected wait increment over max to fail")
  285. ttl, err := s.rdb.TTL(s.ctx, waitKey).Result()
  286. require.NoError(s.T(), err, "TTL waitKey")
  287. s.AssertTTLWithin(ttl, 1*time.Second, testSlotTTL)
  288. require.NoError(s.T(), s.cache.DecrementWaitCount(s.ctx, userID), "DecrementWaitCount")
  289. val, err := s.rdb.Get(s.ctx, waitKey).Int()
  290. if !errors.Is(err, redis.Nil) {
  291. require.NoError(s.T(), err, "Get waitKey")
  292. }
  293. require.Equal(s.T(), 1, val, "expected wait count 1")
  294. }
  295. func (s *ConcurrencyCacheSuite) TestWaitQueue_DecrementNoNegative() {
  296. userID := int64(300)
  297. waitKey := fmt.Sprintf("%s%d", waitQueueKeyPrefix, userID)
  298. // Test decrement on non-existent key - should not error and should not create negative value
  299. require.NoError(s.T(), s.cache.DecrementWaitCount(s.ctx, userID), "DecrementWaitCount on non-existent key")
  300. // Verify no key was created or it's not negative
  301. val, err := s.rdb.Get(s.ctx, waitKey).Int()
  302. if !errors.Is(err, redis.Nil) {
  303. require.NoError(s.T(), err, "Get waitKey")
  304. }
  305. require.GreaterOrEqual(s.T(), val, 0, "expected non-negative wait count after decrement on empty")
  306. // Set count to 1, then decrement twice
  307. ok, err := s.cache.IncrementWaitCount(s.ctx, userID, 5)
  308. require.NoError(s.T(), err, "IncrementWaitCount")
  309. require.True(s.T(), ok)
  310. // Decrement once (1 -> 0)
  311. require.NoError(s.T(), s.cache.DecrementWaitCount(s.ctx, userID), "DecrementWaitCount")
  312. // Decrement again on 0 - should not go negative
  313. require.NoError(s.T(), s.cache.DecrementWaitCount(s.ctx, userID), "DecrementWaitCount on zero")
  314. // Verify count is 0, not negative
  315. val, err = s.rdb.Get(s.ctx, waitKey).Int()
  316. if !errors.Is(err, redis.Nil) {
  317. require.NoError(s.T(), err, "Get waitKey after double decrement")
  318. }
  319. require.GreaterOrEqual(s.T(), val, 0, "expected non-negative wait count")
  320. }
  321. func (s *ConcurrencyCacheSuite) TestAccountWaitQueue_IncrementAndDecrement() {
  322. accountID := int64(30)
  323. waitKey := fmt.Sprintf("%s%d", accountWaitKeyPrefix, accountID)
  324. ok, err := s.cache.IncrementAccountWaitCount(s.ctx, accountID, 2)
  325. require.NoError(s.T(), err, "IncrementAccountWaitCount 1")
  326. require.True(s.T(), ok)
  327. ok, err = s.cache.IncrementAccountWaitCount(s.ctx, accountID, 2)
  328. require.NoError(s.T(), err, "IncrementAccountWaitCount 2")
  329. require.True(s.T(), ok)
  330. ok, err = s.cache.IncrementAccountWaitCount(s.ctx, accountID, 2)
  331. require.NoError(s.T(), err, "IncrementAccountWaitCount 3")
  332. require.False(s.T(), ok, "expected account wait increment over max to fail")
  333. ttl, err := s.rdb.TTL(s.ctx, waitKey).Result()
  334. require.NoError(s.T(), err, "TTL account waitKey")
  335. s.AssertTTLWithin(ttl, 1*time.Second, testSlotTTL)
  336. require.NoError(s.T(), s.cache.DecrementAccountWaitCount(s.ctx, accountID), "DecrementAccountWaitCount")
  337. val, err := s.rdb.Get(s.ctx, waitKey).Int()
  338. if !errors.Is(err, redis.Nil) {
  339. require.NoError(s.T(), err, "Get waitKey")
  340. }
  341. require.Equal(s.T(), 1, val, "expected account wait count 1")
  342. }
  343. func (s *ConcurrencyCacheSuite) TestCleanupStaleProcessSlots() {
  344. // 预置迁移 marker,隔离一次性清扫,只验证索引驱动的清理路径。
  345. require.NoError(s.T(), s.rdb.Set(s.ctx, legacyWaitSweepMarkerKey, "1", 0).Err())
  346. accountID := int64(901)
  347. userID := int64(902)
  348. apiKeyID := int64(903)
  349. unindexedAccountID := int64(1901)
  350. accountKey := fmt.Sprintf("%s%d", accountSlotKeyPrefix, accountID)
  351. userKey := fmt.Sprintf("%s%d", userSlotKeyPrefix, userID)
  352. apiKeyKey := fmt.Sprintf("%s%d", apiKeySlotKeyPrefix, apiKeyID)
  353. unindexedAccountKey := fmt.Sprintf("%s%d", accountSlotKeyPrefix, unindexedAccountID)
  354. userWaitKey := fmt.Sprintf("%s%d", waitQueueKeyPrefix, userID)
  355. accountWaitKey := fmt.Sprintf("%s%d", accountWaitKeyPrefix, accountID)
  356. unindexedAccountWaitKey := fmt.Sprintf("%s%d", accountWaitKeyPrefix, unindexedAccountID)
  357. now, err := s.rawCache.redisUnixSeconds(s.ctx)
  358. require.NoError(s.T(), err)
  359. require.NoError(s.T(), s.rdb.ZAdd(s.ctx, accountKey,
  360. redis.Z{Score: float64(now), Member: "oldproc-1"},
  361. redis.Z{Score: float64(now), Member: "keep-1"},
  362. ).Err())
  363. require.NoError(s.T(), s.rdb.ZAdd(s.ctx, userKey,
  364. redis.Z{Score: float64(now), Member: "oldproc-2"},
  365. redis.Z{Score: float64(now), Member: "keep-2"},
  366. ).Err())
  367. require.NoError(s.T(), s.rdb.ZAdd(s.ctx, unindexedAccountKey,
  368. redis.Z{Score: float64(now), Member: "oldproc-unindexed"},
  369. ).Err())
  370. require.NoError(s.T(), s.rdb.ZAdd(s.ctx, apiKeyKey,
  371. redis.Z{Score: float64(now), Member: "oldproc-3"},
  372. redis.Z{Score: float64(now), Member: "keep-3"},
  373. ).Err())
  374. require.NoError(s.T(), s.rdb.Set(s.ctx, userWaitKey, 3, time.Minute).Err())
  375. require.NoError(s.T(), s.rdb.Set(s.ctx, accountWaitKey, 2, time.Minute).Err())
  376. require.NoError(s.T(), s.rdb.Set(s.ctx, unindexedAccountWaitKey, 2, time.Minute).Err())
  377. require.NoError(s.T(), s.rdb.ZAdd(s.ctx, accountActiveIndexKey, redis.Z{
  378. Score: float64(now + 60),
  379. Member: strconv.FormatInt(accountID, 10),
  380. }).Err())
  381. require.NoError(s.T(), s.rdb.ZAdd(s.ctx, userActiveIndexKey, redis.Z{
  382. Score: float64(now + 60),
  383. Member: strconv.FormatInt(userID, 10),
  384. }).Err())
  385. require.NoError(s.T(), s.cache.CleanupStaleProcessSlots(s.ctx, "keep-"))
  386. accountMembers, err := s.rdb.ZRange(s.ctx, accountKey, 0, -1).Result()
  387. require.NoError(s.T(), err)
  388. require.Equal(s.T(), []string{"keep-1"}, accountMembers)
  389. userMembers, err := s.rdb.ZRange(s.ctx, userKey, 0, -1).Result()
  390. require.NoError(s.T(), err)
  391. require.Equal(s.T(), []string{"keep-2"}, userMembers)
  392. // API Key 槽位(stats-only)不在启动清理范围内,靠分数裁剪与 key TTL 自愈。
  393. apiKeyMembers, err := s.rdb.ZRange(s.ctx, apiKeyKey, 0, -1).Result()
  394. require.NoError(s.T(), err)
  395. require.ElementsMatch(s.T(), []string{"keep-3", "oldproc-3"}, apiKeyMembers)
  396. _, err = s.rdb.Get(s.ctx, userWaitKey).Result()
  397. require.True(s.T(), errors.Is(err, redis.Nil))
  398. _, err = s.rdb.Get(s.ctx, accountWaitKey).Result()
  399. require.True(s.T(), errors.Is(err, redis.Nil))
  400. unindexedMembers, err := s.rdb.ZRange(s.ctx, unindexedAccountKey, 0, -1).Result()
  401. require.NoError(s.T(), err)
  402. require.Equal(s.T(), []string{"oldproc-unindexed"}, unindexedMembers)
  403. _, err = s.rdb.Get(s.ctx, unindexedAccountWaitKey).Result()
  404. require.NoError(s.T(), err)
  405. }
  406. func (s *ConcurrencyCacheSuite) TestGetAccountConcurrency_Missing() {
  407. // When no slots exist, GetAccountConcurrency should return 0
  408. cur, err := s.cache.GetAccountConcurrency(s.ctx, 999)
  409. require.NoError(s.T(), err)
  410. require.Equal(s.T(), 0, cur)
  411. }
  412. func (s *ConcurrencyCacheSuite) TestGetUserConcurrency_Missing() {
  413. // When no slots exist, GetUserConcurrency should return 0
  414. cur, err := s.cache.GetUserConcurrency(s.ctx, 999)
  415. require.NoError(s.T(), err)
  416. require.Equal(s.T(), 0, cur)
  417. }
  418. func (s *ConcurrencyCacheSuite) TestGetAccountsLoadBatch() {
  419. s.T().Skip("TODO: Fix this test - CurrentConcurrency returns 0 instead of expected value in CI")
  420. // Setup: Create accounts with different load states
  421. account1 := int64(100)
  422. account2 := int64(101)
  423. account3 := int64(102)
  424. // Account 1: 2/3 slots used, 1 waiting
  425. ok, err := s.cache.AcquireAccountSlot(s.ctx, account1, 3, "req1")
  426. require.NoError(s.T(), err)
  427. require.True(s.T(), ok)
  428. ok, err = s.cache.AcquireAccountSlot(s.ctx, account1, 3, "req2")
  429. require.NoError(s.T(), err)
  430. require.True(s.T(), ok)
  431. ok, err = s.cache.IncrementAccountWaitCount(s.ctx, account1, 5)
  432. require.NoError(s.T(), err)
  433. require.True(s.T(), ok)
  434. // Account 2: 1/2 slots used, 0 waiting
  435. ok, err = s.cache.AcquireAccountSlot(s.ctx, account2, 2, "req3")
  436. require.NoError(s.T(), err)
  437. require.True(s.T(), ok)
  438. // Account 3: 0/1 slots used, 0 waiting (idle)
  439. // Query batch load
  440. accounts := []service.AccountWithConcurrency{
  441. {ID: account1, MaxConcurrency: 3},
  442. {ID: account2, MaxConcurrency: 2},
  443. {ID: account3, MaxConcurrency: 1},
  444. }
  445. loadMap, err := s.cache.GetAccountsLoadBatch(s.ctx, accounts)
  446. require.NoError(s.T(), err)
  447. require.Len(s.T(), loadMap, 3)
  448. // Verify account1: (2 + 1) / 3 = 100%
  449. load1 := loadMap[account1]
  450. require.NotNil(s.T(), load1)
  451. require.Equal(s.T(), account1, load1.AccountID)
  452. require.Equal(s.T(), 2, load1.CurrentConcurrency)
  453. require.Equal(s.T(), 1, load1.WaitingCount)
  454. require.Equal(s.T(), 100, load1.LoadRate)
  455. // Verify account2: (1 + 0) / 2 = 50%
  456. load2 := loadMap[account2]
  457. require.NotNil(s.T(), load2)
  458. require.Equal(s.T(), account2, load2.AccountID)
  459. require.Equal(s.T(), 1, load2.CurrentConcurrency)
  460. require.Equal(s.T(), 0, load2.WaitingCount)
  461. require.Equal(s.T(), 50, load2.LoadRate)
  462. // Verify account3: (0 + 0) / 1 = 0%
  463. load3 := loadMap[account3]
  464. require.NotNil(s.T(), load3)
  465. require.Equal(s.T(), account3, load3.AccountID)
  466. require.Equal(s.T(), 0, load3.CurrentConcurrency)
  467. require.Equal(s.T(), 0, load3.WaitingCount)
  468. require.Equal(s.T(), 0, load3.LoadRate)
  469. }
  470. func (s *ConcurrencyCacheSuite) TestGetAccountsLoadBatch_Empty() {
  471. // Test with empty account list
  472. loadMap, err := s.cache.GetAccountsLoadBatch(s.ctx, []service.AccountWithConcurrency{})
  473. require.NoError(s.T(), err)
  474. require.Empty(s.T(), loadMap)
  475. }
  476. func (s *ConcurrencyCacheSuite) TestCleanupExpiredAccountSlots() {
  477. accountID := int64(200)
  478. slotKey := fmt.Sprintf("%s%d", accountSlotKeyPrefix, accountID)
  479. // Acquire 3 slots
  480. ok, err := s.cache.AcquireAccountSlot(s.ctx, accountID, 5, "req1")
  481. require.NoError(s.T(), err)
  482. require.True(s.T(), ok)
  483. ok, err = s.cache.AcquireAccountSlot(s.ctx, accountID, 5, "req2")
  484. require.NoError(s.T(), err)
  485. require.True(s.T(), ok)
  486. ok, err = s.cache.AcquireAccountSlot(s.ctx, accountID, 5, "req3")
  487. require.NoError(s.T(), err)
  488. require.True(s.T(), ok)
  489. // Verify 3 slots exist
  490. cur, err := s.cache.GetAccountConcurrency(s.ctx, accountID)
  491. require.NoError(s.T(), err)
  492. require.Equal(s.T(), 3, cur)
  493. // Manually set old timestamps for req1 and req2 (simulate expired slots)
  494. now := time.Now().Unix()
  495. expiredTime := now - int64(testSlotTTL.Seconds()) - 10 // 10 seconds past TTL
  496. err = s.rdb.ZAdd(s.ctx, slotKey, redis.Z{Score: float64(expiredTime), Member: "req1"}).Err()
  497. require.NoError(s.T(), err)
  498. err = s.rdb.ZAdd(s.ctx, slotKey, redis.Z{Score: float64(expiredTime), Member: "req2"}).Err()
  499. require.NoError(s.T(), err)
  500. // Run cleanup
  501. err = s.cache.CleanupExpiredAccountSlots(s.ctx, accountID)
  502. require.NoError(s.T(), err)
  503. // Verify only 1 slot remains (req3)
  504. cur, err = s.cache.GetAccountConcurrency(s.ctx, accountID)
  505. require.NoError(s.T(), err)
  506. require.Equal(s.T(), 1, cur)
  507. // Verify req3 still exists
  508. members, err := s.rdb.ZRange(s.ctx, slotKey, 0, -1).Result()
  509. require.NoError(s.T(), err)
  510. require.Len(s.T(), members, 1)
  511. require.Equal(s.T(), "req3", members[0])
  512. }
  513. func (s *ConcurrencyCacheSuite) TestCleanupExpiredAccountSlots_NoExpired() {
  514. accountID := int64(201)
  515. // Acquire 2 fresh slots
  516. ok, err := s.cache.AcquireAccountSlot(s.ctx, accountID, 5, "req1")
  517. require.NoError(s.T(), err)
  518. require.True(s.T(), ok)
  519. ok, err = s.cache.AcquireAccountSlot(s.ctx, accountID, 5, "req2")
  520. require.NoError(s.T(), err)
  521. require.True(s.T(), ok)
  522. // Run cleanup (should not remove anything)
  523. err = s.cache.CleanupExpiredAccountSlots(s.ctx, accountID)
  524. require.NoError(s.T(), err)
  525. // Verify both slots still exist
  526. cur, err := s.cache.GetAccountConcurrency(s.ctx, accountID)
  527. require.NoError(s.T(), err)
  528. require.Equal(s.T(), 2, cur)
  529. }
  530. func (s *ConcurrencyCacheSuite) TestCleanupExpiredAccountSlotKeys() {
  531. now, err := s.rawCache.redisUnixSeconds(s.ctx)
  532. require.NoError(s.T(), err)
  533. expiredTime := now - int64(testSlotTTL.Seconds()) - 10
  534. accountKeyWithFresh := fmt.Sprintf("%s%d", accountSlotKeyPrefix, 301)
  535. accountKeyExpiredOnly := fmt.Sprintf("%s%d", accountSlotKeyPrefix, 302)
  536. userKey := fmt.Sprintf("%s%d", userSlotKeyPrefix, 303)
  537. unindexedAccountKey := fmt.Sprintf("%s%d", accountSlotKeyPrefix, 304)
  538. require.NoError(s.T(), s.rdb.ZAdd(s.ctx, accountKeyWithFresh,
  539. redis.Z{Score: float64(expiredTime), Member: "expired"},
  540. redis.Z{Score: float64(now), Member: "fresh"},
  541. ).Err())
  542. require.NoError(s.T(), s.rdb.ZAdd(s.ctx, accountKeyExpiredOnly,
  543. redis.Z{Score: float64(expiredTime), Member: "expired-only"},
  544. ).Err())
  545. require.NoError(s.T(), s.rdb.ZAdd(s.ctx, userKey,
  546. redis.Z{Score: float64(expiredTime), Member: "user-expired"},
  547. ).Err())
  548. require.NoError(s.T(), s.rdb.ZAdd(s.ctx, unindexedAccountKey,
  549. redis.Z{Score: float64(expiredTime), Member: "unindexed-expired"},
  550. ).Err())
  551. require.NoError(s.T(), s.rdb.ZAdd(s.ctx, accountActiveIndexKey,
  552. redis.Z{Score: float64(now), Member: "301"},
  553. redis.Z{Score: float64(now), Member: "302"},
  554. ).Err())
  555. require.NoError(s.T(), s.cache.CleanupExpiredAccountSlotKeys(s.ctx))
  556. accountMembers, err := s.rdb.ZRange(s.ctx, accountKeyWithFresh, 0, -1).Result()
  557. require.NoError(s.T(), err)
  558. require.Equal(s.T(), []string{"fresh"}, accountMembers)
  559. exists, err := s.rdb.Exists(s.ctx, accountKeyExpiredOnly).Result()
  560. require.NoError(s.T(), err)
  561. require.EqualValues(s.T(), 0, exists)
  562. userMembers, err := s.rdb.ZRange(s.ctx, userKey, 0, -1).Result()
  563. require.NoError(s.T(), err)
  564. require.Equal(s.T(), []string{"user-expired"}, userMembers)
  565. unindexedMembers, err := s.rdb.ZRange(s.ctx, unindexedAccountKey, 0, -1).Result()
  566. require.NoError(s.T(), err)
  567. require.Equal(s.T(), []string{"unindexed-expired"}, unindexedMembers)
  568. score, err := s.rdb.ZScore(s.ctx, accountActiveIndexKey, "301").Result()
  569. require.NoError(s.T(), err)
  570. require.Greater(s.T(), int64(score), now)
  571. _, err = s.rdb.ZScore(s.ctx, accountActiveIndexKey, "302").Result()
  572. require.ErrorIs(s.T(), err, redis.Nil)
  573. }
  574. func (s *ConcurrencyCacheSuite) TestCleanupExpiredAccountSlotKeys_ReapsUserIndex() {
  575. now, err := s.rawCache.redisUnixSeconds(s.ctx)
  576. require.NoError(s.T(), err)
  577. expiredScore := float64(now - 10)
  578. userKeyWithFresh := fmt.Sprintf("%s%d", userSlotKeyPrefix, 401)
  579. // 401 有真实负载但索引 score 已过期:应刷新而不是删除。
  580. require.NoError(s.T(), s.rdb.ZAdd(s.ctx, userKeyWithFresh,
  581. redis.Z{Score: float64(now), Member: "fresh"},
  582. ).Err())
  583. // 402 无任何负载:过期索引 member 应被回收。
  584. // 非法 member 也应随过期候选一并清除。
  585. require.NoError(s.T(), s.rdb.ZAdd(s.ctx, userActiveIndexKey,
  586. redis.Z{Score: expiredScore, Member: "401"},
  587. redis.Z{Score: expiredScore, Member: "402"},
  588. redis.Z{Score: expiredScore, Member: "not-a-user-id"},
  589. ).Err())
  590. require.NoError(s.T(), s.cache.CleanupExpiredAccountSlotKeys(s.ctx))
  591. score, err := s.rdb.ZScore(s.ctx, userActiveIndexKey, "401").Result()
  592. require.NoError(s.T(), err)
  593. require.Greater(s.T(), int64(score), now, "loaded user should be re-scheduled, not dropped")
  594. _, err = s.rdb.ZScore(s.ctx, userActiveIndexKey, "402").Result()
  595. require.ErrorIs(s.T(), err, redis.Nil, "idle expired user member should be reaped")
  596. _, err = s.rdb.ZScore(s.ctx, userActiveIndexKey, "not-a-user-id").Result()
  597. require.ErrorIs(s.T(), err, redis.Nil, "invalid member should be reaped")
  598. }
  599. func (s *ConcurrencyCacheSuite) TestCleanupStaleProcessSlots_LegacyWaitSweepRunsOnce() {
  600. unindexedAccountWaitKey := fmt.Sprintf("%s%d", accountWaitKeyPrefix, 2901)
  601. unindexedUserWaitKey := fmt.Sprintf("%s%d", waitQueueKeyPrefix, 2902)
  602. require.NoError(s.T(), s.rdb.Set(s.ctx, unindexedAccountWaitKey, 5, time.Minute).Err())
  603. require.NoError(s.T(), s.rdb.Set(s.ctx, unindexedUserWaitKey, 3, time.Minute).Err())
  604. // 首次运行:marker 不存在,一次性清扫删除所有遗留等待计数(含未入索引的)。
  605. require.NoError(s.T(), s.cache.CleanupStaleProcessSlots(s.ctx, "keep-"))
  606. _, err := s.rdb.Get(s.ctx, unindexedAccountWaitKey).Result()
  607. require.ErrorIs(s.T(), err, redis.Nil, "legacy account wait key should be swept on first startup")
  608. _, err = s.rdb.Get(s.ctx, unindexedUserWaitKey).Result()
  609. require.ErrorIs(s.T(), err, redis.Nil, "legacy user wait key should be swept on first startup")
  610. exists, err := s.rdb.Exists(s.ctx, legacyWaitSweepMarkerKey).Result()
  611. require.NoError(s.T(), err)
  612. require.EqualValues(s.T(), 1, exists, "sweep marker should be set after first run")
  613. // 再次运行:marker 已存在,未入索引的等待计数不再被触碰。
  614. require.NoError(s.T(), s.rdb.Set(s.ctx, unindexedAccountWaitKey, 5, time.Minute).Err())
  615. require.NoError(s.T(), s.cache.CleanupStaleProcessSlots(s.ctx, "keep-"))
  616. val, err := s.rdb.Get(s.ctx, unindexedAccountWaitKey).Int()
  617. require.NoError(s.T(), err, "sweep must not run twice")
  618. require.Equal(s.T(), 5, val)
  619. }
  620. func (s *ConcurrencyCacheSuite) TestCleanupStaleProcessSlots_ProcessesExpiredIndexMembers() {
  621. // score 已过期的索引成员往往正是崩溃进程留下的残留,启动清理必须覆盖它们。
  622. require.NoError(s.T(), s.rdb.Set(s.ctx, legacyWaitSweepMarkerKey, "1", 0).Err())
  623. accountID := int64(3901)
  624. userID := int64(3902)
  625. accountKey := fmt.Sprintf("%s%d", accountSlotKeyPrefix, accountID)
  626. userKey := fmt.Sprintf("%s%d", userSlotKeyPrefix, userID)
  627. accountWaitKey := fmt.Sprintf("%s%d", accountWaitKeyPrefix, accountID)
  628. now, err := s.rawCache.redisUnixSeconds(s.ctx)
  629. require.NoError(s.T(), err)
  630. require.NoError(s.T(), s.rdb.ZAdd(s.ctx, accountKey,
  631. redis.Z{Score: float64(now), Member: "oldproc-1"},
  632. ).Err())
  633. require.NoError(s.T(), s.rdb.ZAdd(s.ctx, userKey,
  634. redis.Z{Score: float64(now), Member: "oldproc-2"},
  635. ).Err())
  636. require.NoError(s.T(), s.rdb.Set(s.ctx, accountWaitKey, 4, time.Minute).Err())
  637. // 索引 score 设为过去时刻,模拟长时间停机后索引已“过期”。
  638. require.NoError(s.T(), s.rdb.ZAdd(s.ctx, accountActiveIndexKey, redis.Z{
  639. Score: float64(now - 100),
  640. Member: strconv.FormatInt(accountID, 10),
  641. }).Err())
  642. require.NoError(s.T(), s.rdb.ZAdd(s.ctx, userActiveIndexKey, redis.Z{
  643. Score: float64(now - 100),
  644. Member: strconv.FormatInt(userID, 10),
  645. }).Err())
  646. require.NoError(s.T(), s.cache.CleanupStaleProcessSlots(s.ctx, "keep-"))
  647. exists, err := s.rdb.Exists(s.ctx, accountKey).Result()
  648. require.NoError(s.T(), err)
  649. require.EqualValues(s.T(), 0, exists, "stale slot key of expired index member should be purged")
  650. exists, err = s.rdb.Exists(s.ctx, userKey).Result()
  651. require.NoError(s.T(), err)
  652. require.EqualValues(s.T(), 0, exists)
  653. _, err = s.rdb.Get(s.ctx, accountWaitKey).Result()
  654. require.ErrorIs(s.T(), err, redis.Nil, "wait counter of expired index member should be deleted")
  655. _, err = s.rdb.ZScore(s.ctx, accountActiveIndexKey, strconv.FormatInt(accountID, 10)).Result()
  656. require.ErrorIs(s.T(), err, redis.Nil, "emptied member should be removed from index")
  657. _, err = s.rdb.ZScore(s.ctx, userActiveIndexKey, strconv.FormatInt(userID, 10)).Result()
  658. require.ErrorIs(s.T(), err, redis.Nil)
  659. }
  660. func (s *ConcurrencyCacheSuite) TestCleanupStaleProcessSlots_RemovesOldPrefixesAndWaitCounters() {
  661. // 预置迁移 marker,确保等待计数删除来自索引驱动路径而非一次性清扫。
  662. require.NoError(s.T(), s.rdb.Set(s.ctx, legacyWaitSweepMarkerKey, "1", 0).Err())
  663. accountID := int64(901)
  664. userID := int64(902)
  665. accountSlotKey := fmt.Sprintf("%s%d", accountSlotKeyPrefix, accountID)
  666. userSlotKey := fmt.Sprintf("%s%d", userSlotKeyPrefix, userID)
  667. userWaitKey := fmt.Sprintf("%s%d", waitQueueKeyPrefix, userID)
  668. accountWaitKey := fmt.Sprintf("%s%d", accountWaitKeyPrefix, accountID)
  669. now, err := s.rawCache.redisUnixSeconds(s.ctx)
  670. require.NoError(s.T(), err)
  671. require.NoError(s.T(), s.rdb.ZAdd(s.ctx, accountSlotKey,
  672. redis.Z{Score: float64(now), Member: "oldproc-1"},
  673. redis.Z{Score: float64(now), Member: "activeproc-1"},
  674. ).Err())
  675. require.NoError(s.T(), s.rdb.Expire(s.ctx, accountSlotKey, testSlotTTL).Err())
  676. require.NoError(s.T(), s.rdb.ZAdd(s.ctx, userSlotKey,
  677. redis.Z{Score: float64(now), Member: "oldproc-2"},
  678. redis.Z{Score: float64(now), Member: "activeproc-2"},
  679. ).Err())
  680. require.NoError(s.T(), s.rdb.Expire(s.ctx, userSlotKey, testSlotTTL).Err())
  681. require.NoError(s.T(), s.rdb.Set(s.ctx, userWaitKey, 3, testSlotTTL).Err())
  682. require.NoError(s.T(), s.rdb.Set(s.ctx, accountWaitKey, 2, testSlotTTL).Err())
  683. require.NoError(s.T(), s.rdb.ZAdd(s.ctx, accountActiveIndexKey, redis.Z{
  684. Score: float64(now + 60),
  685. Member: strconv.FormatInt(accountID, 10),
  686. }).Err())
  687. require.NoError(s.T(), s.rdb.ZAdd(s.ctx, userActiveIndexKey, redis.Z{
  688. Score: float64(now + 60),
  689. Member: strconv.FormatInt(userID, 10),
  690. }).Err())
  691. require.NoError(s.T(), s.cache.CleanupStaleProcessSlots(s.ctx, "activeproc-"))
  692. accountMembers, err := s.rdb.ZRange(s.ctx, accountSlotKey, 0, -1).Result()
  693. require.NoError(s.T(), err)
  694. require.Equal(s.T(), []string{"activeproc-1"}, accountMembers)
  695. userMembers, err := s.rdb.ZRange(s.ctx, userSlotKey, 0, -1).Result()
  696. require.NoError(s.T(), err)
  697. require.Equal(s.T(), []string{"activeproc-2"}, userMembers)
  698. _, err = s.rdb.Get(s.ctx, userWaitKey).Result()
  699. require.ErrorIs(s.T(), err, redis.Nil)
  700. _, err = s.rdb.Get(s.ctx, accountWaitKey).Result()
  701. require.ErrorIs(s.T(), err, redis.Nil)
  702. }
  703. func (s *ConcurrencyCacheSuite) TestCleanupStaleProcessSlots_DeletesEmptySlotKeys() {
  704. accountID := int64(903)
  705. accountSlotKey := fmt.Sprintf("%s%d", accountSlotKeyPrefix, accountID)
  706. now, err := s.rawCache.redisUnixSeconds(s.ctx)
  707. require.NoError(s.T(), err)
  708. require.NoError(s.T(), s.rdb.ZAdd(s.ctx, accountSlotKey, redis.Z{Score: float64(now), Member: "oldproc-1"}).Err())
  709. require.NoError(s.T(), s.rdb.Expire(s.ctx, accountSlotKey, testSlotTTL).Err())
  710. require.NoError(s.T(), s.rdb.ZAdd(s.ctx, accountActiveIndexKey, redis.Z{
  711. Score: float64(now + 60),
  712. Member: strconv.FormatInt(accountID, 10),
  713. }).Err())
  714. require.NoError(s.T(), s.cache.CleanupStaleProcessSlots(s.ctx, "activeproc-"))
  715. exists, err := s.rdb.Exists(s.ctx, accountSlotKey).Result()
  716. require.NoError(s.T(), err)
  717. require.EqualValues(s.T(), 0, exists)
  718. }