From 38e3f65c21881e158bd3b428d9dd83d7209a9f31 Mon Sep 17 00:00:00 2001 From: Repinoid Date: Sat, 10 Oct 2026 19:17:25 +0300 Subject: [PATCH] =?UTF-8?q?fix(iot):=20=D0=BF=D0=B0=D0=B3=D0=B8=D0=BD?= =?UTF-8?q?=D0=B0=D1=86=D0=B8=D1=8F=20=E2=80=94=20tiebreaker=20ts=20DESC,i?= =?UTF-8?q?d=20DESC=20+=20=D0=BF=D0=BE=D1=82=D0=BE=D0=BB=D0=BE=D0=BA=20lim?= =?UTF-8?q?it;=20=D1=82=D0=BE=D1=87=D0=BD=D1=8B=D0=B9=20=D0=BB=D0=BE=D0=B3?= =?UTF-8?q?=20(=D0=AD=D1=82=D0=B0=D0=BF=205)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - QueryTelemetry: ORDER BY ts DESC, id DESC (был без tiebreaker → нестабильная пагинация при одинаковых ts). - ListIoTDevices: limit дефолт 100, потолок 1000 (ошибка парсинга больше не даёт безлимитный запрос). - Логи 'batch dropped' → 'валидные строки — построчно' (после fallback батч НЕ теряется целиком). --- internal/service/api/handler/devices.go | 18 +++++++++++++++--- internal/storage/iotpg/iot_telemetry_store.go | 14 +++++++------- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/internal/service/api/handler/devices.go b/internal/service/api/handler/devices.go index c4bcc13..467e3da 100644 --- a/internal/service/api/handler/devices.go +++ b/internal/service/api/handler/devices.go @@ -221,15 +221,27 @@ func (h *Handler) CreateIoTDevice(w http.ResponseWriter, r *http.Request) { } // ListIoTDevices — GET /v1/namespaces/{ns}/iot/devices (без паролей). -// Пагинация: ?limit=N&offset=M (0 = без ограничения). +// Пагинация: ?limit=N&offset=M. limit — дефолт 100, потолок 1000 (защита от +// безлимитного запроса при ошибке парсинга). func (h *Handler) ListIoTDevices(w http.ResponseWriter, r *http.Request) { ns := pathVar(r, "namespace") if ns == "" { ns = "default" } - limit, _ := strconv.Atoi(r.URL.Query().Get("limit")) - offset, _ := strconv.Atoi(r.URL.Query().Get("offset")) + // limit: дефолт 100, потолок 1000 — иначе ошибка парсинга/омиссия = безлимит + // (ревью 2026-10-10). + limit := 100 + if v, err := strconv.Atoi(r.URL.Query().Get("limit")); err == nil && v > 0 { + limit = v + } + if limit > 1000 { + limit = 1000 + } + offset := 0 + if v, err := strconv.Atoi(r.URL.Query().Get("offset")); err == nil && v > 0 { + offset = v + } devices, err := h.Devices.List(r.Context(), ns, limit, offset) if err != nil { diff --git a/internal/storage/iotpg/iot_telemetry_store.go b/internal/storage/iotpg/iot_telemetry_store.go index e2d85c3..dcc79ac 100644 --- a/internal/storage/iotpg/iot_telemetry_store.go +++ b/internal/storage/iotpg/iot_telemetry_store.go @@ -171,10 +171,10 @@ func (s *IoTPostgresStore) flushAll(ctx context.Context) error { s.batches[ns] = append(s.batches[ns], rows...) s.mu.Unlock() } else { - // Постоянная ошибка (неверные данные/constraint) — повтор бессмыслен: - // ВЫБРАСЫВАЕМ батч, иначе он зависнет в буфере навсегда и заблокирует - // весь namespace (вечные 200мс-ретраи). Логируем подробно. - s.log.Error("iotpg: permanent flush error, batch dropped", "namespace", ns, "rows", len(rows), "err", err) + // Постоянная ошибка: валидные строки уже вставлены построчным + // fallback'ом (см. flushTenant), битые отброшены с логом. Повтор + // бессмыслен — в буфер НЕ возвращаем (иначе залипание namespace). + s.log.Error("iotpg: permanent flush error (валидные строки — построчно, битые отброшены)", "namespace", ns, "rows", len(rows), "err", err) } } } @@ -412,7 +412,7 @@ func (s *IoTPostgresStore) InsertTelemetry(ctx context.Context, namespace, devic s.batches[namespace] = append(s.batches[namespace], rows...) s.mu.Unlock() } else { - s.log.Error("iotpg: permanent error, batch dropped", "namespace", namespace, "rows", len(rows), "err", err) + s.log.Error("iotpg: permanent error (валидные строки — построчно, битые отброшены)", "namespace", namespace, "rows", len(rows), "err", err) } } s.flushMu.Unlock() @@ -473,13 +473,13 @@ func (s *IoTPostgresStore) QueryTelemetry(ctx context.Context, namespace, device if deviceID != "" { rows, err = tenantDB.QueryContext(ctx, `SELECT id, device_id, ts, payload FROM iot_telemetry - WHERE device_id = $1 ORDER BY ts DESC LIMIT $2 OFFSET $3`, + WHERE device_id = $1 ORDER BY ts DESC, id DESC LIMIT $2 OFFSET $3`, deviceID, limit, offset, ) } else { rows, err = tenantDB.QueryContext(ctx, `SELECT id, device_id, ts, payload FROM iot_telemetry - ORDER BY ts DESC LIMIT $1 OFFSET $2`, + ORDER BY ts DESC, id DESC LIMIT $1 OFFSET $2`, limit, offset, ) }