100 lines
2.2 KiB
Go
100 lines
2.2 KiB
Go
// 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
|
|
}
|