scheduler_snapshot_full_rebuild_test.go 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162
  1. package service
  2. import (
  3. "context"
  4. "errors"
  5. "sync"
  6. "sync/atomic"
  7. "testing"
  8. "time"
  9. "github.com/stretchr/testify/require"
  10. )
  11. type schedulerFullRebuildTestCache struct {
  12. SchedulerCache
  13. mu sync.Mutex
  14. listErr error
  15. listCalls int
  16. captures int
  17. lockCalls int
  18. }
  19. func (c *schedulerFullRebuildTestCache) ListBuckets(context.Context) ([]SchedulerBucket, error) {
  20. c.mu.Lock()
  21. defer c.mu.Unlock()
  22. c.listCalls++
  23. return nil, c.listErr
  24. }
  25. func (c *schedulerFullRebuildTestCache) TryLockBucket(context.Context, SchedulerBucket, time.Duration) (bool, error) {
  26. c.mu.Lock()
  27. defer c.mu.Unlock()
  28. c.lockCalls++
  29. return false, nil
  30. }
  31. func (c *schedulerFullRebuildTestCache) CaptureBucketWriteToken(_ context.Context, bucket SchedulerBucket) (SchedulerBucketWriteToken, error) {
  32. c.mu.Lock()
  33. c.captures++
  34. c.mu.Unlock()
  35. return SchedulerBucketWriteToken{Bucket: bucket, Epoch: 1}, nil
  36. }
  37. func (c *schedulerFullRebuildTestCache) ReopenBucket(_ context.Context, bucket SchedulerBucket) (SchedulerBucketWriteToken, error) {
  38. return SchedulerBucketWriteToken{Bucket: bucket, Epoch: 1}, nil
  39. }
  40. func TestSchedulerSnapshotServiceFullRebuildCoalescesConcurrentRequestsIntoTrailingRun(t *testing.T) {
  41. svc := &SchedulerSnapshotService{}
  42. wantTrailingErr := errors.New("trailing rebuild failed")
  43. firstStarted := make(chan struct{})
  44. releaseFirst := make(chan struct{})
  45. var releaseOnce sync.Once
  46. release := func() {
  47. releaseOnce.Do(func() { close(releaseFirst) })
  48. }
  49. defer release()
  50. var calls atomic.Int32
  51. var active atomic.Int32
  52. var maxActive atomic.Int32
  53. run := func() error {
  54. call := calls.Add(1)
  55. currentActive := active.Add(1)
  56. defer active.Add(-1)
  57. for {
  58. previousMax := maxActive.Load()
  59. if currentActive <= previousMax || maxActive.CompareAndSwap(previousMax, currentActive) {
  60. break
  61. }
  62. }
  63. if call == 1 {
  64. close(firstStarted)
  65. <-releaseFirst
  66. return nil
  67. }
  68. return wantTrailingErr
  69. }
  70. firstResult := make(chan error, 1)
  71. go func() {
  72. firstResult <- svc.coalesceFullRebuild(run)
  73. }()
  74. select {
  75. case <-firstStarted:
  76. case <-time.After(time.Second):
  77. t.Fatal("first rebuild did not start")
  78. }
  79. const followers = 20
  80. followerResults := make(chan error, followers)
  81. for range followers {
  82. go func() {
  83. followerResults <- svc.coalesceFullRebuild(run)
  84. }()
  85. }
  86. require.Eventually(t, func() bool {
  87. requested, _ := schedulerFullRebuildState(svc)
  88. return requested == followers+1
  89. }, time.Second, time.Millisecond)
  90. release()
  91. require.NoError(t, <-firstResult)
  92. for range followers {
  93. require.ErrorIs(t, <-followerResults, wantTrailingErr)
  94. }
  95. require.EqualValues(t, 2, calls.Load())
  96. require.EqualValues(t, 1, maxActive.Load())
  97. requested, completed := schedulerFullRebuildState(svc)
  98. require.EqualValues(t, followers+1, requested)
  99. require.Equal(t, requested, completed)
  100. }
  101. func TestSchedulerSnapshotServiceFullRebuildRunsAgainForSequentialRequest(t *testing.T) {
  102. svc := &SchedulerSnapshotService{}
  103. wantSecondErr := errors.New("second rebuild failed")
  104. var calls atomic.Int32
  105. run := func() error {
  106. if calls.Add(1) == 2 {
  107. return wantSecondErr
  108. }
  109. return nil
  110. }
  111. require.NoError(t, svc.coalesceFullRebuild(run))
  112. require.ErrorIs(t, svc.coalesceFullRebuild(run), wantSecondErr)
  113. require.EqualValues(t, 2, calls.Load())
  114. requested, completed := schedulerFullRebuildState(svc)
  115. require.EqualValues(t, 2, requested)
  116. require.Equal(t, requested, completed)
  117. }
  118. func TestSchedulerSnapshotServiceInitialFullRebuildFailsClosedWhenListBucketsFails(t *testing.T) {
  119. cache := &schedulerFullRebuildTestCache{listErr: errors.New("list buckets failed")}
  120. svc := NewSchedulerSnapshotService(cache, nil, nil, nil, nil)
  121. svc.runInitialRebuild()
  122. cache.mu.Lock()
  123. listCalls := cache.listCalls
  124. captures := cache.captures
  125. lockCalls := cache.lockCalls
  126. cache.mu.Unlock()
  127. require.Equal(t, 1, listCalls)
  128. require.Zero(t, captures)
  129. require.Zero(t, lockCalls)
  130. requested, completed := schedulerFullRebuildState(svc)
  131. require.EqualValues(t, 1, requested)
  132. require.Equal(t, requested, completed)
  133. svc.fullRebuildStateMu.Lock()
  134. require.ErrorIs(t, svc.fullRebuildLastErr, cache.listErr)
  135. svc.fullRebuildStateMu.Unlock()
  136. }
  137. func schedulerFullRebuildState(svc *SchedulerSnapshotService) (requested uint64, completed uint64) {
  138. svc.fullRebuildStateMu.Lock()
  139. defer svc.fullRebuildStateMu.Unlock()
  140. return svc.fullRebuildRequested, svc.fullRebuildCompleted
  141. }