github.com/treeverse/lakefs@v1.24.1-0.20240520134607-95648127bfb0/pkg/batch/executor_test.go (about) 1 package batch_test 2 3 import ( 4 "context" 5 "sync" 6 "sync/atomic" 7 "testing" 8 "time" 9 10 "github.com/treeverse/lakefs/pkg/batch" 11 "github.com/treeverse/lakefs/pkg/logging" 12 ) 13 14 type trackableExecuter struct { 15 batchTracker chan struct{} 16 execTracker chan struct{} 17 } 18 19 func (te *trackableExecuter) WasExecuted() bool { 20 chanOpen := true 21 select { 22 case _, chanOpen = <-te.execTracker: 23 default: 24 } 25 return !chanOpen 26 } 27 28 func (te *trackableExecuter) Execute() (interface{}, error) { 29 close(te.execTracker) 30 return nil, nil 31 } 32 33 func (te *trackableExecuter) Batched() { 34 close(te.batchTracker) 35 } 36 37 type db struct { 38 kvStore sync.Map 39 accessCount int32 40 } 41 42 func (d *db) Insert(key string, val string) { 43 d.kvStore.Store(key, val) 44 } 45 46 func (d *db) Get(key string) (interface{}, bool) { 47 atomic.AddInt32(&d.accessCount, 1) 48 return d.kvStore.Load(key) 49 } 50 51 func (d *db) GetAccessCount() int32 { 52 return d.accessCount 53 } 54 55 func testReadAfterWrite(t *testing.T) { 56 // Setup executor 57 exec := batch.NewExecutor(logging.ContextUnavailable()) 58 ctx, cancel := context.WithCancel(context.Background()) 59 defer cancel() 60 go exec.Run(ctx) 61 62 // Prove the executor does not violate read-after-write consistency. 63 // First, let's define read-after-write consistency: 64 // Any read that started after a successful write has returned, must return the updated value. 65 // To test this, let's simulate the following scenario: 66 // 1. reader (r1) starts (Current version: v0) 67 // 2. writer (w1) writes v1 68 // 3. writer (w1) returns (Current version: v1) 69 // 4. reader (r2) starts 70 // 5. reader (r1) returns 71 // 6. reader (r2) returns 72 // 7. both readers (r1,r2) return with v1 as their response. 73 db := &db{} 74 db.Insert("v", "v0") 75 76 read1Done := make(chan bool) 77 write1Done := make(chan bool) 78 read2Done := make(chan bool) 79 read2Batched := make(chan struct{}) 80 81 // we pass a custom delay func that ensures writer starts when reader is waiting for batch 82 // reader1 started 83 waitWrite := make(chan bool) 84 delays := int32(0) 85 delayFn := func(dur time.Duration) { 86 delaysDone := atomic.AddInt32(&delays, 1) 87 if delaysDone == 1 { 88 close(waitWrite) 89 } 90 // We want to make sure that r2 is in the same batch with r1 91 <-read2Batched 92 } 93 exec.Delay = delayFn 94 95 // reader1 starts 96 go func() { 97 r1, _ := exec.BatchFor(context.Background(), "k", time.Millisecond*50, batch.ExecuterFunc(func() (interface{}, error) { 98 version, _ := db.Get("v") 99 return version, nil 100 })) 101 r1v := r1.(string) 102 if r1v != "v1" { 103 // reader1, while it could have returned either v0 or v1 without violating read-after-write consistency, 104 // is expected to return v1 with this batching logic 105 t.Errorf("expected r1 to get v1, got %s instead", r1v) 106 } 107 close(read1Done) 108 }() 109 110 // Writer1 writes 111 go func() { 112 <-waitWrite 113 db.Insert("v", "v1") 114 close(write1Done) 115 }() 116 117 // following that write, another reader starts, and must read the updated value 118 go func() { 119 <-write1Done // ensure we start AFTER write1 has completed 120 te := trackableExecuter{batchTracker: read2Batched, execTracker: make(chan struct{})} 121 r2, _ := exec.BatchFor(context.Background(), "k", 50*time.Millisecond, &te) 122 r2v := r2.(string) 123 if r2v != "v1" { 124 t.Errorf("expected r2 to get v1, got %s instead", r2v) 125 } 126 127 // We expect r2's exec function to not execute because it should join r1's batch 128 if te.WasExecuted() { 129 t.Errorf("r2's exec function should not be called, only r1's") 130 } 131 132 if accessCount := db.GetAccessCount(); accessCount != 1 { 133 t.Errorf("db should only be accessed once, but was accessed %d times", accessCount) 134 } 135 close(read2Done) 136 }() 137 138 <-read1Done 139 <-read2Done 140 } 141 142 func testBatchExpiration(t *testing.T) { 143 // Setup executor 144 exec := batch.NewExecutor(logging.ContextUnavailable()) 145 ctx, cancel := context.WithCancel(context.Background()) 146 defer cancel() 147 go exec.Run(ctx) 148 149 // Confirm that batches expire after a delay period, and hence requests don't hang 150 // To test this, let's simulate the following scenario: 151 // 1. reader (r1) makes request with key 'k' 152 // 2. reader 1 returns v1 153 // 3. reader (r2) makes request with key 'k' 154 // 4. a new batch is used to making r2's request and the returned value is v2 155 read1Done := make(chan bool) 156 read2Done := make(chan bool) 157 158 // reader1 starts 159 go func() { 160 r1, _ := exec.BatchFor(context.Background(), "k", time.Millisecond*50, batch.ExecuterFunc(func() (interface{}, error) { 161 return "v1", nil 162 })) 163 if r1 != "v1" { 164 t.Errorf("expected r1 to get v1 but got %s instead", r1) 165 } 166 close(read1Done) 167 }() 168 169 go func() { 170 <-read1Done // ensure r2 starts after r1 has returned 171 r2, _ := exec.BatchFor(context.Background(), "k", time.Millisecond*50, batch.ExecuterFunc(func() (interface{}, error) { 172 return "v2", nil 173 })) 174 if r2 != "v2" { 175 t.Errorf("expected r2 to get v2 but got %s instead", r2) 176 } 177 close(read2Done) 178 }() 179 180 <-read2Done 181 } 182 183 func testBatchByKey(t *testing.T) { 184 // Setup executor 185 exec := batch.NewExecutor(logging.ContextUnavailable()) 186 ctx, cancel := context.WithCancel(context.Background()) 187 defer cancel() 188 go exec.Run(ctx) 189 190 // Confirm that requests are batched by key. 191 // To test this, let's simulate the following scenario: 192 // 1. reader (r1) makes a request with key 'k1' 193 // 2. reader (r2) makes a request with key 'k2' before te1's batch has been expired 194 // 3. Two batches are created and two requests are executed 195 read1Done := make(chan bool) 196 read2Done := make(chan bool) 197 198 // we pass a custom delay func that ensures r2 starts only after r1 started, and that r1 executes only 199 // after r2 made a request 200 waitRead2 := make(chan bool) 201 delays := int32(0) 202 delayFn := func(dur time.Duration) { 203 delaysDone := atomic.AddInt32(&delays, 1) 204 if delaysDone == 1 { 205 close(waitRead2) 206 <-read2Done 207 } 208 } 209 exec.Delay = delayFn 210 211 te1 := trackableExecuter{execTracker: make(chan struct{})} 212 te2 := trackableExecuter{execTracker: make(chan struct{})} 213 214 // reader1 starts 215 go func(te *trackableExecuter) { 216 _, _ = exec.BatchFor(context.Background(), "k1", 50*time.Millisecond, te) 217 close(read1Done) 218 }(&te1) 219 220 // reader2 starts 221 go func(te *trackableExecuter) { 222 <-waitRead2 // ensure we start AFTER r1 started a new batch 223 _, err := exec.BatchFor(context.Background(), "k2", 0, te) 224 if err != nil { 225 t.Errorf("BatchFor error: %s", err) 226 } 227 close(read2Done) 228 }(&te2) 229 230 <-read1Done 231 232 r1Succeeded := te1.WasExecuted() 233 r2Succeeded := te2.WasExecuted() 234 if !(r1Succeeded && r2Succeeded) { 235 t.Errorf("both r1's and r2's exec functions should be executed but r1Succeeded=%t and r2Succeeded=%t", r1Succeeded, r2Succeeded) 236 } 237 } 238 239 func TestExecutor_BatchFor(t *testing.T) { 240 var wg sync.WaitGroup 241 wg.Add(50) 242 for i := 0; i < 50; i++ { 243 go func() { 244 defer wg.Done() 245 testReadAfterWrite(t) 246 testBatchByKey(t) 247 testBatchExpiration(t) 248 }() 249 } 250 wg.Wait() 251 }