Enhance NATS configuration and error handling. Introduce stream limits and consumer rules in configuration files. Refactor message processing to handle permanent errors. Update README and development configuration to reflect changes. Add tests for new error handling mechanisms.

This commit is contained in:
windyboy
2025-11-14 21:42:04 +08:00
parent a574cfcf27
commit 9cce4610b6
13 changed files with 510 additions and 57 deletions
+39
View File
@@ -0,0 +1,39 @@
package app
import "errors"
// PermanentError indicates a failure that should not be retried.
type PermanentError struct {
err error
}
// Error implements the error interface.
func (e *PermanentError) Error() string {
if e == nil || e.err == nil {
return ""
}
return e.err.Error()
}
// Unwrap allows errors.Unwrap/Is/As to inspect the underlying error.
func (e *PermanentError) Unwrap() error {
if e == nil {
return nil
}
return e.err
}
// Permanent wraps err to mark it as non-retriable.
func Permanent(err error) error {
if err == nil {
return nil
}
return &PermanentError{err: err}
}
// IsPermanent reports whether the error or any wrapped error is permanent.
func IsPermanent(err error) bool {
var target *PermanentError
return errors.As(err, &target)
}
+33
View File
@@ -0,0 +1,33 @@
package app
import (
"errors"
"testing"
)
func TestPermanentWrapsError(t *testing.T) {
base := errors.New("boom")
perr := Permanent(base)
if perr == nil {
t.Fatalf("expected wrapped error, got nil")
}
if !IsPermanent(perr) {
t.Fatalf("expected IsPermanent to be true")
}
if !errors.Is(perr, base) {
t.Fatalf("expected wrapped error to unwrap to base")
}
if errors.Is(base, perr) {
t.Fatalf("expected base not to consider wrapper as same")
}
}
func TestPermanentNil(t *testing.T) {
if Permanent(nil) != nil {
t.Fatalf("Permanent(nil) should return nil")
}
if IsPermanent(nil) {
t.Fatalf("IsPermanent(nil) should be false")
}
}
+3 -3
View File
@@ -1,9 +1,9 @@
package app
import (
"context"
"caatsm/internal/adapter"
"caatsm/internal/adapter/parser"
"context"
"fmt"
"go.uber.org/zap"
)
@@ -34,13 +34,13 @@ func NewMessageProcessor(
// Handle processes a message
func (p *MessageProcessor) Handle(ctx context.Context, raw []byte, msgID string) error {
if raw == nil || len(raw) == 0 {
return fmt.Errorf("empty message")
return Permanent(fmt.Errorf("empty message"))
}
// Parse the message
parsed := p.parser.Parse(string(raw))
if parsed == nil {
return fmt.Errorf("parser returned nil")
return Permanent(fmt.Errorf("parser returned nil"))
}
// Set the message ID from NATS
+115
View File
@@ -0,0 +1,115 @@
package app
import (
"caatsm/internal/adapter"
"caatsm/internal/adapter/parser"
"caatsm/internal/domain"
"context"
"errors"
"testing"
"go.uber.org/zap"
)
type stubParser struct {
value *domain.ParsedMessage
}
func (s *stubParser) Parse(rawText string) *domain.ParsedMessage {
return s.value
}
type stubRepository struct {
inserted []*domain.ParsedMessage
err error
}
func (s *stubRepository) InsertOne(ctx context.Context, msg *domain.ParsedMessage) error {
if s.err != nil {
return s.err
}
s.inserted = append(s.inserted, msg)
return nil
}
func (s *stubRepository) InsertBatch(ctx context.Context, msgs []*domain.ParsedMessage) error {
return errors.New("not implemented")
}
type stubPublisher struct {
last interface{}
err error
}
func (s *stubPublisher) Publish(message interface{}) error {
s.last = message
return s.err
}
func newTestProcessor(p parser.Parser, repo adapter.Repository, pub adapter.Publisher) *MessageProcessor {
return NewMessageProcessor(p, repo, pub, zap.NewNop())
}
func TestHandleEmptyMessageIsPermanent(t *testing.T) {
proc := newTestProcessor(&stubParser{}, &stubRepository{}, &stubPublisher{})
err := proc.Handle(context.Background(), nil, "id-1")
if err == nil || !IsPermanent(err) {
t.Fatalf("expected permanent error for empty message, got %v", err)
}
}
func TestHandleNilParserResultIsPermanent(t *testing.T) {
proc := newTestProcessor(&stubParser{value: nil}, &stubRepository{}, &stubPublisher{})
err := proc.Handle(context.Background(), []byte("payload"), "id-2")
if err == nil || !IsPermanent(err) {
t.Fatalf("expected permanent error for nil parser result, got %v", err)
}
}
func TestHandleSuccessSetsUuidAndPublishes(t *testing.T) {
parsed := domain.NewParsedMessage()
parsed.Parsed = true
repo := &stubRepository{}
pub := &stubPublisher{}
proc := newTestProcessor(&stubParser{value: parsed}, repo, pub)
const msgID = "uuid-123"
err := proc.Handle(context.Background(), []byte("payload"), msgID)
if err != nil {
t.Fatalf("expected success, got %v", err)
}
if len(repo.inserted) != 1 {
t.Fatalf("expected one inserted message, got %d", len(repo.inserted))
}
if repo.inserted[0].Uuid != msgID {
t.Fatalf("expected message uuid to be %s, got %s", msgID, repo.inserted[0].Uuid)
}
if pub.last == nil {
t.Fatalf("expected publisher to receive message")
}
if pub.last != repo.inserted[0] {
t.Fatalf("publisher received unexpected message pointer")
}
}
func TestHandlePublisherErrorIsRetriable(t *testing.T) {
parsed := domain.NewParsedMessage()
parsed.Parsed = true
repo := &stubRepository{}
pub := &stubPublisher{err: errors.New("publish failed")}
proc := newTestProcessor(&stubParser{value: parsed}, repo, pub)
err := proc.Handle(context.Background(), []byte("payload"), "id-3")
if err == nil {
t.Fatalf("expected error when publisher fails")
}
if IsPermanent(err) {
t.Fatalf("publisher failure should not be permanent")
}
if len(repo.inserted) != 1 {
t.Fatalf("expected message to insert before publish failure")
}
}