2025-11-18 08:38:03 +08:00
|
|
|
package nats
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"caatsm/internal/app"
|
|
|
|
|
"context"
|
|
|
|
|
"errors"
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
"github.com/nats-io/nats.go"
|
|
|
|
|
"go.uber.org/zap"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// ErrorHandler handles various error scenarios in NATS operations
|
|
|
|
|
type ErrorHandler struct {
|
|
|
|
|
logger *zap.Logger
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// NewErrorHandler creates a new error handler
|
|
|
|
|
func NewErrorHandler(logger *zap.Logger) *ErrorHandler {
|
|
|
|
|
return &ErrorHandler{
|
|
|
|
|
logger: logger,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// FetchErrorResult represents the result of handling a fetch error
|
|
|
|
|
type FetchErrorResult struct {
|
|
|
|
|
ShouldContinue bool
|
|
|
|
|
RecoveredSub *nats.Subscription
|
|
|
|
|
Error error
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// HandleFetchError handles errors during message fetching with recovery logic
|
|
|
|
|
func (h *ErrorHandler) HandleFetchError(
|
|
|
|
|
ctx context.Context,
|
|
|
|
|
err error,
|
|
|
|
|
sub **nats.Subscription,
|
|
|
|
|
fetchErrorStreak *int,
|
|
|
|
|
streamName, consumerName string,
|
|
|
|
|
recoverFunc func() (*nats.Subscription, error),
|
|
|
|
|
) FetchErrorResult {
|
|
|
|
|
if errors.Is(err, nats.ErrTimeout) {
|
|
|
|
|
// Timeout is expected when no messages are available.
|
|
|
|
|
return FetchErrorResult{ShouldContinue: true}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// JetStream API is currently unavailable (e.g., NATS just restarted or JetStream not ready).
|
|
|
|
|
if errors.Is(err, nats.ErrNoResponders) {
|
|
|
|
|
*fetchErrorStreak++
|
|
|
|
|
backoff := time.Duration(*fetchErrorStreak) * time.Second
|
2025-11-18 11:47:35 +08:00
|
|
|
backoff = min(backoff, 30*time.Second)
|
2025-11-18 08:38:03 +08:00
|
|
|
h.logger.Warn("JetStream not available, will retry with backoff",
|
|
|
|
|
zap.Error(err),
|
|
|
|
|
zap.String("stream", streamName),
|
|
|
|
|
zap.String("consumer", consumerName),
|
|
|
|
|
zap.Duration("backoff", backoff),
|
|
|
|
|
)
|
|
|
|
|
if !sleepWithContext(ctx, backoff) {
|
|
|
|
|
return FetchErrorResult{ShouldContinue: false, Error: ctx.Err()}
|
|
|
|
|
}
|
|
|
|
|
return FetchErrorResult{ShouldContinue: true}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Underlying consumer/stream removed while app is running.
|
|
|
|
|
if isJetStreamResourceNotFound(err) {
|
|
|
|
|
if isDevLikeEnv() && shouldBootstrapStream() {
|
|
|
|
|
h.logger.Warn("JetStream consumer or stream missing; attempting to recreate",
|
|
|
|
|
zap.Error(err),
|
|
|
|
|
zap.String("stream", streamName),
|
|
|
|
|
zap.String("consumer", consumerName),
|
|
|
|
|
)
|
|
|
|
|
newSub, subErr := recoverFunc()
|
|
|
|
|
if subErr != nil {
|
|
|
|
|
return FetchErrorResult{ShouldContinue: false, Error: subErr}
|
|
|
|
|
}
|
|
|
|
|
*sub = newSub
|
|
|
|
|
*fetchErrorStreak = 0
|
|
|
|
|
return FetchErrorResult{ShouldContinue: true, RecoveredSub: newSub}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Production: treat as configuration/operational error.
|
|
|
|
|
h.logger.Error("JetStream consumer or stream missing; not auto-recreating in this environment",
|
|
|
|
|
zap.Error(err),
|
|
|
|
|
zap.String("stream", streamName),
|
|
|
|
|
zap.String("consumer", consumerName),
|
|
|
|
|
)
|
|
|
|
|
return FetchErrorResult{ShouldContinue: false, Error: err}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Generic error path with modest backoff.
|
|
|
|
|
*fetchErrorStreak++
|
|
|
|
|
backoff := time.Duration(*fetchErrorStreak) * time.Second
|
2025-11-18 11:47:35 +08:00
|
|
|
backoff = min(backoff, 10*time.Second)
|
2025-11-18 08:38:03 +08:00
|
|
|
h.logger.Error("Failed to fetch messages; backing off",
|
|
|
|
|
zap.Error(err),
|
|
|
|
|
zap.Duration("backoff", backoff),
|
|
|
|
|
)
|
|
|
|
|
if !sleepWithContext(ctx, backoff) {
|
|
|
|
|
return FetchErrorResult{ShouldContinue: false, Error: ctx.Err()}
|
|
|
|
|
}
|
|
|
|
|
return FetchErrorResult{ShouldContinue: true}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ProcessingErrorResult represents the result of handling a processing error
|
|
|
|
|
type ProcessingErrorResult struct {
|
|
|
|
|
IsPermanent bool
|
|
|
|
|
ShouldApplyBackpressure bool
|
|
|
|
|
BackpressureDelay time.Duration
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// HandleProcessingError analyzes processing errors and determines appropriate action
|
|
|
|
|
func (h *ErrorHandler) HandleProcessingError(
|
|
|
|
|
consecutiveErrors int,
|
|
|
|
|
err error,
|
|
|
|
|
logger *zap.Logger,
|
|
|
|
|
subject string,
|
|
|
|
|
) ProcessingErrorResult {
|
|
|
|
|
isPermanent := app.IsPermanent(err)
|
|
|
|
|
|
|
|
|
|
result := ProcessingErrorResult{
|
|
|
|
|
IsPermanent: isPermanent,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if isPermanent {
|
|
|
|
|
// Reset error streak for permanent errors
|
|
|
|
|
return result
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Transient error: increment error streak and apply simple backpressure if needed.
|
|
|
|
|
if consecutiveErrors >= 10 {
|
|
|
|
|
result.ShouldApplyBackpressure = true
|
|
|
|
|
result.BackpressureDelay = time.Duration(consecutiveErrors) * 100 * time.Millisecond
|
2025-11-18 11:47:35 +08:00
|
|
|
result.BackpressureDelay = min(result.BackpressureDelay, 5*time.Second)
|
2025-11-18 08:38:03 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return result
|
|
|
|
|
}
|