Files
sless/doc/thinking/2026-04-06.md
T

31 KiB
Raw Blame History

Thinking Log — 2026-04-06

Агент: GitHub Copilot (Claude Sonnet 4.6)


Архитектурные обсуждения перед началом Kafka

Контекст

Пользователь обсуждал будущую prod-архитектуру IoT сервиса. Никакого кода не менялось — чистое планирование.

Итоги обсуждений

Три отдельных кластера (принято):

  1. IoT кластер — EMQX, bridge, Kafka, iot-consumer, Postgres, REST API
  2. Serverless кластер — operator, builder, event-dispatcher, Functions
  3. Infra/Control кластер — Terraform для provisioning кластеров 1 и 2, DNS, TLS, auth, billing

Это классическая схема "control plane отдельно от data plane".

Kafka — выбор подтверждён:

  • Сейчас: bridge → Postgres напрямую (синхронно, без буфера)
  • Prod: bridge → Kafka → {consumer → Postgres, event-dispatcher → Functions}
  • Dev/test: Kafka через Helm (bitnami, KRaft mode, 1 нод, PVC)
  • Prod: managed Kafka (Confluent/Aiven) — только меняется KAFKA_BROKERS в Secret

Postgres → managed облачный: легко

  • bridge и API используют DATABASE_URL из env
  • Для переключения: только заменить Secret в кластере
  • Код не трогается

Состояние RabbitMQ для IoT (важное открытие):

  • Bridge сейчас пишет в RabbitMQ очередь iot.{namespace}.telemetry
  • НО event-dispatcher эту очередь не читает — он настроен на serverless functions triggers
  • То есть IoT-сообщения в RabbitMQ лежат мёртвым грузом — никто не читает
  • Kafka заменяет RabbitMQ для IoT-части полностью

Что проверяли в кластере:

  • 2026-04-05: только один активный тенант sless-16367aacb67a4a01 (созданный после инцидента)
  • Устройство device2, одно сообщение: {"msg":"hello1dddd1777"} от 14:34 UTC
  • 2026-04-06: kubeconfig истёк → обновил → тот же один тенант, никто новый не входил

План интеграции Kafka

Анализ текущего bridge

Читал iot/cmd/mqtt-bridge/main.go. Текущая логика в buildMQTTMessageHandler:

  1. Получает MQTT сообщение
  2. Публикует в RabbitMQ (бесполезно — никто не читает)
  3. Пишет напрямую в Postgres через iotpg.Store

С Kafka нужно:

  1. Получает MQTT сообщение
  2. Публикует в Kafka топик iot.telemetry (единый топик, namespace в payload)
  3. Убрать прямой INSERT в Postgres из bridge

Что создаётся заново

iot/cmd/kafka-consumer/main.go — новый сервис:

  • Читает из Kafka топика iot.telemetry
  • Пишет в Postgres (та же логика что сейчас в bridge)
  • Consumer group: iot-pg-consumer

Изменения в bridge:

  • Убрать RabbitMQ
  • Добавить Kafka producer (библиотека github.com/segmentio/kafka-go)
  • Env var: KAFKA_BROKERS вместо RABBITMQ_URL

Новые env vars:

  • bridge: KAFKA_BROKERS=kafka.sless.svc.cluster.local:9092
  • consumer: KAFKA_BROKERS=..., IOT_PG_DSN=...

Что НЕ меняется

  • EMQX, operator, REST API, IoT Console — не трогаются
  • iotpg storage package — используется consumer-ом напрямую
  • ACL, auth, namespace-изоляция — не меняются

Порядок работы

  1. Документация + коммит (сейчас)
  2. Ветка iot-kafka
  3. Helm: установить Kafka в namespace sless
  4. Переписать bridge: убрать RabbitMQ, добавить Kafka producer
  5. Создать iot/cmd/kafka-consumer/main.go
  6. Обновить Dockerfile (добавить сборку consumer)
  7. Обновить deployment манифесты
  8. Сборка v0.1.67, деплой, тест

Риски

  • kafka-go vs confluent-kafka-go — выбираем segmentio/kafka-go (pure Go, без CGO, совместим с alpine)
  • KRaft mode в Helm bitnami — убедиться что включён (без Zookeeper)
  • Topic iot.telemetry — создаётся автоматически при первой публикации (auto.create.topics.enable=true по умолчанию)

Сессия (продолжение) — реализация Kafka pipeline

Что было сделано

Ветка: iot-kafka

1. Kafka StatefulSet (deployments/k8s/kafka.yaml)

Установка через Helm bitnami провалилась — образ bitnami/kafka:4.0.0 заблокирован (paywall с Aug 2025). Переключились на официальный apache/kafka:3.7.0 — бесплатный, полнофункциональный.

Написан кастомный kafka.yaml:

  • KRaft mode (без Zookeeper) — node.id=1, roles=broker+controller
  • ConfigMap монтируется в /tmp/kafka-config (не /etc/kafka — read-only в образе)
  • securityContext.fsGroup=1000 — kafka user (UID 1000) может писать в PVC
  • PVC 1Gi на vcd-disk-ext4 (local-path отказал: not enough disk space)
  • Два Service: kafka:9092 и headless kafka-headless

2. bridge переписан (iot/cmd/mqtt-bridge/main.go)

  • Убран RabbitMQ (amqp091-go)
  • Убрана прямая запись в Postgres через iotpg
  • Добавлен Kafka writer (segmentio/kafka-go)
  • Топик: iot.telemetry, ключ = namespace (партиционирование по тенанту)
  • Async: false, RequiredAcks: RequireOne — синхронная запись, подтверждение от лидера

3. kafka-consumer создан (iot/cmd/kafka-consumer/main.go)

  • Consumer group: iot-pg-consumer
  • Читает из iot.telemetry, пишет в Postgres через iotpg.Store
  • Offset коммитится ТОЛЬКО после успешной записи (at-least-once)
  • Retry loop при недоступности Kafka

4. Dockerfile обновлён

  • Добавлена сборка iot-kafka-consumer бинаря
  • COPY --from=builder /workspace/iot-kafka-consumer .
  • Итого в образе 3 бинаря: manager, iot-mqtt-bridge, iot-kafka-consumer

5. Манифесты обновлены

  • iot-mqtt-bridge.yaml: убран RABBITMQ_URL, добавлен KAFKA_BROKERS
  • iot-kafka-consumer.yaml: новый deployment

Баги которые встретили и решили

Bug 1: дублирующий package main

create_file вставил package main дважды — в начале и перед import. Фикс: replace_string_in_file удалил дубликат.

Bug 2: kafka-go помечен как // indirect в go.mod

gopls не видел пакет как доступный. Причина: зависимость добавлена без прямого импорта в момент добавления. Фикс: go mod tidy убрал // indirect.

Bug 3: Race condition — consumer зависал при холодном старте

Когда: consumer стартовал одновременно с Kafka (первый деплой, топика нет). Что происходило: consumer JOIN-ил group → Kafka auto-создавала топик в момент JOIN → kafka-go зависал на FetchMessage навсегда. Гипотеза №1: postStart lifecycle hook на Kafka — создать топик сразу после старта брокера. Проблема с гипотезой: kafka-topics.sh --list без таймаута зависает бесконечно → pod застрял в PodInitializing. Попытка с ncnc не установлен в образе. Попытка с request.timeout.ms через properties — postStart возвращал exit code 1 → Kubernetes убивал контейнер → CrashLoopBackOff. Итоговое решение: ensureKafkaTopic() в consumer — создаёт топик через kafka.DialContext + conn.CreateTopics() ДО создания Reader и JOIN группы. Retry 30 раз × 3 сек = 90 сек макс ожидания.

// Порядок в consumer:
// 1. Connect IoT Postgres
// 2. ensureKafkaTopic() ← создаём топик, ждём брокер
// 3. kafka.NewReader()  ← только теперь join group
// 4. FetchMessage() loop

Почему это решение правильное: race исключён на уровне приложения, не инфраструктуры. Даже если kafka.yaml не имеет никакого init — consumer сам дождётся Kafka и создаст топик.

Bug 4: CrashLoopBackOff после force delete pod-а

Force delete оставил .lock файл на PVC. Kafka падала с: Failed to acquire lock on file .lock in /var/kafka-data/logs Фикс: удалить StatefulSet + PVC (kubectl delete statefulset kafka && kubectl delete pvc kafka-data-kafka-0), пересоздать.

Урок: НИКОГДА не делать kubectl delete pod --force для stateful pod-ов. Только graceful (kubectl delete pod, подождать). Force delete = гарантированная поломка PVC.


Результаты тестирования (v0.1.68)

Тест Условие Результат
Cold start consumer стартует раньше Kafka ensureKafkaTopic ретраится, дожидается
5 рестартов consumer Kafka работает каждый раз kafka topic ready
MQTT → Pipeline device2, 1 сообщение offset=0 в Postgres
Рестарт Kafka consumer живёт ретраится с ERROR fetch, восстанавливается
10 сообщений параллельно 10 pod-ов mosquitto offsets 2-11 все в Postgres

Что НЕ тестировалось:

  • Полный холодный старт с нуля (kubectl apply -f на чистый кластер)
  • Consumer стартует одновременно с Kafka (оба новые) — race condition исправлен кодом, но на новом кластере не проверялся

Текущее состояние кластера (2026-04-06 ~17:30 МСК)

sless-operator:v0.1.68 — Running
kafka-0              — Running (после удаления PVC и пересоздания)
iot-mqtt-bridge      — Running, подключён к EMQX и Kafka
iot-kafka-consumer   — Running, waiting for messages
iot-postgres         — Running

Тенант: sless-16367aacb67a4a01, устройство device2. В IoT Postgres: 12+ записей телеметрии (offsets 0-11).


Что нужно сделать ещё

  1. Тест: полный холодный старт — удалить kafka + consumer + PVC, применить всё одновременно, убедиться что race не вылезает
  2. Helm chart — параметризовать KAFKA_BROKERS, IOT_PG_DSN, тег образа, StorageClass для values-dev.yaml / values-prod.yaml
  3. Managed Kafka/Postgres — при переходе только менять values-prod.yaml
  4. Merge iot-kafka в main — после тестов

Архитектурные выводы сессии

Будущая prod-архитектура (принято):

  • 3 кластера: IoT / Serverless / Infra-Control
  • Managed Kafka + Managed Postgres (переключение через env vars, код не меняется)
  • Helm chart для параметризации per-environment

Текущий статус пути данных:

IoT Device
  → MQTT PUBLISH
  → EMQX (sless namespace)
  → iot-mqtt-bridge (подписан на +/telemetry/+)
  → Kafka топик iot.telemetry (key=namespace)
  → iot-kafka-consumer (group iot-pg-consumer)
  → IoT Postgres (per-tenant schema через EnsureTenantDB)
  → GET /v1/{ns}/iot/telemetry (IoT Console)

Полное суровое тестирование IoT pipeline (2026-04-06, вечер)

Агент: GitHub Copilot (Claude Sonnet 4.6)

Исходное состояние

  • Все поды Running: kafka-0, iot-kafka-consumer, iot-mqtt-bridge, iot-postgres, emqx
  • Baseline: 18 строк в iot_telemetry (tenant_sless_16367aacb67a4a01)
  • Образ: v0.1.68, ветка iot-kafka

Тест-окружение

MQTT broker:   emqx.sless.svc.cluster.local:1883
MQTT user:     sless-16367aacb67a4a01_device2
MQTT topic:    sless-16367aacb67a4a01/telemetry/device2
Kafka topic:   iot.telemetry
Consumer group: iot-pg-consumer
Postgres DB:   tenant_sless_16367aacb67a4a01, таблица iot_telemetry

TEST 1: Cold Start — удаление ВСЕХ IoT подов одновременно

Сценарий: kubectl delete pod kafka-0 iot-kafka-consumer iot-mqtt-bridge

Ожидание: consumer дождётся Kafka через ensureKafkaTopic(), поднимется без паники.

Что произошло:

  • kafka-0 поднялся через ~40с (StatefulSet, PVC сохранился)
  • consumer запустился, попал в retry loop ensureKafkaTopic():
    • 16 попыток × 3с = ~48с ждал пока Kafka полностью инициализируется
    • Logged: "kafka not reachable yet, retrying..." attempt=1..16
    • На попытке 16: "kafka topic ready" → "kafka reader ready, waiting for messages..."
  • bridge поднялся за <5с (stateless)

Верификация E2E: отправлен 1 MQTT сообщение → id=19 с {"test":"cold_start"} появился в Postgres

Результат: PASS


TEST 2: Restart resilience — 3 принудительных рестарта consumer

Сценарий: 3 раза kubectl delete pod iot-kafka-consumer --grace-period=0 подряд

Результат каждого рестарта:

  • Restart 1: pod recreated, logged "starting iot-kafka-consumer"
  • Restart 2: "connected to IoT Postgres" + "kafka topic ready" + "kafka reader ready" — <1с
  • Restart 3: "starting iot-kafka-consumer" — <1с

Ключевое наблюдение: когда Kafka уже running, ensureKafkaTopic() проходит мгновенно (first attempt succeeds). Никакого зависания.

Результат: PASS — начало работы после рестарта: <1с


TEST 3: Load 100 сообщений — КРИТИЧЕСКОЕ ОТКРЫТИЕ

Сценарий: for i in 1..100; do mosquitto_pub ...; done из ephemeral pod

Ожидание: ≥100 строк в Postgres за ~2 мин

Что произошло:

  • Цикл mosquitto_pub завершился быстро (каждый вызов QoS 0: connect+publish+disconnect)
  • Все 100 сообщений упали в EMQX
  • Bridge начал доставку в Kafka — при этом каждый WriteMessages СИНХРОННЫЙ блокирует ~1с
  • Bridge обрабатывает 1 сообщение/сек (throughput bottleneck!)
  • После 27 доставок (25с): EMQX keepalive timeout → bridge потерял MQTT-соединение (pingresp not received)
  • Bridge переподключился через 28мс (CleanSession=false)
  • НО: устройства публиковали QoS 0 → EMQX не хранит un-ACK сообщения QoS 0 → 73 сообщения ПОТЕРЯНЫ безвозвратно

Итог: в Postgres попало только 27/100 сообщений

Корень проблемы — архитектурный недостаток:

Kafka.Writer{Async: false}  ← каждый WriteMessages блокирует на ACK от Kafka
mosquitto_pub QoS 0         ← EMQX не хранит для оффлайн подписчиков
= при burst load потери гарантированы

Что нужно исправить (FIX backlog):

  1. kafka.Writer{Async: true} в bridge — не блокировать MQTT loop
  2. Устройства должны публиковать QoS ≥ 1 для гарантированной доставки
  3. Или увеличить keepalive timeout в bridge

Результат: ⚠️ PARTIAL FAIL — 27/100 msg. Функционально работает, но не масштабируется без фикса.


TEST 4: Burst при оффлайн consumer (Kafka buffering)

Сценарий:

  1. kubectl scale deploy iot-kafka-consumer --replicas=0 (consumer offline)
  2. Отправить 10 сообщений через MQTT
  3. Проверить что в Postgres 0 новых строк (Kafka буферизует)
  4. kubectl scale --replicas=1 → consumer поднялся
  5. Проверить что все 10 дошли

Что произошло:

  • Consumer scaled to 0
  • Sent 10 msgs → bridge forwarded все 10 в Kafka (bridge работает независимо от consumer)
  • Postgres: 0 новых строк (consumer offline, данные в Kafka)
  • Consumer поднялся → "kafka topic ready" в <1с
  • Все 10 сообщений обработаны за <300мс (offsets 39-48 в одном flush)

Ключевое наблюдение: когда Kafka имеет накопленные сообщения, consumer читает их пачками (не 1/сек). Bottleneck 1/сек — только при live доставке через bridge.

Результат: PASS — Kafka держит сообщения при оффлайн consumer, доставка после старта мгновенная.


TEST 5: Невалидные сообщения

Сценарий: отправить 3 типа "невалидного" payload:

  1. {not:valid:json — невалидный JSON
  2. Пустое сообщение (-n flag)
  3. plain text payload — просто строка

Что произошло:

  • Bridge получил все 3 через MQTT
  • Bridge код: if !json.Valid(payload) { quotedBytes, _ := json.Marshal(string(payload)) } — оборачивает non-JSON в JSON строку
  • Конверсия:
    • {not:valid:json"{not:valid:json" (JSON string)
    • пустое → "" (пустая JSON строка)
    • plain text payload"plain text payload" (JSON string)
  • Consumer получил 3 валидных envelope, не увидел WARNов, все 3 записи сохранились в Postgres
  • Consumer: статус Running, никаких крашей, никаких ошибок

Что записалось в Postgres (id=56,57,58):

56 | "{not:valid:json"
57 | ""
58 | "plain text payload"

Результат: PASS — система gracefully обрабатывает любой payload, не крашится.


TEST 6: Дублированные сообщения (at-least-once delivery)

Сценарий: отправить одно и то же сообщение {test:duplicate, value:42} 3 раза

Ожидание: 3 отдельные записи (at-least-once, нет дедупликации)

Что произошло: ровно 3 строки id=59,60,61 с одинаковым payload в Postgres

Это ожидаемое поведение. Система не deduplicate по умолчанию.

Результат: PASS (ожидаемое поведение)


TEST 7: Kafka недоступна — убить kafka-0

Сценарий:

  1. kubectl delete pod kafka-0 --grace-period=0
  2. Отправить 2 сообщения: a. kafka_down — пока Kafka недоступна b. after_kafka_restart — после восстановления

Что произошло:

Bridge реакция на Kafka downtime:

  • При попытке WriteMessages → dial tcp 10.104.151.227:9092: connect: operation not permitted
  • 1 ERROR в логе, сообщение kafka_down ПОТЕРЯНО (нет retry, нет local buffer)
  • kafka-go Writer автоматически переподключается

Consumer реакция:

  • При попытке FetchMessage → серия ERROR: connection refused, затем operation not permitted
  • Retry через continue в цикле (немедленный retry, не exponential backoff)
  • Kafka запустилась через ~2 мин — consumer начал получать ошибки "operation not permitted" (KRaft init)
  • Через ~3 мин total: consumer переподключился автоматически

Сообщение after_kafka_restart:

  • Bridge успешно forwarded в Kafka (15:11:11)
  • Consumer прочитал и сохранил в Postgres (offset=55, 15:11:12)

Результат: PASS с замечаниями:

  • 1 сообщение потеряно при bridge Kafka error (нет retry — это FIX backlog)
  • Recovery time: ~3 мин (Kafka init ~2мин + consumer reconnect ~1мин)
  • После recovery: система работает нормально

Итоговая таблица тестов

# Тест Статус Примечание
1 Cold start (все поды) PASS 48с ожидание Kafka (16 retry × 3с)
2 Restart resilience (3×) PASS <1с при running Kafka
3 Load 100 msgs ⚠️ PARTIAL FAIL 27/100 доставлено. Архит. баг: Async=false + QoS 0
4 Burst при offline consumer PASS Kafka держит, consumer обработал 10 за <300мс
5 Невалидные сообщения (3 типа) PASS Bridge оборачивает, consumer не крашится
6 Дубликаты PASS at-least-once, 3×identical→3 rows
7 Kafka restart (network drop) PASS Recovery ~3мин автоматически, 1 msg lost

Критические находки (требуют fix)

FINDING #1: Bridge throughput bottleneck — ~1 msg/сек

Причина: kafka.Writer{Async: false} = каждый WriteMessages ждёт ACK от Kafka (~1с/msg) Симптом: MQTT keepalive timeout → disconnect → QoS 0 loss Fix: kafka.Writer{Async: true, ErrorLogger: ...} c обработкой ошибок Приоритет: HIGH (потеря данных при burst)

FINDING #2: QoS 0 от устройств = no durability при bridge disconnect

Причина: mosquitto_pub без флага -q = QoS 0 = EMQX fire-and-forget Симптом: при кратком bridge disconnect (28мс!) теряются непрочитанные сообщения Fix: устройства должны публиковать с QoS 1 (-q 1 в mosquitto_pub) Приоритет: HIGH (потеря данных)

FINDING #3: Bridge не retry при Kafka error

Причина: нет retry logic в buildMQTTMessageHandler Симптом: 1 сообщение потеряно при Kafka restart Fix: local message buffer + retry с exponential backoff Приоритет: MEDIUM

FINDING #4: Consumer retry на Kafka error — немедленный (no backoff)

Причина: continue в цикле после ошибки = busy-wait Симптом: срабатывает редко, но при длительном Kafka downtime = CPU waste Fix: time.Sleep(min(retryCount*100ms, 30s)) перед continue Приоритет: LOW


Состояние системы после тестов

Postgres:     62 строки в iot_telemetry (было 18)
Kafka offset: 55 (последний обработанный)
All pods:     Running
Consumer:     iot-kafka-consumer-577f7ff88d-pkqd8, Running, 0 restarts
Bridge:       iot-mqtt-bridge-7dc87c46bc-tqjgz, Running, 0 restarts
kafka-0:      Running, 4 мин (перезапускался в TEST 7)

Fix: v0.1.69 — Kafka write async (2026-04-06, после тестирования)

Агент: GitHub Copilot (Claude Sonnet 4.6)

Проблема, выявленная тестом #3

При load test 100 сообщений выяснилось: 27/100 доставлено.

Первичная диагностика показала throughput ~1 msg/сек — я объяснил это "bottleneck bridge" и записал в backlog. Но пользователь указал: это не backlog, это архитектурная ошибка. Между звеньями pipeline не должно быть ничего синхронного.

Анализ root cause

MQTT callback (paho.mqtt.golang) вызывается синхронно в своём goroutine.
Если callback долго выполняется — следующие входящие MQTT сообщения накапливаются.
При Async=false: WriteMessages блокируется до получения ACK от Kafka (~1-10мс в норме,
но при burst + latency spike → сотни мс → EMQX keepalive timeout = disconnect).

Цепочка событий при burst:

  1. 100 сообщений за <100мс влетают в EMQX
  2. Bridge получает первое, вызывает WriteMessages (blocking ~1с)
  3. Пока bridge заблокирован — EMQX keepalive не получает pingresp
  4. После 30с (keepalive): EMQX разрывает соединение
  5. Сообщения QoS 0, которые не были получены bridge — испаряются

Решение

kafka.Writer{Async: true} — WriteMessages возвращается немедленно, Kafka batching работает в фоновом goroutine внутри kafka-go. Ошибки доставки идут в ErrorLogger, который логирует без блокировки MQTT loop.

Почему не нужен отдельный channel/goroutine в handler:
kafka-go с Async: true уже внутри держит буфер и горутину записи. Добавлять ещё один слой buffering — overengineering без причины.

Что изменено в коде (v0.1.69)

iot/cmd/mqtt-bridge/main.go:

// ДО (v0.1.68) — НЕПРАВИЛЬНО:
kafkaWriter := &kafka.Writer{
    Async: false,  // блокирует MQTT callback до ACK Kafka
}
// в handler:
err = w.WriteMessages(ctx, ...)  // блокировка ~1с/msg

// ПОСЛЕ (v0.1.69) — ПРАВИЛЬНО:
kafkaWriter := &kafka.Writer{
    Async: true,   // WriteMessages возвращается немедленно
    ErrorLogger: kafka.LoggerFunc(func(msg string, args ...interface{}) {
        log.Error("kafka async write error", ...)  // ошибки не блокируют MQTT
    }),
}
// в handler:
_ = w.WriteMessages(ctx, ...)  // немедленный возврат, доставка в фоне

Deployment manifests

Оба yaml обновлены: v0.1.68v0.1.69:

  • deployments/k8s/iot-mqtt-bridge.yaml
  • deployments/k8s/iot-kafka-consumer.yaml

Что ожидаем после фикса

  • MQTT callback завершается за <1мс (только marshal JSON + WriteMessages enqueue)
  • Bridge не теряет keepalive с EMQX при burst
  • Throughput: лимитируется сетью/Kafka, а не синхронным write (~тысячи msg/сек)
  • Load test 100 сообщений: должны дойти все 100

Re-test v0.1.69 — полный прогон 8 тестов

Дата: 2026-04-06 (продолжение сессии)
Базовое состояние: 163 строки в DB перед стартом повторного прогона

T1: Cold start

  • Consumer pod ждал Kafka: 15 retry × 3с = 45с
  • kafka topic ready → msg id=163 появился в DB
  • PASS

T2: Restart 3×

  • 3 последовательных kubectl delete pod по consumer
  • Каждый перезапуск < 1с до kafka topic ready
  • PASS

T3: Load 100 msgs (главный — здесь был баг)

  • Baseline: 163. Отправлено: 100. Результат в DB: +100 (итого 263)
  • v0.1.68 давал 27/100. v0.1.69: 100/100
  • PASS ← баг исправлен

T4: Burst при offline consumer

  • Baseline: 263. Consumer масштабирован в 0 → отправлено 20 msgs → DB +0 (consumer offline)
  • Consumer поднят обратно → через 15с: DB +20
  • Kafka буферизовал все 20 сообщений, consumer догнал сразу
  • PASS

T5: Невалидные payload

  • Отправлено: non-JSON строка, пустая строка, валидный JSON
  • DB: +3 строки (bridge оборачивает non-JSON в {"raw": "..."})
  • Consumer пережил 0 crashes
  • PASS

T6: Дубликаты (at-least-once)

  • Baseline: 286. 3 идентичных сообщения {"test":"t6_dup","value":42}
  • DB: +3 строки (каждый инстанс сохранён)
  • Семантика at-least-once подтверждена
  • PASS

T7: Kafka restart

  • Baseline: 289. Kafka pod kafka-0 убит → 5 msgs отправлены во время рестарта
  • Kafka восстановился: pod/kafka-0 condition met
  • 5 msgs после восстановления: все дошли. Итого DB +5
  • Msgs во время рестарта потеряны — ожидаемо (QoS 0 / async writer без буфера во время outage)
  • PASS (recovery автоматический, post-recovery 100%)

T8: Load 1000 msgs (суровый)

  • Baseline: 294. 1000 msgs burst за 56 секунд
  • DB: +1000 (итого 1294)
  • 1000/1000 = 100%
  • PASS

Итог v0.1.69

Тест v0.1.68 v0.1.69
T1 Cold start PASS PASS
T2 Restart 3× PASS PASS
T3 Load 100 27/100 100/100
T4 Offline burst PASS PASS
T5 Invalid payload PASS PASS
T6 Duplicates PASS PASS
T7 Kafka restart PASS PASS
T8 Load 1000 — (новый) 1000/1000

Вывод: Async fix полностью решил проблему потерь. Система стабильна на нагрузке 1000 msgs.