billing_cache_service_singleflight_test.go 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167
  1. //go:build unit
  2. package service
  3. import (
  4. "context"
  5. "errors"
  6. "sync"
  7. "sync/atomic"
  8. "testing"
  9. "time"
  10. "github.com/Wei-Shaw/sub2api/internal/config"
  11. "github.com/stretchr/testify/require"
  12. )
  13. type billingCacheMissStub struct {
  14. setBalanceCalls atomic.Int64
  15. }
  16. func (s *billingCacheMissStub) GetUserBalance(ctx context.Context, userID int64) (float64, error) {
  17. return 0, errors.New("cache miss")
  18. }
  19. func (s *billingCacheMissStub) SetUserBalance(ctx context.Context, userID int64, balance float64) error {
  20. s.setBalanceCalls.Add(1)
  21. return nil
  22. }
  23. func (s *billingCacheMissStub) DeductUserBalance(ctx context.Context, userID int64, amount float64) error {
  24. return nil
  25. }
  26. func (s *billingCacheMissStub) InvalidateUserBalance(ctx context.Context, userID int64) error {
  27. return nil
  28. }
  29. func (s *billingCacheMissStub) GetSubscriptionCache(ctx context.Context, userID, groupID int64) (*SubscriptionCacheData, error) {
  30. return nil, errors.New("cache miss")
  31. }
  32. func (s *billingCacheMissStub) SetSubscriptionCache(ctx context.Context, userID, groupID int64, data *SubscriptionCacheData) error {
  33. return nil
  34. }
  35. func (s *billingCacheMissStub) UpdateSubscriptionUsage(ctx context.Context, userID, groupID int64, cost float64) error {
  36. return nil
  37. }
  38. func (s *billingCacheMissStub) InvalidateSubscriptionCache(ctx context.Context, userID, groupID int64) error {
  39. return nil
  40. }
  41. func (s *billingCacheMissStub) GetAPIKeyRateLimit(ctx context.Context, keyID int64) (*APIKeyRateLimitCacheData, error) {
  42. return nil, errors.New("cache miss")
  43. }
  44. func (s *billingCacheMissStub) SetAPIKeyRateLimit(ctx context.Context, keyID int64, data *APIKeyRateLimitCacheData) error {
  45. return nil
  46. }
  47. func (s *billingCacheMissStub) UpdateAPIKeyRateLimitUsage(ctx context.Context, keyID int64, cost float64) error {
  48. return nil
  49. }
  50. func (s *billingCacheMissStub) InvalidateAPIKeyRateLimit(ctx context.Context, keyID int64) error {
  51. return nil
  52. }
  53. func (s *billingCacheMissStub) GetUserPlatformQuotaCache(ctx context.Context, userID int64, platform string) (*UserPlatformQuotaCacheEntry, bool, error) {
  54. return nil, false, nil
  55. }
  56. func (s *billingCacheMissStub) SetUserPlatformQuotaCache(ctx context.Context, userID int64, platform string, entry *UserPlatformQuotaCacheEntry, ttl time.Duration) error {
  57. return nil
  58. }
  59. func (s *billingCacheMissStub) DeleteUserPlatformQuotaCache(ctx context.Context, userID int64, platform string) error {
  60. return nil
  61. }
  62. func (s *billingCacheMissStub) IncrUserPlatformQuotaUsageCache(ctx context.Context, userID int64, platform string, cost float64, ttl time.Duration, markDirty bool) error {
  63. return nil
  64. }
  65. func (s *billingCacheMissStub) PopDirtyUserPlatformQuotaKeys(ctx context.Context, n int) ([]UserPlatformQuotaKey, error) {
  66. return nil, nil
  67. }
  68. func (s *billingCacheMissStub) ReaddDirtyUserPlatformQuotaKeys(ctx context.Context, keys []UserPlatformQuotaKey) error {
  69. return nil
  70. }
  71. func (s *billingCacheMissStub) BatchGetUserPlatformQuotaCache(ctx context.Context, keys []UserPlatformQuotaKey) ([]*UserPlatformQuotaCacheEntry, error) {
  72. return nil, nil
  73. }
  74. type balanceLoadUserRepoStub struct {
  75. mockUserRepo
  76. calls atomic.Int64
  77. delay time.Duration
  78. balance float64
  79. }
  80. func (s *balanceLoadUserRepoStub) GetByID(ctx context.Context, id int64) (*User, error) {
  81. s.calls.Add(1)
  82. if s.delay > 0 {
  83. select {
  84. case <-time.After(s.delay):
  85. case <-ctx.Done():
  86. return nil, ctx.Err()
  87. }
  88. }
  89. return &User{ID: id, Balance: s.balance}, nil
  90. }
  91. func (s *balanceLoadUserRepoStub) ListUserAuthIdentities(context.Context, int64) ([]UserAuthIdentityRecord, error) {
  92. return nil, nil
  93. }
  94. func (s *balanceLoadUserRepoStub) UnbindUserAuthProvider(context.Context, int64, string) error {
  95. return nil
  96. }
  97. func TestBillingCacheServiceGetUserBalance_Singleflight(t *testing.T) {
  98. cache := &billingCacheMissStub{}
  99. userRepo := &balanceLoadUserRepoStub{
  100. delay: 80 * time.Millisecond,
  101. balance: 12.34,
  102. }
  103. svc := NewBillingCacheService(cache, userRepo, nil, nil, nil, nil, &config.Config{}, nil)
  104. t.Cleanup(svc.Stop)
  105. const goroutines = 16
  106. start := make(chan struct{})
  107. var wg sync.WaitGroup
  108. errCh := make(chan error, goroutines)
  109. balCh := make(chan float64, goroutines)
  110. for i := 0; i < goroutines; i++ {
  111. wg.Add(1)
  112. go func() {
  113. defer wg.Done()
  114. <-start
  115. bal, err := svc.GetUserBalance(context.Background(), 99)
  116. errCh <- err
  117. balCh <- bal
  118. }()
  119. }
  120. close(start)
  121. wg.Wait()
  122. close(errCh)
  123. close(balCh)
  124. for err := range errCh {
  125. require.NoError(t, err)
  126. }
  127. for bal := range balCh {
  128. require.Equal(t, 12.34, bal)
  129. }
  130. require.Equal(t, int64(1), userRepo.calls.Load(), "并发穿透应被 singleflight 合并")
  131. require.Eventually(t, func() bool {
  132. return cache.setBalanceCalls.Load() >= 1
  133. }, time.Second, 10*time.Millisecond)
  134. }