package main import ( "context" "encoding/json" "strings" "time" "github.com/segmentio/kafka-go" "github.com/tinode/chat/server/logs" "github.com/tinode/chat/server/store" "github.com/tinode/chat/server/store/types" ) func startKafkaDeadLetterConsumer() { startKafkaConsumer("dead_letter", handleKafkaDeadLetter) } func handleKafkaDeadLetter(ctx context.Context, envelope kafkaEnvelope, _ kafka.Message) error { var dead types.KafkaDeadLetter if err := json.Unmarshal(envelope.Payload, &dead); err != nil { return err } if dead.DeadID == "" { dead.DeadID = envelope.EventID } tenantID, err := tenantIDFromDeadLetter(envelope, &dead) if err != nil { return err } now := time.Now().UTC().Round(time.Millisecond) if err = store.HTTPInfrastructure.MarkKafkaConsumeDead(tenantID, &dead, now); err != nil { return err } if auditErr := enqueueKafkaAuditEvent(ctx, tenantID, "system", types.ZeroUid, "kafka.dead_letter.created", "kafka_event", dead.EventID, "dead", dead.LastReason, map[string]any{ "dead_id": dead.DeadID, "event_type": dead.EventType, "biz_type": dead.BizType, "biz_id": dead.BizID, "consumer_group": dead.ConsumerGroup, }); auditErr != nil { logs.Warn.Printf("kafka dead letter audit enqueue failed: event=%s dead=%s err=%v", dead.EventID, dead.DeadID, auditErr) } return nil } func tenantIDFromDeadLetter(envelope kafkaEnvelope, dead *types.KafkaDeadLetter) (types.TenantID, error) { if !envelope.TenantID.IsZero() { return envelope.TenantID, nil } if dead != nil && len(dead.Envelope) > 0 { var original kafkaEnvelope if err := json.Unmarshal(dead.Envelope, &original); err == nil && !original.TenantID.IsZero() { return original.TenantID, nil } } return kafkaTenantID(envelope.TenantCode) } func replayKafkaDeadLetter(ctx context.Context, tenantID types.TenantID, deadID string, actor types.Uid, note string) (*types.KafkaDeadLetter, error) { now := time.Now().UTC().Round(time.Millisecond) dead, err := store.HTTPInfrastructure.MarkKafkaDeadLetterReplay(tenantID, deadID, actor.UserId(), note, now) if err != nil { return nil, err } var envelope kafkaEnvelope if err = json.Unmarshal(dead.Envelope, &envelope); err != nil { return nil, err } if envelope.EventID == "" { envelope.EventID = dead.EventID } if envelope.ParentEventID == "" { envelope.ParentEventID = dead.ParentEventID } if err = publishKafkaEnvelope(ctx, strings.TrimSpace(dead.SourceTopic), envelope.EventID, envelope); err != nil { return nil, err } if err = store.HTTPInfrastructure.MarkKafkaDeadLetter(tenantID, deadID, types.KafkaDeadStatusReplayed, actor.UserId(), note, now); err != nil { return nil, err } return store.HTTPInfrastructure.GetKafkaDeadLetter(tenantID, deadID) } func resolveKafkaDeadLetter(tenantID types.TenantID, deadID, status string, actor types.Uid, note string) error { now := time.Now().UTC().Round(time.Millisecond) return store.HTTPInfrastructure.MarkKafkaDeadLetter(tenantID, deadID, status, actor.UserId(), note, now) }