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, ) }