fix(iot): ретраи отправки SQS до shutdown + таймаут на попытку (🔴4)

- sendWithRetry больше не дропает после 3 попыток (~7с) при почти пустом буфере:
  ретраит до ctx.Done() (shutdown) или успеха, backoff капится на 8с.
- Добавлен sendAttemptTimeout=10с на одну попытку — зависшее соединение SQS
  не блокирует воркер навсегда.
This commit is contained in:
Repinoid
2026-10-10 18:59:37 +03:00
parent ab146861d3
commit 5600646db3
+20 -11
View File
@@ -27,8 +27,13 @@ const (
dispatcherQueueSize = 10000 dispatcherQueueSize = 10000
// dispatcherWorkers — горутин, отправляющих в SQS. // dispatcherWorkers — горутин, отправляющих в SQS.
dispatcherWorkers = 4 dispatcherWorkers = 4
// sendMaxAttempts — попыток SendMessage на одно сообщение.
sendMaxAttempts = 3 // sendAttemptTimeout — таймаут ОДНОЙ попытки SendMessage. Без него зависшее
// соединение (SQS висит, но не отвечает) блокировало бы воркера навсегда и
// забило бы весь пул (ревью 🔴4).
sendAttemptTimeout = 10 * time.Second
// sendMaxBackoff — потолок паузы между попытками (экспоненциальный backoff).
sendMaxBackoff = 8 * time.Second
// maxSQSBodyBytes — потолок размера envelope для SQS. // maxSQSBodyBytes — потолок размера envelope для SQS.
// SQS (и shared-sqs) режут сообщения >256KB — запас на оверхед. // SQS (и shared-sqs) режут сообщения >256KB — запас на оверхед.
@@ -116,30 +121,34 @@ func (d *sqsDispatcher) worker(ctx context.Context, wid int) {
} }
} }
// sendWithRetry — SendMessage с backoff 1с/2с/4с. // sendWithRetry — SendMessage с ретраями до остановки (ctx) и таймаутом на
// попытку. НЕ дропаем после фиксированного числа попыток (было 3 ≈ 7с): буфер
// на dispatcherQueueSize рассчитан на поглощение временной недоступности SQS,
// поэтому ретраим до ctx.Done() (shutdown) или успеха — иначе потеря была бы
// безвозвратной (ревью 🔴4).
func (d *sqsDispatcher) sendWithRetry(ctx context.Context, body string) error { func (d *sqsDispatcher) sendWithRetry(ctx context.Context, body string) error {
var lastErr error
backoff := time.Second backoff := time.Second
for attempt := 1; attempt <= sendMaxAttempts; attempt++ { for {
_, err := d.client.SendMessage(ctx, &sqs.SendMessageInput{ attemptCtx, cancel := context.WithTimeout(ctx, sendAttemptTimeout)
_, err := d.client.SendMessage(attemptCtx, &sqs.SendMessageInput{
QueueUrl: aws.String(d.queueURL), QueueUrl: aws.String(d.queueURL),
MessageBody: aws.String(body), MessageBody: aws.String(body),
}) })
cancel()
if err == nil { if err == nil {
return nil return nil
} }
lastErr = err
if ctx.Err() != nil { if ctx.Err() != nil {
return lastErr // shutdown: ctx отменён, ретраить бессмысленно.
return err
} }
if attempt < sendMaxAttempts {
select { select {
case <-ctx.Done(): case <-ctx.Done():
return lastErr return err
case <-time.After(backoff): case <-time.After(backoff):
} }
if backoff < sendMaxBackoff {
backoff *= 2 backoff *= 2
} }
} }
return lastErr
} }