feat: use postgres

This commit is contained in:
Ilia Denisov
2026-04-26 20:34:39 +02:00
committed by GitHub
parent 48b0056b49
commit fe829285a6
365 changed files with 29223 additions and 24049 deletions
+29 -12
View File
@@ -8,11 +8,13 @@ import (
"strings"
"time"
"galaxy/notification/internal/adapters/redisstate"
"galaxy/notification/internal/api/intentstream"
"galaxy/notification/internal/logging"
"galaxy/notification/internal/service/acceptintent"
"galaxy/notification/internal/service/publishmail"
"galaxy/notification/internal/service/routestate"
"github.com/redis/go-redis/v9"
)
const (
@@ -24,7 +26,7 @@ const (
// by EmailPublisher.
type EmailRouteStateStore interface {
// ListDueRoutes loads due scheduled routes.
ListDueRoutes(context.Context, time.Time, int64) ([]redisstate.ScheduledRoute, error)
ListDueRoutes(context.Context, time.Time, int64) ([]routestate.ScheduledRoute, error)
// TryAcquireRouteLease attempts to acquire one temporary route lease.
TryAcquireRouteLease(context.Context, string, string, string, time.Duration) (bool, error)
@@ -39,13 +41,13 @@ type EmailRouteStateStore interface {
GetRoute(context.Context, string, string) (acceptintent.NotificationRoute, bool, error)
// CompleteRoutePublished records one successful publication.
CompleteRoutePublished(context.Context, redisstate.CompleteRoutePublishedInput) error
CompleteRoutePublished(context.Context, routestate.CompleteRoutePublishedInput) error
// CompleteRouteFailed records one retryable publication failure.
CompleteRouteFailed(context.Context, redisstate.CompleteRouteFailedInput) error
CompleteRouteFailed(context.Context, routestate.CompleteRouteFailedInput) error
// CompleteRouteDeadLetter records one exhausted publication failure.
CompleteRouteDeadLetter(context.Context, redisstate.CompleteRouteDeadLetterInput) error
CompleteRouteDeadLetter(context.Context, routestate.CompleteRouteDeadLetterInput) error
}
// EmailCommandEncoder encodes one email-capable notification route into a
@@ -90,6 +92,10 @@ type EmailPublisherConfig struct {
// Clock provides wall-clock timestamps.
Clock Clock
// StreamPublisher emits the outbound mail-delivery command before the
// route's PostgreSQL state transition is committed.
StreamPublisher StreamPublisher
}
// EmailPublisher publishes due email routes into the Mail Service command
@@ -105,6 +111,7 @@ type EmailPublisher struct {
encoder EmailCommandEncoder
telemetry RoutePublisherTelemetry
clock Clock
streamPublisher StreamPublisher
workerToken string
logger *slog.Logger
}
@@ -114,6 +121,8 @@ func NewEmailPublisher(cfg EmailPublisherConfig, logger *slog.Logger) (*EmailPub
switch {
case cfg.Store == nil:
return nil, errors.New("new email publisher: nil store")
case cfg.StreamPublisher == nil:
return nil, errors.New("new email publisher: nil stream publisher")
case strings.TrimSpace(cfg.MailDeliveryCommandsStream) == "":
return nil, errors.New("new email publisher: mail delivery-commands stream must not be empty")
case cfg.RouteLeaseTTL <= 0:
@@ -157,6 +166,7 @@ func NewEmailPublisher(cfg EmailPublisherConfig, logger *slog.Logger) (*EmailPub
encoder: cfg.Encoder,
telemetry: cfg.Telemetry,
clock: cfg.Clock,
streamPublisher: cfg.StreamPublisher,
workerToken: workerToken,
logger: logger.With("component", "email_publisher", "stream", cfg.MailDeliveryCommandsStream),
}, nil
@@ -237,7 +247,7 @@ func (publisher *EmailPublisher) publishDueRoutes(ctx context.Context) (bool, er
return progress, nil
}
func (publisher *EmailPublisher) publishRoute(ctx context.Context, now time.Time, dueRoute redisstate.ScheduledRoute) (bool, error) {
func (publisher *EmailPublisher) publishRoute(ctx context.Context, now time.Time, dueRoute routestate.ScheduledRoute) (bool, error) {
acquired, err := publisher.store.TryAcquireRouteLease(ctx, dueRoute.NotificationID, dueRoute.RouteID, publisher.workerToken, publisher.routeLeaseTTL)
if err != nil {
return false, fmt.Errorf("acquire route lease %q: %w", dueRoute.RouteID, err)
@@ -283,7 +293,14 @@ func (publisher *EmailPublisher) publishRoute(ctx context.Context, now time.Time
return publisher.recordFailure(ctx, notification, route, emailFailureClassificationPayloadEncoding, err.Error())
}
err = publisher.store.CompleteRoutePublished(ctx, redisstate.CompleteRoutePublishedInput{
if err := publisher.streamPublisher.XAdd(ctx, &redis.XAddArgs{
Stream: publisher.mailDeliveryCommandsStream,
Values: command.Values(),
}).Err(); err != nil {
return publisher.recordFailure(ctx, notification, route, emailFailureClassificationMailStreamWrite, err.Error())
}
err = publisher.store.CompleteRoutePublished(ctx, routestate.CompleteRoutePublishedInput{
ExpectedRoute: route,
LeaseToken: publisher.workerToken,
PublishedAt: publisher.now(),
@@ -312,7 +329,7 @@ func (publisher *EmailPublisher) publishRoute(ctx context.Context, now time.Time
logArgs = append(logArgs, logging.TraceAttrsFromContext(ctx)...)
publisher.logger.Info("email route published", logArgs...)
return true, nil
case errors.Is(err, redisstate.ErrConflict):
case errors.Is(err, routestate.ErrConflict):
return false, nil
default:
return publisher.recordFailure(ctx, notification, route, emailFailureClassificationMailStreamWrite, err.Error())
@@ -349,7 +366,7 @@ func (publisher *EmailPublisher) recordFailure(
logArgs = append(logArgs, logging.TraceAttrsFromContext(ctx)...)
if attemptNumber >= route.MaxAttempts {
err := publisher.store.CompleteRouteDeadLetter(ctx, redisstate.CompleteRouteDeadLetterInput{
err := publisher.store.CompleteRouteDeadLetter(ctx, routestate.CompleteRouteDeadLetterInput{
ExpectedRoute: route,
LeaseToken: publisher.workerToken,
DeadLetteredAt: failureAt,
@@ -362,7 +379,7 @@ func (publisher *EmailPublisher) recordFailure(
publisher.recordRouteDeadLetter(ctx, notification, route, classification)
publisher.logger.Warn("email route dead-lettered", logArgs...)
return true, nil
case errors.Is(err, redisstate.ErrConflict):
case errors.Is(err, routestate.ErrConflict):
return false, nil
default:
return false, fmt.Errorf("dead-letter route %q: %w", route.RouteID, err)
@@ -370,7 +387,7 @@ func (publisher *EmailPublisher) recordFailure(
}
nextAttemptAt := failureAt.Add(routeBackoffDelay(attemptNumber, publisher.routeBackoffMin, publisher.routeBackoffMax)).UTC().Truncate(time.Millisecond)
err := publisher.store.CompleteRouteFailed(ctx, redisstate.CompleteRouteFailedInput{
err := publisher.store.CompleteRouteFailed(ctx, routestate.CompleteRouteFailedInput{
ExpectedRoute: route,
LeaseToken: publisher.workerToken,
FailedAt: failureAt,
@@ -385,7 +402,7 @@ func (publisher *EmailPublisher) recordFailure(
logArgs = append(logArgs, "next_attempt_at", nextAttemptAt)
publisher.logger.Warn("email route failed and was rescheduled", logArgs...)
return true, nil
case errors.Is(err, redisstate.ErrConflict):
case errors.Is(err, routestate.ErrConflict):
return false, nil
default:
return false, fmt.Errorf("reschedule route %q: %w", route.RouteID, err)