proxy-pool/internal/gateway/outcome/queue.go
youfak 6766097ea7
Some checks are pending
ci / proto (push) Waiting to run
ci / test (ubuntu-latest) (push) Waiting to run
ci / test (windows-latest) (push) Waiting to run
ci / race (push) Waiting to run
ci / integration (push) Waiting to run
feat: report gateway proxy outcomes
2026-07-31 17:36:17 +08:00

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
}