feat(uploads): a bounded queue, in total and per user (review F6)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -17,6 +17,11 @@ import (
|
|||||||
// ErrStopped is returned by Submit once the worker has stopped.
|
// ErrStopped is returned by Submit once the worker has stopped.
|
||||||
var ErrStopped = errors.New("uploads: worker stopped")
|
var ErrStopped = errors.New("uploads: worker stopped")
|
||||||
|
|
||||||
|
// ErrQueueFull is returned by Submit when the queue, or one user's share of it, is at
|
||||||
|
// its cap. Every queued job holds up to the upload cap on disk until its turn, so an
|
||||||
|
// unbounded queue is a disk-filling lever (ADR-035 review).
|
||||||
|
var ErrQueueFull = errors.New("uploads: queue full")
|
||||||
|
|
||||||
// Converter turns the uploaded file into the 16 kHz mono PCM WAV whisper wants.
|
// Converter turns the uploaded file into the 16 kHz mono PCM WAV whisper wants.
|
||||||
type Converter interface {
|
type Converter interface {
|
||||||
ToWAV(ctx context.Context, src, dst string) error
|
ToWAV(ctx context.Context, src, dst string) error
|
||||||
@@ -54,6 +59,12 @@ func WithHeartbeat(beat, still time.Duration) WorkerOption {
|
|||||||
return func(w *Worker) { w.beatEvery, w.stillEvery = beat, still }
|
return func(w *Worker) { w.beatEvery, w.stillEvery = beat, still }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithQueueLimits bounds how many jobs may WAIT: in total, and per user. A job being
|
||||||
|
// processed is not waiting. The defaults are 20 and 3 (one instance, one job at a time).
|
||||||
|
func WithQueueLimits(total, perUser int) WorkerOption {
|
||||||
|
return func(w *Worker) { w.maxQueued, w.maxPerUser = total, perUser }
|
||||||
|
}
|
||||||
|
|
||||||
// WithMaxDuration makes the worker measure the converted audio and fail a job whose real
|
// WithMaxDuration makes the worker measure the converted audio and fail a job whose real
|
||||||
// length exceeds the limit, or falls far short of what the file claimed. Zero (the
|
// length exceeds the limit, or falls far short of what the file claimed. Zero (the
|
||||||
// default) measures nothing. The limit is first checked against the file's header, which
|
// default) measures nothing. The limit is first checked against the file's header, which
|
||||||
@@ -64,6 +75,8 @@ func WithMaxDuration(d time.Duration) WorkerOption { return func(w *Worker) { w.
|
|||||||
func WithLogger(l *slog.Logger) WorkerOption { return func(w *Worker) { w.log = l } }
|
func WithLogger(l *slog.Logger) WorkerOption { return func(w *Worker) { w.log = l } }
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
defaultMaxQueued = 20
|
||||||
|
defaultMaxPerUser = 3
|
||||||
defaultBeatEvery = 10 * time.Second
|
defaultBeatEvery = 10 * time.Second
|
||||||
defaultStillEvery = 5 * time.Minute
|
defaultStillEvery = 5 * time.Minute
|
||||||
)
|
)
|
||||||
@@ -95,6 +108,7 @@ type Worker struct {
|
|||||||
log *slog.Logger
|
log *slog.Logger
|
||||||
beatEvery, stillEvery time.Duration
|
beatEvery, stillEvery time.Duration
|
||||||
maxDuration time.Duration
|
maxDuration time.Duration
|
||||||
|
maxQueued, maxPerUser int
|
||||||
|
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
queue []Job
|
queue []Job
|
||||||
@@ -108,6 +122,7 @@ func NewWorker(s WorkerStore, c Converter, t Transcriber, tmpDir string, timeout
|
|||||||
w := &Worker{
|
w := &Worker{
|
||||||
store: s, conv: c, tr: t, tmpDir: tmpDir, timeout: timeout, wake: make(chan struct{}, 1),
|
store: s, conv: c, tr: t, tmpDir: tmpDir, timeout: timeout, wake: make(chan struct{}, 1),
|
||||||
beatEvery: defaultBeatEvery, stillEvery: defaultStillEvery,
|
beatEvery: defaultBeatEvery, stillEvery: defaultStillEvery,
|
||||||
|
maxQueued: defaultMaxQueued, maxPerUser: defaultMaxPerUser,
|
||||||
}
|
}
|
||||||
for _, o := range opts {
|
for _, o := range opts {
|
||||||
o(w)
|
o(w)
|
||||||
@@ -122,6 +137,18 @@ func (w *Worker) Submit(j Job) error {
|
|||||||
if w.stopped {
|
if w.stopped {
|
||||||
return ErrStopped
|
return ErrStopped
|
||||||
}
|
}
|
||||||
|
if len(w.queue) >= w.maxQueued {
|
||||||
|
return ErrQueueFull
|
||||||
|
}
|
||||||
|
mine := 0
|
||||||
|
for _, q := range w.queue {
|
||||||
|
if q.UserID == j.UserID {
|
||||||
|
mine++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if mine >= w.maxPerUser {
|
||||||
|
return ErrQueueFull
|
||||||
|
}
|
||||||
w.queue = append(w.queue, j)
|
w.queue = append(w.queue, j)
|
||||||
select {
|
select {
|
||||||
case w.wake <- struct{}{}:
|
case w.wake <- struct{}{}:
|
||||||
|
|||||||
Reference in New Issue
Block a user