| |
| package queue |
|
|
| import ( |
| "context" |
| "sync" |
| "sync/atomic" |
| "time" |
|
|
| log "github.com/sirupsen/logrus" |
| ) |
|
|
| |
| type Priority int |
|
|
| const ( |
| PriorityCritical Priority = 0 |
| PriorityHigh Priority = 1 |
| PriorityNormal Priority = 2 |
| PriorityLow Priority = 3 |
| PriorityBatch Priority = 4 |
| ) |
|
|
| |
| func (p Priority) String() string { |
| switch p { |
| case PriorityCritical: |
| return "critical" |
| case PriorityHigh: |
| return "high" |
| case PriorityNormal: |
| return "normal" |
| case PriorityLow: |
| return "low" |
| case PriorityBatch: |
| return "batch" |
| default: |
| return "unknown" |
| } |
| } |
|
|
| |
| type Config struct { |
| Enabled bool `yaml:"enabled" json:"enabled"` |
| Workers int `yaml:"workers" json:"workers"` |
| CriticalBuffer int `yaml:"critical-buffer" json:"critical_buffer"` |
| HighBuffer int `yaml:"high-buffer" json:"high_buffer"` |
| NormalBuffer int `yaml:"normal-buffer" json:"normal_buffer"` |
| LowBuffer int `yaml:"low-buffer" json:"low_buffer"` |
| BatchBuffer int `yaml:"batch-buffer" json:"batch_buffer"` |
| } |
|
|
| |
| func DefaultConfig() *Config { |
| return &Config{ |
| Enabled: true, |
| Workers: 10, |
| CriticalBuffer: 100, |
| HighBuffer: 200, |
| NormalBuffer: 500, |
| LowBuffer: 300, |
| BatchBuffer: 1000, |
| } |
| } |
|
|
| |
| type Request struct { |
| ID string |
| Priority Priority |
| Context context.Context |
| Handler func() (interface{}, error) |
| ResultCh chan *Result |
| EnqueuedAt time.Time |
| } |
|
|
| |
| type Result struct { |
| Value interface{} |
| Error error |
| WaitTime time.Duration |
| } |
|
|
| |
| type PriorityQueue struct { |
| queues [5]chan *Request |
| workers int |
| stopCh chan struct{} |
| wg sync.WaitGroup |
| config *Config |
| mu sync.RWMutex |
| closed bool |
|
|
| |
| processed [5]int64 |
| totalWait [5]int64 |
| } |
|
|
| |
| func NewPriorityQueue(config *Config) *PriorityQueue { |
| if config == nil { |
| config = DefaultConfig() |
| } |
|
|
| if !config.Enabled { |
| return nil |
| } |
|
|
| if config.Workers <= 0 { |
| config.Workers = 10 |
| } |
|
|
| pq := &PriorityQueue{ |
| workers: config.Workers, |
| stopCh: make(chan struct{}), |
| config: config, |
| } |
|
|
| |
| pq.queues[PriorityCritical] = make(chan *Request, config.CriticalBuffer) |
| pq.queues[PriorityHigh] = make(chan *Request, config.HighBuffer) |
| pq.queues[PriorityNormal] = make(chan *Request, config.NormalBuffer) |
| pq.queues[PriorityLow] = make(chan *Request, config.LowBuffer) |
| pq.queues[PriorityBatch] = make(chan *Request, config.BatchBuffer) |
|
|
| |
| for i := 0; i < config.Workers; i++ { |
| pq.wg.Add(1) |
| go pq.worker(i) |
| } |
|
|
| log.WithFields(log.Fields{ |
| "workers": config.Workers, |
| "critical_buffer": config.CriticalBuffer, |
| "normal_buffer": config.NormalBuffer, |
| }).Info("priority queue initialized") |
|
|
| return pq |
| } |
|
|
| |
| func (pq *PriorityQueue) Enqueue(req *Request) bool { |
| if pq == nil { |
| return false |
| } |
|
|
| pq.mu.RLock() |
| if pq.closed { |
| pq.mu.RUnlock() |
| return false |
| } |
| pq.mu.RUnlock() |
|
|
| req.EnqueuedAt = time.Now() |
| if req.ResultCh == nil { |
| req.ResultCh = make(chan *Result, 1) |
| } |
|
|
| select { |
| case pq.queues[req.Priority] <- req: |
| return true |
| default: |
| |
| if req.Priority == PriorityCritical { |
| go pq.processRequest(req) |
| return true |
| } |
| return false |
| } |
| } |
|
|
| |
| func (pq *PriorityQueue) Submit(ctx context.Context, priority Priority, handler func() (interface{}, error)) (interface{}, error) { |
| if pq == nil { |
| |
| return handler() |
| } |
|
|
| req := &Request{ |
| Priority: priority, |
| Context: ctx, |
| Handler: handler, |
| ResultCh: make(chan *Result, 1), |
| } |
|
|
| if !pq.Enqueue(req) { |
| |
| return handler() |
| } |
|
|
| select { |
| case result := <-req.ResultCh: |
| return result.Value, result.Error |
| case <-ctx.Done(): |
| return nil, ctx.Err() |
| } |
| } |
|
|
| func (pq *PriorityQueue) worker(id int) { |
| defer pq.wg.Done() |
|
|
| for { |
| select { |
| case <-pq.stopCh: |
| return |
| default: |
| req := pq.getNextRequest() |
| if req != nil { |
| pq.processRequest(req) |
| } |
| } |
| } |
| } |
|
|
| func (pq *PriorityQueue) getNextRequest() *Request { |
| |
| select { |
| case req := <-pq.queues[PriorityCritical]: |
| return req |
| default: |
| } |
|
|
| select { |
| case req := <-pq.queues[PriorityCritical]: |
| return req |
| case req := <-pq.queues[PriorityHigh]: |
| return req |
| default: |
| } |
|
|
| select { |
| case req := <-pq.queues[PriorityCritical]: |
| return req |
| case req := <-pq.queues[PriorityHigh]: |
| return req |
| case req := <-pq.queues[PriorityNormal]: |
| return req |
| default: |
| } |
|
|
| |
| select { |
| case req := <-pq.queues[PriorityCritical]: |
| return req |
| case req := <-pq.queues[PriorityHigh]: |
| return req |
| case req := <-pq.queues[PriorityNormal]: |
| return req |
| case req := <-pq.queues[PriorityLow]: |
| return req |
| case req := <-pq.queues[PriorityBatch]: |
| return req |
| case <-pq.stopCh: |
| return nil |
| case <-time.After(100 * time.Millisecond): |
| return nil |
| } |
| } |
|
|
| func (pq *PriorityQueue) processRequest(req *Request) { |
| waitTime := time.Since(req.EnqueuedAt) |
|
|
| |
| if req.Context != nil { |
| select { |
| case <-req.Context.Done(): |
| req.ResultCh <- &Result{Error: req.Context.Err(), WaitTime: waitTime} |
| return |
| default: |
| } |
| } |
|
|
| |
| value, err := req.Handler() |
|
|
| |
| atomic.AddInt64(&pq.processed[req.Priority], 1) |
| atomic.AddInt64(&pq.totalWait[req.Priority], int64(waitTime)) |
|
|
| |
| select { |
| case req.ResultCh <- &Result{Value: value, Error: err, WaitTime: waitTime}: |
| default: |
| } |
| } |
|
|
| |
| type Stats struct { |
| Processed [5]int64 |
| AvgWaitTimeMs [5]float64 |
| QueueLengths [5]int |
| } |
|
|
| |
| func (pq *PriorityQueue) Stats() *Stats { |
| if pq == nil { |
| return nil |
| } |
|
|
| stats := &Stats{} |
| for i := 0; i < 5; i++ { |
| stats.Processed[i] = atomic.LoadInt64(&pq.processed[i]) |
| stats.QueueLengths[i] = len(pq.queues[i]) |
|
|
| totalWait := atomic.LoadInt64(&pq.totalWait[i]) |
| if stats.Processed[i] > 0 { |
| stats.AvgWaitTimeMs[i] = float64(totalWait) / float64(stats.Processed[i]) / float64(time.Millisecond) |
| } |
| } |
|
|
| return stats |
| } |
|
|
| |
| func (pq *PriorityQueue) Close() { |
| if pq == nil { |
| return |
| } |
|
|
| pq.mu.Lock() |
| if pq.closed { |
| pq.mu.Unlock() |
| return |
| } |
| pq.closed = true |
| close(pq.stopCh) |
| pq.mu.Unlock() |
|
|
| pq.wg.Wait() |
|
|
| log.Info("priority queue closed") |
| } |
|
|
| |
| func (pq *PriorityQueue) IsEnabled() bool { |
| return pq != nil && !pq.closed |
| } |
|
|
| |
| func DetectPriority(isStreaming bool, messageCount int, headers map[string]string) Priority { |
| |
| if p, ok := headers["X-Request-Priority"]; ok { |
| switch p { |
| case "critical": |
| return PriorityCritical |
| case "high": |
| return PriorityHigh |
| case "low": |
| return PriorityLow |
| case "batch": |
| return PriorityBatch |
| } |
| } |
|
|
| |
| if isStreaming { |
| return PriorityCritical |
| } |
|
|
| |
| if _, ok := headers["X-Batch-Request"]; ok { |
| return PriorityBatch |
| } |
|
|
| |
| if messageCount > 5 { |
| return PriorityHigh |
| } |
|
|
| return PriorityNormal |
| } |
|
|