fix: kafka race condition — ensureKafkaTopic in consumer before group join (v0.1.68)

This commit is contained in:
Naeel
2026-04-06 17:31:19 +03:00
parent 63f834da2b
commit 07ada8e362
7 changed files with 59 additions and 13 deletions
+41
View File
@@ -21,6 +21,7 @@ import (
"os/signal"
"strings"
"syscall"
"time"
kafka "github.com/segmentio/kafka-go"
@@ -71,6 +72,11 @@ func main() {
defer iotStore.Close()
log.Info("connected to IoT Postgres")
// Предсоздаём топик ДО присоединения к consumer group.
// Это устраняет race condition в kafka-go: если consumer joinит группу в момент
// когда топик auto-создаётся — kafka-go зависает. Явное создание до Join это исключает.
ensureKafkaTopic(ctx, cfg.KafkaBrokers, log)
// Kafka reader с consumer group — автоматически коммитит offsets после обработки
reader := kafka.NewReader(kafka.ReaderConfig{
Brokers: strings.Split(cfg.KafkaBrokers, ","),
@@ -148,3 +154,38 @@ func getEnvOrDefault(key, defaultVal string) string {
}
return defaultVal
}
// ensureKafkaTopic создаёт топик iot.telemetry если не существует.
// Вызывается ДО создания Reader и Join consumer group — исключает race condition
// в kafka-go при одновременном auto-create топика и join группы.
// Ретраится пока Kafka не ответит (брокер может ещё стартовать).
func ensureKafkaTopic(ctx context.Context, brokers string, log *slog.Logger) {
brokerList := strings.Split(brokers, ",")
for attempt := 1; attempt <= 30; attempt++ {
conn, err := kafka.DialContext(ctx, "tcp", brokerList[0])
if err != nil {
log.Warn("kafka not reachable yet, retrying...", "attempt", attempt, "err", err)
select {
case <-ctx.Done():
return
case <-time.After(3 * time.Second):
continue
}
}
defer conn.Close()
// Создаём топик идемпотентно — ошибка TopicAlreadyExists игнорируется
err = conn.CreateTopics(kafka.TopicConfig{
Topic: iotTelemetryTopic,
NumPartitions: 1,
ReplicationFactor: 1,
})
if err != nil && err != kafka.TopicAlreadyExists {
log.Warn("failed to create kafka topic, auto.create.topics.enable will handle it", "err", err)
} else {
log.Info("kafka topic ready", "topic", iotTelemetryTopic)
}
return
}
log.Warn("kafka did not respond after 30 attempts, proceeding without pre-creation")
}
+3 -3
View File
@@ -82,9 +82,9 @@ func main() {
// Kafka writer — асинхронный, с автоматическим созданием топика.
kafkaWriter := &kafka.Writer{
Addr: kafka.TCP(strings.Split(cfg.KafkaBrokers, ",")...),
Topic: iotTelemetryTopic,
Balancer: &kafka.LeastBytes{},
Addr: kafka.TCP(strings.Split(cfg.KafkaBrokers, ",")...),
Topic: iotTelemetryTopic,
Balancer: &kafka.LeastBytes{},
// Позволяет продолжать работу при временной недоступности Kafka (буфер в памяти)
Async: false,
RequiredAcks: kafka.RequireOne,