[hw-6] add notifier service, kafka

This commit is contained in:
Никита Шубин
2025-07-17 19:20:27 +00:00
parent 424d6905da
commit 6e1ad86128
33 changed files with 1412 additions and 92 deletions

View File

@@ -0,0 +1,7 @@
package kafka
type OrderEvent struct {
OrderID int64 `json:"order_id"`
Status string `json:"status"`
Moment string `json:"moment"`
}

View File

@@ -0,0 +1,110 @@
package kafka
import (
"context"
"encoding/json"
"fmt"
"strconv"
"time"
"route256/loms/internal/domain/entity"
"github.com/IBM/sarama"
"github.com/rs/zerolog/log"
pb "route256/pkg/api/loms/v1"
)
type statusProducer struct {
topic string
producer sarama.AsyncProducer
}
func NewStatusProducer(topic string, producer sarama.AsyncProducer) (*statusProducer, error) {
p := &statusProducer{topic: topic, producer: producer}
go p.runCallbacks()
return p, nil
}
func mapOrderStatus(pbStatus string) (string, error) {
switch pbStatus {
case pb.OrderStatus_ORDER_STATUS_AWAITING_PAYMENT.String():
return "awaiting payment", nil
case pb.OrderStatus_ORDER_STATUS_CANCELLED.String():
return "cancelled", nil
case pb.OrderStatus_ORDER_STATUS_FAILED.String():
return "failed", nil
case pb.OrderStatus_ORDER_STATUS_NEW.String():
return "new", nil
case pb.OrderStatus_ORDER_STATUS_PAYED.String():
return "payed", nil
default:
return "", fmt.Errorf("unexpected OrderStatus: %v", pbStatus)
}
}
func (p *statusProducer) runCallbacks() {
for {
select {
case err, ok := <-p.producer.Errors():
if !ok {
return
}
log.Error().Err(err).Msgf("kafka publish error")
case _, ok := <-p.producer.Successes():
if !ok {
return
}
// TODO: add msg metrics (latency/partition/offset)
}
}
}
func (p *statusProducer) Send(ctx context.Context, id entity.ID, status string) error {
log.Debug().Msgf("sending event for id: %d; status: %s", id, status)
newStatus, err := mapOrderStatus(status)
if err != nil {
return err
}
evt := OrderEvent{
OrderID: int64(id),
Status: newStatus,
Moment: time.Now().UTC().Format(time.RFC3339Nano),
}
value, err := json.Marshal(evt)
if err != nil {
return fmt.Errorf("marshal event: %w", err)
}
return p.SendRaw(ctx, id, value)
}
func (p *statusProducer) SendRaw(ctx context.Context, id entity.ID, value []byte) error {
if len(value) == 0 {
return fmt.Errorf("empty message value")
}
msg := &sarama.ProducerMessage{
Topic: p.topic,
Key: sarama.StringEncoder(strconv.FormatInt(int64(id), 10)),
Value: sarama.ByteEncoder(value),
Timestamp: time.Now(),
}
select {
case p.producer.Input() <- msg:
return nil
case <-ctx.Done():
return ctx.Err()
}
}
func (p *statusProducer) Close() error {
return p.producer.Close()
}

View File

@@ -0,0 +1,54 @@
package kafkaoutbox
import (
"context"
"time"
"route256/loms/internal/domain/entity"
"route256/loms/internal/domain/repository/outbox"
"github.com/rs/zerolog/log"
)
type producer interface {
SendRaw(ctx context.Context, id entity.ID, value []byte) error
}
type outboxRepo interface {
WithNewEvents(ctx context.Context, limit int, h func(c context.Context, e outbox.Event) error) error
}
type Dispatcher struct {
repo outboxRepo
producer producer
limit int
interval time.Duration
}
func NewDispatcher(repo outboxRepo, producer producer, batch int, pollEvery time.Duration) *Dispatcher {
return &Dispatcher{
repo: repo,
producer: producer,
limit: batch,
interval: pollEvery,
}
}
func (d *Dispatcher) Run(ctx context.Context) {
t := time.NewTicker(d.interval)
defer t.Stop()
for {
if err := d.repo.WithNewEvents(ctx, d.limit, func(c context.Context, e outbox.Event) error {
return d.producer.SendRaw(c, e.OrderID, e.Payload)
}); err != nil {
log.Error().Err(err).Msg("d.repo.WithNewEvents")
}
select {
case <-ctx.Done():
return
case <-t.C:
}
}
}