// Package outcome provides the Gateway's bounded, non-blocking observation // buffer. It deliberately has no dependency on Controller or storage code. package outcome import ( "context" "errors" "sync/atomic" domain "proxy-pool/internal/domain/outcome" ) var ErrInvalidQueueOptions = errors.New("invalid gateway outcome queue options") type QueueOptions struct { Capacity int MaxBatch int } // Queue accepts observations from the request path without blocking. Events // beyond Capacity are intentionally dropped because outcome telemetry must not // delay proxy forwarding or consume unbounded process memory. type Queue struct { events chan domain.Event maxBatch int dropped atomic.Uint64 } func NewQueue(options QueueOptions) (*Queue, error) { if options.Capacity <= 0 || options.MaxBatch <= 0 || options.MaxBatch > options.Capacity { return nil, ErrInvalidQueueOptions } return &Queue{events: make(chan domain.Event, options.Capacity), maxBatch: options.MaxBatch}, nil } func (queue *Queue) Record(event domain.Event) { if queue == nil { return } select { case queue.events <- event: default: queue.dropped.Add(1) } } func (queue *Queue) Dropped() uint64 { if queue == nil { return 0 } return queue.dropped.Load() } func (queue *Queue) MaxBatch() int { if queue == nil { return 0 } return queue.maxBatch } // Next waits for one event then drains an immediately available bounded batch. func (queue *Queue) Next(ctx context.Context) ([]domain.Event, error) { if queue == nil || ctx == nil { return nil, ErrInvalidQueueOptions } select { case <-ctx.Done(): return nil, ctx.Err() case first := <-queue.events: return queue.drain(first), nil } } // TryNext drains one batch only when work is already buffered. func (queue *Queue) TryNext() ([]domain.Event, bool) { if queue == nil { return nil, false } select { case first := <-queue.events: return queue.drain(first), true default: return nil, false } } func (queue *Queue) drain(first domain.Event) []domain.Event { batch := make([]domain.Event, 0, queue.maxBatch) batch = append(batch, first) for len(batch) < queue.maxBatch { select { case event := <-queue.events: batch = append(batch, event) default: return batch } } return batch }