diff --git a/internal/extsvc/manager.go b/internal/extsvc/manager.go index 70e18f9..5b6c481 100644 --- a/internal/extsvc/manager.go +++ b/internal/extsvc/manager.go @@ -87,77 +87,39 @@ type Manager struct { mu sync.Mutex cfg ExternalServices rnd *rand.Rand - - // uploadCh serialises immediate auto-uploads through a single worker. Firing a - // goroutine per QSO meant a pileup / ADIF-import burst hit a service with dozens - // of concurrent requests at once — Club Log's nginx answers that with 403, the - // upload is counted as failed and the QSO stays at "R" despite the others going - // through. One-at-a-time with a small gap keeps every upload under the limit. - uploadCh chan uploadJob } -// uploadJob is one queued auto-upload. -type uploadJob struct { - svc Service - id int64 - cfg ServiceConfig - attempt int // 0 on first try; incremented on each retry -} - -// uploadGap spaces serialized uploads so a burst never trips a service's per-IP -// rate limiter. maxUploadAttempts bounds retries of a transient failure. -const ( - uploadGap = 250 * time.Millisecond - maxUploadAttempts = 4 -) +// maxUploadAttempts bounds retries of a transient upload failure. +const maxUploadAttempts = 4 func NewManager(deps Deps) *Manager { if deps.Client == nil { deps.Client = &http.Client{Timeout: 20 * time.Second} } - m := &Manager{ + return &Manager{ deps: deps, // Seeded from the clock; the delay only needs to be unpredictable // enough to spread bursts, not cryptographically random. - rnd: rand.New(rand.NewSource(time.Now().UnixNano())), - uploadCh: make(chan uploadJob, 4096), + rnd: rand.New(rand.NewSource(time.Now().UnixNano())), } - go m.uploadWorker() - return m } -// uploadWorker drains the queue one upload at a time, spacing them so a burst of -// freshly-logged QSOs can't hammer (and get 403'd by) a service. A transient -// failure is re-queued with an exponential back-off, so a QSO that hit a -// momentary rate-limit still ends up marked instead of stuck at "R". -func (m *Manager) uploadWorker() { - for job := range m.uploadCh { - ok, retryable := m.upload(job.svc, job.id, job.cfg) - if !ok && retryable && job.attempt+1 < maxUploadAttempts { - next := job - next.attempt++ - backoff := time.Duration(1<