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:
@@ -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,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user