scheduler_cache_last_used_unit_test.go 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183
  1. //go:build unit
  2. package repository
  3. import (
  4. "context"
  5. "strconv"
  6. "strings"
  7. "testing"
  8. "time"
  9. "github.com/Wei-Shaw/sub2api/internal/service"
  10. "github.com/redis/go-redis/v9"
  11. "github.com/stretchr/testify/require"
  12. )
  13. func TestSchedulerCacheUpdateLastUsedUsesSideKeyWithoutRewritingPayloads(t *testing.T) {
  14. ctx := context.Background()
  15. cache := newSchedulerCacheUnit(t)
  16. bucket := service.SchedulerBucket{
  17. GroupID: 9,
  18. Platform: service.PlatformGrok,
  19. Mode: service.SchedulerModeSingle,
  20. }
  21. initial := time.Now().UTC().Truncate(time.Millisecond).Add(-time.Hour)
  22. account := service.Account{
  23. ID: 9201,
  24. Name: "grok-large-oauth",
  25. Platform: service.PlatformGrok,
  26. Type: service.AccountTypeOAuth,
  27. Status: service.StatusActive,
  28. Schedulable: true,
  29. LastUsedAt: &initial,
  30. Credentials: map[string]any{
  31. "access_token": strings.Repeat("a", 4096),
  32. "refresh_token": strings.Repeat("r", 4096),
  33. },
  34. Extra: map[string]any{"large": strings.Repeat("x", 4096)},
  35. }
  36. token, err := cache.CaptureBucketWriteToken(ctx, bucket)
  37. require.NoError(t, err)
  38. require.NoError(t, cache.SetSnapshot(ctx, bucket, token, []service.Account{account}))
  39. id := strconv.FormatInt(account.ID, 10)
  40. fullBefore, err := cache.rdb.Get(ctx, schedulerAccountKey(id)).Bytes()
  41. require.NoError(t, err)
  42. metaBefore, err := cache.rdb.Get(ctx, schedulerAccountMetaKey(id)).Bytes()
  43. require.NoError(t, err)
  44. latest := initial.Add(37 * time.Second)
  45. require.NoError(t, cache.UpdateLastUsed(ctx, map[int64]time.Time{account.ID: latest}))
  46. fullAfter, err := cache.rdb.Get(ctx, schedulerAccountKey(id)).Bytes()
  47. require.NoError(t, err)
  48. metaAfter, err := cache.rdb.Get(ctx, schedulerAccountMetaKey(id)).Bytes()
  49. require.NoError(t, err)
  50. require.Equal(t, fullBefore, fullAfter)
  51. require.Equal(t, metaBefore, metaAfter)
  52. require.Equal(t, strconv.FormatInt(latest.UnixMilli(), 10), cache.rdb.Get(ctx, schedulerLastUsedKey(id)).Val())
  53. cached, err := cache.GetAccount(ctx, account.ID)
  54. require.NoError(t, err)
  55. require.NotNil(t, cached)
  56. require.NotNil(t, cached.LastUsedAt)
  57. require.Equal(t, latest, *cached.LastUsedAt)
  58. snapshot, hit, err := cache.GetSnapshot(ctx, bucket)
  59. require.NoError(t, err)
  60. require.True(t, hit)
  61. require.Len(t, snapshot, 1)
  62. require.NotNil(t, snapshot[0].LastUsedAt)
  63. require.Equal(t, latest, *snapshot[0].LastUsedAt)
  64. }
  65. func TestSchedulerCacheLastUsedSideKeyIsMonotonicAndRequiresAccount(t *testing.T) {
  66. ctx := context.Background()
  67. cache := newSchedulerCacheUnit(t)
  68. account := service.Account{ID: 9202, Platform: service.PlatformGrok, Type: service.AccountTypeOAuth}
  69. require.NoError(t, cache.SetAccount(ctx, &account))
  70. newer := time.Now().UTC().Truncate(time.Millisecond)
  71. older := newer.Add(-time.Minute)
  72. require.NoError(t, cache.UpdateLastUsed(ctx, map[int64]time.Time{account.ID: newer}))
  73. require.NoError(t, cache.UpdateLastUsed(ctx, map[int64]time.Time{account.ID: older}))
  74. id := strconv.FormatInt(account.ID, 10)
  75. require.Equal(t, strconv.FormatInt(newer.UnixMilli(), 10), cache.rdb.Get(ctx, schedulerLastUsedKey(id)).Val())
  76. cached, err := cache.GetAccount(ctx, account.ID)
  77. require.NoError(t, err)
  78. require.NotNil(t, cached)
  79. require.Equal(t, newer, *cached.LastUsedAt)
  80. const missingID int64 = 9299
  81. require.NoError(t, cache.UpdateLastUsed(ctx, map[int64]time.Time{missingID: newer}))
  82. _, err = cache.rdb.Get(ctx, schedulerLastUsedKey(strconv.FormatInt(missingID, 10))).Result()
  83. require.ErrorIs(t, err, redis.Nil)
  84. require.NoError(t, cache.DeleteAccount(ctx, account.ID))
  85. _, err = cache.rdb.Get(ctx, schedulerLastUsedKey(id)).Result()
  86. require.ErrorIs(t, err, redis.Nil)
  87. }
  88. func TestSchedulerCacheLastUsedSideKeyFallsBackToNewerEmbeddedValue(t *testing.T) {
  89. ctx := context.Background()
  90. cache := newSchedulerCacheUnit(t)
  91. embedded := time.Now().UTC().Truncate(time.Millisecond)
  92. account := service.Account{
  93. ID: 9203,
  94. Platform: service.PlatformGrok,
  95. Type: service.AccountTypeOAuth,
  96. LastUsedAt: &embedded,
  97. }
  98. require.NoError(t, cache.SetAccount(ctx, &account))
  99. id := strconv.FormatInt(account.ID, 10)
  100. require.NoError(t, cache.rdb.Set(ctx, schedulerLastUsedKey(id), embedded.Add(-time.Hour).UnixMilli(), 0).Err())
  101. cached, err := cache.GetAccount(ctx, account.ID)
  102. require.NoError(t, err)
  103. require.NotNil(t, cached)
  104. require.Equal(t, embedded, *cached.LastUsedAt)
  105. }
  106. func TestSchedulerCacheLastUsedSideKeySurvivesStaleAccountAndSnapshotWrites(t *testing.T) {
  107. ctx := context.Background()
  108. cache := newSchedulerCacheUnit(t)
  109. bucket := service.SchedulerBucket{
  110. GroupID: 10,
  111. Platform: service.PlatformGrok,
  112. Mode: service.SchedulerModeSingle,
  113. }
  114. embedded := time.Now().UTC().Truncate(time.Millisecond).Add(-time.Minute)
  115. latest := embedded.Add(30 * time.Second)
  116. account := service.Account{
  117. ID: 9204,
  118. Platform: service.PlatformGrok,
  119. Type: service.AccountTypeOAuth,
  120. Schedulable: true,
  121. LastUsedAt: &embedded,
  122. }
  123. require.NoError(t, cache.SetAccount(ctx, &account))
  124. require.NoError(t, cache.UpdateLastUsed(ctx, map[int64]time.Time{account.ID: latest}))
  125. require.NoError(t, cache.SetAccount(ctx, &account))
  126. token, err := cache.CaptureBucketWriteToken(ctx, bucket)
  127. require.NoError(t, err)
  128. require.NoError(t, cache.SetSnapshot(ctx, bucket, token, []service.Account{account}))
  129. id := strconv.FormatInt(account.ID, 10)
  130. require.Equal(t, strconv.FormatInt(latest.UnixMilli(), 10), cache.rdb.Get(ctx, schedulerLastUsedKey(id)).Val())
  131. cached, err := cache.GetAccount(ctx, account.ID)
  132. require.NoError(t, err)
  133. require.NotNil(t, cached)
  134. require.Equal(t, latest, *cached.LastUsedAt)
  135. snapshot, hit, err := cache.GetSnapshot(ctx, bucket)
  136. require.NoError(t, err)
  137. require.True(t, hit)
  138. require.Len(t, snapshot, 1)
  139. require.Equal(t, latest, *snapshot[0].LastUsedAt)
  140. }
  141. func TestSchedulerCacheUpdateLastUsedChunksLargeBatches(t *testing.T) {
  142. ctx := context.Background()
  143. cache := newSchedulerCacheUnit(t)
  144. total := schedulerLastUsedUpdateChunkSize + 1
  145. accounts := make([]service.Account, 0, total)
  146. updates := make(map[int64]time.Time, total)
  147. base := time.Now().UTC().Truncate(time.Millisecond)
  148. for i := 0; i < total; i++ {
  149. id := int64(9300 + i)
  150. accounts = append(accounts, service.Account{ID: id, Platform: service.PlatformGrok})
  151. updates[id] = base.Add(time.Duration(i) * time.Millisecond)
  152. }
  153. written, err := cache.writeAccountIDs(ctx, accounts)
  154. require.NoError(t, err)
  155. require.Len(t, written, total)
  156. require.NoError(t, cache.UpdateLastUsed(ctx, updates))
  157. for id, usedAt := range updates {
  158. key := schedulerLastUsedKey(strconv.FormatInt(id, 10))
  159. require.Equal(t, strconv.FormatInt(usedAt.UnixMilli(), 10), cache.rdb.Get(ctx, key).Val())
  160. }
  161. }