github.com/gitbundle/modules@v0.0.0-20231025071548-85b91c5c3b01/queue/unique_queue_channel.go (about) 1 // Copyright 2023 The GitBundle Inc. All rights reserved. 2 // Copyright 2017 The Gitea Authors. All rights reserved. 3 // Use of this source code is governed by a MIT-style 4 // license that can be found in the LICENSE file. 5 6 package queue 7 8 import ( 9 "context" 10 "fmt" 11 "runtime/pprof" 12 "sync" 13 "sync/atomic" 14 "time" 15 16 "github.com/gitbundle/modules/json" 17 "github.com/gitbundle/modules/log" 18 ) 19 20 // ChannelUniqueQueueType is the type for channel queue 21 const ChannelUniqueQueueType Type = "unique-channel" 22 23 // ChannelUniqueQueueConfiguration is the configuration for a ChannelUniqueQueue 24 type ChannelUniqueQueueConfiguration ChannelQueueConfiguration 25 26 // ChannelUniqueQueue implements UniqueQueue 27 // 28 // It is basically a thin wrapper around a WorkerPool but keeps a store of 29 // what has been pushed within a table. 30 // 31 // Please note that this Queue does not guarantee that a particular 32 // task cannot be processed twice or more at the same time. Uniqueness is 33 // only guaranteed whilst the task is waiting in the queue. 34 type ChannelUniqueQueue struct { 35 *WorkerPool 36 lock sync.Mutex 37 table map[string]bool 38 shutdownCtx context.Context 39 shutdownCtxCancel context.CancelFunc 40 terminateCtx context.Context 41 terminateCtxCancel context.CancelFunc 42 exemplar interface{} 43 workers int 44 name string 45 } 46 47 // NewChannelUniqueQueue create a memory channel queue 48 func NewChannelUniqueQueue(handle HandlerFunc, cfg, exemplar interface{}) (Queue, error) { 49 configInterface, err := toConfig(ChannelUniqueQueueConfiguration{}, cfg) 50 if err != nil { 51 return nil, err 52 } 53 config := configInterface.(ChannelUniqueQueueConfiguration) 54 if config.BatchLength == 0 { 55 config.BatchLength = 1 56 } 57 58 terminateCtx, terminateCtxCancel := context.WithCancel(context.Background()) 59 shutdownCtx, shutdownCtxCancel := context.WithCancel(terminateCtx) 60 61 queue := &ChannelUniqueQueue{ 62 table: map[string]bool{}, 63 shutdownCtx: shutdownCtx, 64 shutdownCtxCancel: shutdownCtxCancel, 65 terminateCtx: terminateCtx, 66 terminateCtxCancel: terminateCtxCancel, 67 exemplar: exemplar, 68 workers: config.Workers, 69 name: config.Name, 70 } 71 queue.WorkerPool = NewWorkerPool(func(data ...Data) (unhandled []Data) { 72 for _, datum := range data { 73 // No error is possible here because PushFunc ensures that this can be marshalled 74 bs, _ := json.Marshal(datum) 75 76 queue.lock.Lock() 77 delete(queue.table, string(bs)) 78 queue.lock.Unlock() 79 80 if u := handle(datum); u != nil { 81 if queue.IsPaused() { 82 // We can only pushback to the channel if we're paused. 83 go func() { 84 if err := queue.Push(u[0]); err != nil { 85 log.Error("Unable to push back to queue %d. Error: %v", queue.qid, err) 86 } 87 }() 88 } else { 89 unhandled = append(unhandled, u...) 90 } 91 } 92 } 93 return unhandled 94 }, config.WorkerPoolConfiguration) 95 96 queue.qid = GetManager().Add(queue, ChannelUniqueQueueType, config, exemplar) 97 return queue, nil 98 } 99 100 // Run starts to run the queue 101 func (q *ChannelUniqueQueue) Run(atShutdown, atTerminate func(func())) { 102 pprof.SetGoroutineLabels(q.baseCtx) 103 atShutdown(q.Shutdown) 104 atTerminate(q.Terminate) 105 log.Debug("ChannelUniqueQueue: %s Starting", q.name) 106 _ = q.AddWorkers(q.workers, 0) 107 } 108 109 // Push will push data into the queue if the data is not already in the queue 110 func (q *ChannelUniqueQueue) Push(data Data) error { 111 return q.PushFunc(data, nil) 112 } 113 114 // PushFunc will push data into the queue 115 func (q *ChannelUniqueQueue) PushFunc(data Data, fn func() error) error { 116 if !assignableTo(data, q.exemplar) { 117 return fmt.Errorf("unable to assign data: %v to same type as exemplar: %v in queue: %s", data, q.exemplar, q.name) 118 } 119 120 bs, err := json.Marshal(data) 121 if err != nil { 122 return err 123 } 124 q.lock.Lock() 125 locked := true 126 defer func() { 127 if locked { 128 q.lock.Unlock() 129 } 130 }() 131 if _, ok := q.table[string(bs)]; ok { 132 return ErrAlreadyInQueue 133 } 134 // FIXME: We probably need to implement some sort of limit here 135 // If the downstream queue blocks this table will grow without limit 136 q.table[string(bs)] = true 137 if fn != nil { 138 err := fn() 139 if err != nil { 140 delete(q.table, string(bs)) 141 return err 142 } 143 } 144 locked = false 145 q.lock.Unlock() 146 q.WorkerPool.Push(data) 147 return nil 148 } 149 150 // Has checks if the data is in the queue 151 func (q *ChannelUniqueQueue) Has(data Data) (bool, error) { 152 bs, err := json.Marshal(data) 153 if err != nil { 154 return false, err 155 } 156 157 q.lock.Lock() 158 defer q.lock.Unlock() 159 _, has := q.table[string(bs)] 160 return has, nil 161 } 162 163 // Flush flushes the channel with a timeout - the Flush worker will be registered as a flush worker with the manager 164 func (q *ChannelUniqueQueue) Flush(timeout time.Duration) error { 165 if q.IsPaused() { 166 return nil 167 } 168 ctx, cancel := q.commonRegisterWorkers(1, timeout, true) 169 defer cancel() 170 return q.FlushWithContext(ctx) 171 } 172 173 // FlushWithContext is very similar to CleanUp but it will return as soon as the dataChan is empty 174 func (q *ChannelUniqueQueue) FlushWithContext(ctx context.Context) error { 175 log.Trace("ChannelUniqueQueue: %d Flush", q.qid) 176 paused, _ := q.IsPausedIsResumed() 177 for { 178 select { 179 case <-paused: 180 return nil 181 default: 182 } 183 select { 184 case data, ok := <-q.dataChan: 185 if !ok { 186 return nil 187 } 188 if unhandled := q.handle(data); unhandled != nil { 189 log.Error("Unhandled Data whilst flushing queue %d", q.qid) 190 } 191 atomic.AddInt64(&q.numInQueue, -1) 192 case <-q.baseCtx.Done(): 193 return q.baseCtx.Err() 194 case <-ctx.Done(): 195 return ctx.Err() 196 default: 197 return nil 198 } 199 } 200 } 201 202 // Shutdown processing from this queue 203 func (q *ChannelUniqueQueue) Shutdown() { 204 log.Trace("ChannelUniqueQueue: %s Shutting down", q.name) 205 select { 206 case <-q.shutdownCtx.Done(): 207 return 208 default: 209 } 210 go func() { 211 log.Trace("ChannelUniqueQueue: %s Flushing", q.name) 212 if err := q.FlushWithContext(q.terminateCtx); err != nil { 213 log.Warn("ChannelUniqueQueue: %s Terminated before completed flushing", q.name) 214 return 215 } 216 log.Debug("ChannelUniqueQueue: %s Flushed", q.name) 217 }() 218 q.shutdownCtxCancel() 219 log.Debug("ChannelUniqueQueue: %s Shutdown", q.name) 220 } 221 222 // Terminate this queue and close the queue 223 func (q *ChannelUniqueQueue) Terminate() { 224 log.Trace("ChannelUniqueQueue: %s Terminating", q.name) 225 q.Shutdown() 226 select { 227 case <-q.terminateCtx.Done(): 228 return 229 default: 230 } 231 q.terminateCtxCancel() 232 q.baseCtxFinished() 233 log.Debug("ChannelUniqueQueue: %s Terminated", q.name) 234 } 235 236 // Name returns the name of this queue 237 func (q *ChannelUniqueQueue) Name() string { 238 return q.name 239 } 240 241 func init() { 242 queuesMap[ChannelUniqueQueueType] = NewChannelUniqueQueue 243 }