fix(iot): готовность consumer по SQS + ограничение wg.Wait + идемпотентный Close (Этап 3)
- consumer.Run получил onReady: /ready теперь false при ошибке ReceiveMessage (раньше флаг consumer'а выставлялся до старта и не отражал SQS). - main.go: wg.Wait заменён на waitWithTimeout (shutdownGrace=15с) — зависшую подсистему не ждём бесконечно. - iotpg.Close идемпотентен (sync.Once) — повторный вызов не паникует.
This commit is contained in:
@@ -32,8 +32,9 @@ const maxProcessBackoff = 60 * time.Second
|
||||
// уходит туда на первом же приёме.
|
||||
const maxReceiveCount = 5
|
||||
|
||||
// Run — бесконечный long-poll цикл потребителя.
|
||||
func Run(ctx context.Context, cfg *config.Config, sqsClient *sqs.Client, store *iotpg.IoTPostgresStore, log *slog.Logger) error {
|
||||
// Run — бесконечный long-poll цикл потребителя. onReady — колбэк готовности:
|
||||
// true после успешного ReceiveMessage, false при ошибке SQS (отражается в /ready).
|
||||
func Run(ctx context.Context, cfg *config.Config, sqsClient *sqs.Client, store *iotpg.IoTPostgresStore, log *slog.Logger, onReady func(bool)) error {
|
||||
queueURL, err := sqsclient.ResolveQueueURLWithRetry(ctx, sqsClient, cfg.SQSQueueName)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -80,11 +81,13 @@ func Run(ctx context.Context, cfg *config.Config, sqsClient *sqs.Client, store *
|
||||
break
|
||||
}
|
||||
log.Error("consumer: ReceiveMessage failed", "err", err)
|
||||
setReady(onReady, false)
|
||||
if !sleepCtx(ctx, errorBackoff) {
|
||||
break
|
||||
}
|
||||
continue
|
||||
}
|
||||
setReady(onReady, true)
|
||||
|
||||
processBackoff := time.Second
|
||||
// Копим подтверждения, но НЕ удаляем из SQS сразу: InsertTelemetry лишь
|
||||
@@ -205,6 +208,13 @@ func receiveCount(msg sqstypes.Message) int {
|
||||
return n
|
||||
}
|
||||
|
||||
// setReady — nil-safe вызов колбэка готовности.
|
||||
func setReady(fn func(bool), v bool) {
|
||||
if fn != nil {
|
||||
fn(v)
|
||||
}
|
||||
}
|
||||
|
||||
// sleepCtx — ожидание с прерыванием по ctx. false = ctx завершён.
|
||||
func sleepCtx(ctx context.Context, d time.Duration) bool {
|
||||
select {
|
||||
|
||||
@@ -52,6 +52,10 @@ type IoTPostgresStore struct {
|
||||
batches map[string][]telemetryInsert
|
||||
stopFlush chan struct{}
|
||||
flushWG sync.WaitGroup
|
||||
|
||||
// closeOnce — Close идемпотентен: повторный вызов не паникует на close(stopFlush)
|
||||
// (ревью 2026-10-10).
|
||||
closeOnce sync.Once
|
||||
}
|
||||
|
||||
// telemetryInsert — строка для батч-вставки.
|
||||
@@ -602,17 +606,22 @@ func isDBNotExistErr(err error) bool {
|
||||
}
|
||||
|
||||
// Close останавливает flusher, дописывает остаток и закрывает все подключения.
|
||||
// Идемпотентен (closeOnce): повторный вызов не паникует на close(stopFlush).
|
||||
func (s *IoTPostgresStore) Close() error {
|
||||
close(s.stopFlush)
|
||||
s.flushWG.Wait()
|
||||
_ = s.flushAll(context.Background())
|
||||
s.tenants.Range(func(_, value any) bool {
|
||||
if db, ok := value.(*sql.DB); ok {
|
||||
db.Close()
|
||||
}
|
||||
return true
|
||||
var err error
|
||||
s.closeOnce.Do(func() {
|
||||
close(s.stopFlush)
|
||||
s.flushWG.Wait()
|
||||
_ = s.flushAll(context.Background())
|
||||
s.tenants.Range(func(_, value any) bool {
|
||||
if db, ok := value.(*sql.DB); ok {
|
||||
db.Close()
|
||||
}
|
||||
return true
|
||||
})
|
||||
err = s.adminDB.Close()
|
||||
})
|
||||
return s.adminDB.Close()
|
||||
return err
|
||||
}
|
||||
|
||||
// getTenantDB возвращает *sql.DB для tenant DB из кэша или открывает новый.
|
||||
|
||||
Reference in New Issue
Block a user