fix(iot): пагинация — tiebreaker ts DESC,id DESC + потолок limit; точный лог (Этап 5)

- QueryTelemetry: ORDER BY ts DESC, id DESC (был без tiebreaker → нестабильная
  пагинация при одинаковых ts).
- ListIoTDevices: limit дефолт 100, потолок 1000 (ошибка парсинга больше не даёт
  безлимитный запрос).
- Логи 'batch dropped' → 'валидные строки — построчно' (после fallback батч НЕ
  теряется целиком).
This commit is contained in:
Repinoid
2026-10-10 19:17:25 +03:00
parent f3927fdf11
commit 38e3f65c21
2 changed files with 22 additions and 10 deletions
+15 -3
View File
@@ -221,15 +221,27 @@ func (h *Handler) CreateIoTDevice(w http.ResponseWriter, r *http.Request) {
} }
// ListIoTDevices — GET /v1/namespaces/{ns}/iot/devices (без паролей). // 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) { func (h *Handler) ListIoTDevices(w http.ResponseWriter, r *http.Request) {
ns := pathVar(r, "namespace") ns := pathVar(r, "namespace")
if ns == "" { if ns == "" {
ns = "default" ns = "default"
} }
limit, _ := strconv.Atoi(r.URL.Query().Get("limit")) // limit: дефолт 100, потолок 1000 — иначе ошибка парсинга/омиссия = безлимит
offset, _ := strconv.Atoi(r.URL.Query().Get("offset")) // (ревью 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) devices, err := h.Devices.List(r.Context(), ns, limit, offset)
if err != nil { if err != nil {
@@ -171,10 +171,10 @@ func (s *IoTPostgresStore) flushAll(ctx context.Context) error {
s.batches[ns] = append(s.batches[ns], rows...) s.batches[ns] = append(s.batches[ns], rows...)
s.mu.Unlock() s.mu.Unlock()
} else { } else {
// Постоянная ошибка (неверные данные/constraint) — повтор бессмыслен: // Постоянная ошибка: валидные строки уже вставлены построчным
// ВЫБРАСЫВАЕМ батч, иначе он зависнет в буфере навсегда и заблокирует // fallback'ом (см. flushTenant), битые отброшены с логом. Повтор
// весь namespace (вечные 200мс-ретраи). Логируем подробно. // бессмыслен — в буфер НЕ возвращаем (иначе залипание namespace).
s.log.Error("iotpg: permanent flush error, batch dropped", "namespace", ns, "rows", len(rows), "err", err) 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.batches[namespace] = append(s.batches[namespace], rows...)
s.mu.Unlock() s.mu.Unlock()
} else { } 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() s.flushMu.Unlock()
@@ -473,13 +473,13 @@ func (s *IoTPostgresStore) QueryTelemetry(ctx context.Context, namespace, device
if deviceID != "" { if deviceID != "" {
rows, err = tenantDB.QueryContext(ctx, rows, err = tenantDB.QueryContext(ctx,
`SELECT id, device_id, ts, payload FROM iot_telemetry `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, deviceID, limit, offset,
) )
} else { } else {
rows, err = tenantDB.QueryContext(ctx, rows, err = tenantDB.QueryContext(ctx,
`SELECT id, device_id, ts, payload FROM iot_telemetry `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, limit, offset,
) )
} }