Files
go-caatsm/internal/infra/nats/advisory_dlq.go
T

170 lines
5.1 KiB
Go

package nats
import (
"context"
"encoding/json"
"fmt"
"time"
"github.com/nats-io/nats.go"
"go.uber.org/zap"
)
// MaxDeliveriesAdvisoryEvent represents the advisory message published when
// a message reaches MaxDeliver attempts.
type MaxDeliveriesAdvisoryEvent struct {
Type string `json:"type"`
Stream string `json:"stream"`
Consumer string `json:"consumer"`
StreamSeq uint64 `json:"stream_seq"`
Deliveries uint64 `json:"deliveries"`
Time string `json:"time"`
}
// AdvisoryDLQHandler handles messages that exhaust MaxDeliver attempts
// by subscribing to JetStream advisory events.
type AdvisoryDLQHandler struct {
js nats.JetStreamContext
nc *nats.Conn
streamName string
consumerName string
dlqSubject string
logger *zap.Logger
telemetry TelemetryRecorder
}
// TelemetryRecorder is an interface for recording telemetry events.
// This matches the telemetry.Recorder interface used by Consumer.
type TelemetryRecorder interface {
RecordDLQMessage(ctx context.Context, stream, consumer string)
RecordDLQPublishFailure(ctx context.Context, stream, consumer string)
}
// NewAdvisoryDLQHandler creates a new advisory-based DLQ handler.
func NewAdvisoryDLQHandler(
js nats.JetStreamContext,
nc *nats.Conn,
streamName string,
consumerName string,
dlqSubject string,
logger *zap.Logger,
telemetry TelemetryRecorder,
) (*AdvisoryDLQHandler, error) {
return &AdvisoryDLQHandler{
js: js,
nc: nc,
streamName: streamName,
consumerName: consumerName,
dlqSubject: dlqSubject,
logger: logger,
telemetry: telemetry,
}, nil
}
// Start begins listening for advisory messages and routing failed messages to DLQ.
func (h *AdvisoryDLQHandler) Start(ctx context.Context) error {
// Subscribe to advisory subject pattern
// Format: $JS.EVENT.ADVISORY.CONSUMER.MAX_DELIVERIES.<STREAM>.<CONSUMER>
advisorySubject := fmt.Sprintf("$JS.EVENT.ADVISORY.CONSUMER.MAX_DELIVERIES.%s.%s",
h.streamName, h.consumerName)
h.logger.Info("Starting advisory DLQ handler",
zap.String("advisory_subject", advisorySubject),
zap.String("stream", h.streamName),
zap.String("consumer", h.consumerName),
zap.String("dlq_subject", h.dlqSubject),
)
sub, err := h.nc.Subscribe(advisorySubject, func(msg *nats.Msg) {
h.handleAdvisory(ctx, msg)
})
if err != nil {
return fmt.Errorf("failed to subscribe to advisory subject: %w", err)
}
// Wait for context cancellation
go func() {
<-ctx.Done()
if err := sub.Unsubscribe(); err != nil {
h.logger.Error("Failed to unsubscribe advisory subscription", zap.Error(err))
}
h.logger.Info("Stopped advisory DLQ handler")
}()
return nil
}
// handleAdvisory processes an advisory message about max deliveries.
func (h *AdvisoryDLQHandler) handleAdvisory(ctx context.Context, advisoryMsg *nats.Msg) {
var event MaxDeliveriesAdvisoryEvent
if err := json.Unmarshal(advisoryMsg.Data, &event); err != nil {
h.logger.Error("Failed to unmarshal advisory event",
zap.Error(err),
zap.String("data", string(advisoryMsg.Data)),
)
return
}
h.logger.Warn("Message reached MaxDeliver attempts",
zap.String("stream", event.Stream),
zap.String("consumer", event.Consumer),
zap.Uint64("stream_seq", event.StreamSeq),
zap.Uint64("deliveries", event.Deliveries),
)
// Retrieve the original message from the stream using GetMsg API
originalMsg, err := h.js.GetMsg(h.streamName, event.StreamSeq)
if err != nil {
h.logger.Error("Failed to retrieve original message from stream",
zap.Uint64("stream_seq", event.StreamSeq),
zap.Error(err),
)
h.telemetry.RecordDLQPublishFailure(ctx, h.streamName, h.consumerName)
return
}
// Extract message metadata
msgID := ""
subject := originalMsg.Subject
if originalMsg.Header != nil {
msgID = originalMsg.Header.Get("Nats-Msg-Id")
}
// Create enriched DLQ payload (similar to existing routeToDLQ)
payload := map[string]interface{}{
"transport_msg_id": msgID,
"subject": subject,
"stream": h.streamName,
"consumer": h.consumerName,
"nats_sequence": event.StreamSeq,
"deliveries": event.Deliveries,
"error": fmt.Sprintf("message exhausted max_deliver (%d) attempts", event.Deliveries),
"received_at": time.Now().UTC(),
"body": string(originalMsg.Data),
"advisory_source": true, // Flag to distinguish from immediate DLQ
}
data, err := json.Marshal(payload)
if err != nil {
h.logger.Error("Failed to marshal advisory DLQ payload", zap.Error(err))
h.telemetry.RecordDLQPublishFailure(ctx, h.streamName, h.consumerName)
return
}
// Publish to DLQ
if _, err := h.js.Publish(h.dlqSubject, data); err != nil {
h.logger.Error("Failed to publish advisory message to DLQ",
zap.Uint64("stream_seq", event.StreamSeq),
zap.Error(err),
)
h.telemetry.RecordDLQPublishFailure(ctx, h.streamName, h.consumerName)
return
}
h.logger.Info("Routed max-deliveries message to DLQ",
zap.Uint64("stream_seq", event.StreamSeq),
zap.Uint64("deliveries", event.Deliveries),
)
h.telemetry.RecordDLQMessage(ctx, h.streamName, h.consumerName)
}