github.com/treeverse/lakefs@v1.24.1-0.20240520134607-95648127bfb0/pkg/batch/executor.go (about) 1 package batch 2 3 import ( 4 "context" 5 "time" 6 7 "github.com/treeverse/lakefs/pkg/logging" 8 ) 9 10 // RequestBufferSize is the amount of requests users can dispatch that haven't been processed yet before 11 // dispatching new ones would start blocking. 12 const RequestBufferSize = 1 << 17 13 14 type Executer interface { 15 Execute() (interface{}, error) 16 } 17 18 type ExecuterFunc func() (interface{}, error) 19 20 func (b ExecuterFunc) Execute() (interface{}, error) { 21 return b() 22 } 23 24 type Tracker interface { 25 // Batched is called when a request is added to an existing batch. 26 Batched() 27 } 28 29 type DelayFn func(dur time.Duration) 30 31 type Batcher interface { 32 BatchFor(ctx context.Context, key string, dur time.Duration, exec Executer) (interface{}, error) 33 } 34 35 type NoOpBatchingExecutor struct{} 36 37 // contextKey used to keep values on context.Context 38 type contextKey string 39 40 // SkipBatchContextKey existence on a context will eliminate the request batching 41 const SkipBatchContextKey contextKey = "skip_batch" 42 43 func (n *NoOpBatchingExecutor) BatchFor(_ context.Context, _ string, _ time.Duration, exec Executer) (interface{}, error) { 44 return exec.Execute() 45 } 46 47 // ConditionalExecutor will batch requests only if SkipBatchContextKey is not on the context 48 // of the batch request. 49 type ConditionalExecutor struct { 50 executor *Executor 51 } 52 53 func NewConditionalExecutor(logger logging.Logger) *ConditionalExecutor { 54 return &ConditionalExecutor{executor: NewExecutor(logger)} 55 } 56 57 func (c *ConditionalExecutor) Run(ctx context.Context) { 58 c.executor.Run(ctx) 59 } 60 61 func (c *ConditionalExecutor) BatchFor(ctx context.Context, key string, timeout time.Duration, exec Executer) (interface{}, error) { 62 if ctx.Value(SkipBatchContextKey) != nil { 63 return exec.Execute() 64 } 65 return c.executor.BatchFor(ctx, key, timeout, exec) 66 } 67 68 type response struct { 69 v interface{} 70 err error 71 } 72 73 type request struct { 74 key string 75 timeout time.Duration 76 exec Executer 77 onResponse chan *response 78 } 79 80 type Executor struct { 81 // requests is the channel accepting inbound requests 82 requests chan *request 83 // execs is the internal channel used to dispatch the callback functions. 84 // Several requests with the same key in a given duration will trigger a single write to exec said key. 85 execs chan string 86 waitingOnKey map[string][]*request 87 Logger logging.Logger 88 Delay DelayFn 89 } 90 91 func NopExecutor() *NoOpBatchingExecutor { 92 return &NoOpBatchingExecutor{} 93 } 94 95 func NewExecutor(logger logging.Logger) *Executor { 96 return &Executor{ 97 requests: make(chan *request, RequestBufferSize), 98 execs: make(chan string, RequestBufferSize), 99 waitingOnKey: make(map[string][]*request), 100 Logger: logger, 101 Delay: time.Sleep, 102 } 103 } 104 105 func (e *Executor) BatchFor(_ context.Context, key string, timeout time.Duration, exec Executer) (interface{}, error) { 106 cb := make(chan *response) 107 e.requests <- &request{ 108 key: key, 109 timeout: timeout, 110 exec: exec, 111 onResponse: cb, 112 } 113 res := <-cb 114 return res.v, res.err 115 } 116 117 func (e *Executor) Run(ctx context.Context) { 118 for { 119 select { 120 case <-ctx.Done(): 121 return 122 case req := <-e.requests: 123 // see if we have it scheduled already 124 if _, exists := e.waitingOnKey[req.key]; !exists { 125 e.waitingOnKey[req.key] = []*request{req} 126 // this is a new key, let's fire a timer for it 127 go func(req *request) { 128 e.Delay(req.timeout) 129 e.execs <- req.key 130 }(req) 131 } else { 132 if b, ok := req.exec.(Tracker); ok { 133 b.Batched() 134 } 135 e.waitingOnKey[req.key] = append(e.waitingOnKey[req.key], req) 136 } 137 case execKey := <-e.execs: 138 // let's take all callbacks 139 waiters := e.waitingOnKey[execKey] 140 delete(e.waitingOnKey, execKey) 141 go func(key string) { 142 // execute and call all mapped callbacks 143 v, err := waiters[0].exec.Execute() 144 if e.Logger.IsTracing() { 145 e.Logger.WithFields(logging.Fields{ 146 "waiters": len(waiters), 147 "key": key, 148 }).Trace("dispatched execute result") 149 } 150 for _, waiter := range waiters { 151 waiter.onResponse <- &response{v, err} 152 } 153 }(execKey) 154 } 155 } 156 }