✨ Refactor NATS consumer management by introducing a dedicated ConsumerManager and StreamManager for lifecycle management. Enhance error handling with a new ErrorHandler to streamline message processing and recovery logic. Update consumer initialization to utilize the new managers, improving code organization and maintainability. Add comprehensive tests for the new components to ensure reliability and robustness in handling NATS operations.
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
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
|
||||
if backoff > 30*time.Second {
|
||||
backoff = 30 * time.Second
|
||||
}
|
||||
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
|
||||
if backoff > 10*time.Second {
|
||||
backoff = 10 * time.Second
|
||||
}
|
||||
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
|
||||
if result.BackpressureDelay > 5*time.Second {
|
||||
result.BackpressureDelay = 5 * time.Second
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
Reference in New Issue
Block a user