✨ Upgrade Go version to 1.23.0 and update dependencies. Introduce new application structure with Clean Architecture principles, including message processing, NATS integration, and PostgreSQL repository. Add configuration management using Koanf and structured logging with Zap. Remove legacy GraphQL integration and related files. Implement dependency injection with Google Wire.
This commit is contained in:
@@ -0,0 +1,184 @@
|
||||
package nats
|
||||
|
||||
import (
|
||||
"caatsm/internal/app"
|
||||
"caatsm/internal/infra/config"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"go.uber.org/zap"
|
||||
"github.com/nats-io/nats.go"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Consumer handles NATS JetStream message consumption
|
||||
type Consumer struct {
|
||||
js nats.JetStreamContext
|
||||
processor *app.MessageProcessor
|
||||
cfg *config.Config
|
||||
logger *zap.Logger
|
||||
subject string
|
||||
consumerName string
|
||||
}
|
||||
|
||||
// ProvideConsumer creates a NATS consumer
|
||||
func ProvideConsumer(
|
||||
js nats.JetStreamContext,
|
||||
processor *app.MessageProcessor,
|
||||
cfg *config.Config,
|
||||
logger *zap.Logger,
|
||||
) (*Consumer, error) {
|
||||
subject := cfg.Subscription.Topic
|
||||
if subject == "" {
|
||||
subject = "telegram.>"
|
||||
}
|
||||
|
||||
consumerName := cfg.NATS.Consumer
|
||||
if consumerName == "" {
|
||||
consumerName = "telegram-consumer"
|
||||
}
|
||||
|
||||
consumer := &Consumer{
|
||||
js: js,
|
||||
processor: processor,
|
||||
cfg: cfg,
|
||||
logger: logger,
|
||||
subject: subject,
|
||||
consumerName: consumerName,
|
||||
}
|
||||
|
||||
// Create consumer if it doesn't exist
|
||||
if err := consumer.ensureConsumer(); err != nil {
|
||||
return nil, fmt.Errorf("failed to ensure consumer: %w", err)
|
||||
}
|
||||
|
||||
return consumer, nil
|
||||
}
|
||||
|
||||
// ensureConsumer creates the consumer if it doesn't exist
|
||||
func (c *Consumer) ensureConsumer() error {
|
||||
streamName := c.cfg.NATS.Stream
|
||||
if streamName == "" {
|
||||
streamName = "TELEGRAM"
|
||||
}
|
||||
|
||||
consumerConfig := &nats.ConsumerConfig{
|
||||
Durable: c.consumerName,
|
||||
DeliverPolicy: nats.DeliverAllPolicy,
|
||||
AckPolicy: nats.AckExplicitPolicy,
|
||||
AckWait: c.cfg.Timeouts.AckWait,
|
||||
MaxDeliver: 5, // Maximum number of delivery attempts
|
||||
FilterSubject: c.subject,
|
||||
}
|
||||
|
||||
_, err := c.js.AddConsumer(streamName, consumerConfig)
|
||||
if err != nil && err != nats.ErrConsumerNameAlreadyInUse {
|
||||
return fmt.Errorf("failed to create consumer: %w", err)
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
c.logger.Info("Created JetStream consumer",
|
||||
zap.String("consumer", c.consumerName),
|
||||
zap.String("stream", streamName),
|
||||
zap.String("subject", c.subject),
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Start starts consuming messages
|
||||
func (c *Consumer) Start(ctx context.Context) error {
|
||||
streamName := c.cfg.NATS.Stream
|
||||
if streamName == "" {
|
||||
streamName = "TELEGRAM"
|
||||
}
|
||||
|
||||
// Create pull subscription
|
||||
sub, err := c.js.PullSubscribe(c.subject, c.consumerName, nats.Bind(streamName, c.consumerName))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create pull subscription: %w", err)
|
||||
}
|
||||
defer sub.Unsubscribe()
|
||||
|
||||
c.logger.Info("Started consuming messages",
|
||||
zap.String("subject", c.subject),
|
||||
zap.String("consumer", c.consumerName),
|
||||
zap.String("stream", streamName),
|
||||
)
|
||||
|
||||
batchSize := c.cfg.App.BatchSize
|
||||
if batchSize == 0 {
|
||||
batchSize = 50
|
||||
}
|
||||
batchTimeout := c.cfg.App.BatchTimeout
|
||||
if batchTimeout == 0 {
|
||||
batchTimeout = 2 * time.Second
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
c.logger.Info("Stopping consumer", zap.Error(ctx.Err()))
|
||||
return ctx.Err()
|
||||
default:
|
||||
}
|
||||
|
||||
// Fetch messages in batch
|
||||
msgs, err := sub.Fetch(batchSize, nats.MaxWait(batchTimeout))
|
||||
if err != nil {
|
||||
if errors.Is(err, nats.ErrTimeout) {
|
||||
// Timeout is expected when no messages are available
|
||||
continue
|
||||
}
|
||||
c.logger.Error("Failed to fetch messages", zap.Error(err))
|
||||
time.Sleep(time.Second)
|
||||
continue
|
||||
}
|
||||
|
||||
// Process each message
|
||||
for _, msg := range msgs {
|
||||
if err := c.processMessage(ctx, msg); err != nil {
|
||||
c.logger.Error("Failed to process message",
|
||||
zap.String("subject", msg.Subject),
|
||||
zap.Error(err),
|
||||
)
|
||||
// NAK the message to retry
|
||||
if nakErr := msg.Nak(); nakErr != nil {
|
||||
c.logger.Error("Failed to NAK message", zap.Error(nakErr))
|
||||
}
|
||||
} else {
|
||||
// ACK the message
|
||||
if ackErr := msg.Ack(); ackErr != nil {
|
||||
c.logger.Error("Failed to ACK message", zap.Error(ackErr))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// processMessage processes a single message
|
||||
func (c *Consumer) processMessage(ctx context.Context, msg *nats.Msg) error {
|
||||
msgID := msg.Header.Get("Nats-Msg-Id")
|
||||
if msgID == "" {
|
||||
// Use reply subject or generate a simple ID
|
||||
if msg.Reply != "" {
|
||||
msgID = msg.Reply
|
||||
} else {
|
||||
msgID = fmt.Sprintf("msg-%d", time.Now().UnixNano())
|
||||
}
|
||||
}
|
||||
|
||||
c.logger.Debug("Processing message",
|
||||
zap.String("subject", msg.Subject),
|
||||
zap.String("msg_id", msgID),
|
||||
zap.Int("data_size", len(msg.Data)),
|
||||
)
|
||||
|
||||
// Call processor
|
||||
if err := c.processor.Handle(ctx, msg.Data, msgID); err != nil {
|
||||
return fmt.Errorf("processor error: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package nats
|
||||
|
||||
import (
|
||||
"caatsm/internal/infra/config"
|
||||
"fmt"
|
||||
"go.uber.org/zap"
|
||||
"github.com/nats-io/nats.go"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ProvideJetStream creates a NATS JetStream connection
|
||||
func ProvideJetStream(cfg *config.Config, logger *zap.Logger) (nats.JetStreamContext, error) {
|
||||
// Connect to NATS
|
||||
nc, err := nats.Connect(
|
||||
cfg.NATS.URL,
|
||||
nats.RetryOnFailedConnect(true),
|
||||
nats.Timeout(cfg.Timeouts.Server),
|
||||
nats.ReconnectWait(cfg.Timeouts.ReconnectWait),
|
||||
nats.DisconnectErrHandler(func(nc *nats.Conn, err error) {
|
||||
if err != nil {
|
||||
logger.Warn("NATS disconnected", zap.Error(err))
|
||||
}
|
||||
}),
|
||||
nats.ReconnectHandler(func(nc *nats.Conn) {
|
||||
logger.Info("NATS reconnected", zap.String("url", nc.ConnectedUrl()))
|
||||
}),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to connect to NATS: %w", err)
|
||||
}
|
||||
|
||||
// Get JetStream context
|
||||
js, err := nc.JetStream()
|
||||
if err != nil {
|
||||
nc.Close()
|
||||
return nil, fmt.Errorf("failed to get JetStream context: %w", err)
|
||||
}
|
||||
|
||||
// Create stream if it doesn't exist
|
||||
streamName := cfg.NATS.Stream
|
||||
subject := cfg.Subscription.Topic
|
||||
if subject == "" {
|
||||
subject = "telegram.>"
|
||||
}
|
||||
|
||||
streamConfig := &nats.StreamConfig{
|
||||
Name: streamName,
|
||||
Subjects: []string{subject},
|
||||
Retention: nats.LimitsPolicy,
|
||||
MaxAge: 24 * time.Hour,
|
||||
Storage: nats.FileStorage,
|
||||
Replicas: 1,
|
||||
}
|
||||
|
||||
_, err = js.AddStream(streamConfig)
|
||||
if err != nil && err != nats.ErrStreamNameAlreadyInUse {
|
||||
nc.Close()
|
||||
return nil, fmt.Errorf("failed to create stream: %w", err)
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
logger.Info("Created JetStream", zap.String("stream", streamName), zap.String("subject", subject))
|
||||
}
|
||||
|
||||
return js, nil
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package nats
|
||||
|
||||
import (
|
||||
"caatsm/internal/adapter"
|
||||
"caatsm/internal/infra/config"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"go.uber.org/zap"
|
||||
"github.com/nats-io/nats.go"
|
||||
)
|
||||
|
||||
// Publisher publishes messages to NATS JetStream
|
||||
type Publisher struct {
|
||||
js nats.JetStreamContext
|
||||
cfg *config.Config
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// ProvidePublisher creates a NATS publisher
|
||||
func ProvidePublisher(
|
||||
js nats.JetStreamContext,
|
||||
cfg *config.Config,
|
||||
logger *zap.Logger,
|
||||
) (adapter.Publisher, error) {
|
||||
return &Publisher{
|
||||
js: js,
|
||||
cfg: cfg,
|
||||
logger: logger,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Publish publishes a message
|
||||
func (p *Publisher) Publish(message interface{}) error {
|
||||
topic := p.cfg.Publisher.Topic
|
||||
if topic == "" {
|
||||
return fmt.Errorf("publisher topic is not configured")
|
||||
}
|
||||
|
||||
// Marshal message to JSON
|
||||
messageBytes, err := json.Marshal(message)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal message: %w", err)
|
||||
}
|
||||
|
||||
// Publish to JetStream
|
||||
_, err = p.js.Publish(topic, messageBytes)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to publish message: %w", err)
|
||||
}
|
||||
|
||||
p.logger.Debug("Published message",
|
||||
zap.String("topic", topic),
|
||||
zap.Int("size", len(messageBytes)),
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user