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  }