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
+34 -12
View File
@@ -10,11 +10,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/publishpush"
"galaxy/notification/internal/service/routestate"
"github.com/redis/go-redis/v9"
)
const (
@@ -29,7 +31,7 @@ const (
// PushPublisher.
type PushRouteStateStore 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)
@@ -44,13 +46,13 @@ type PushRouteStateStore 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
}
// PushEventEncoder encodes one push-capable notification route into a
@@ -109,6 +111,10 @@ type PushPublisherConfig struct {
// Clock provides wall-clock timestamps.
Clock Clock
// StreamPublisher emits the outbound Gateway client-event before the
// route's PostgreSQL state transition is committed.
StreamPublisher StreamPublisher
}
// PushPublisher publishes due push routes into the Gateway client-events
@@ -125,6 +131,7 @@ type PushPublisher struct {
encoder PushEventEncoder
telemetry RoutePublisherTelemetry
clock Clock
streamPublisher StreamPublisher
workerToken string
logger *slog.Logger
}
@@ -134,6 +141,8 @@ func NewPushPublisher(cfg PushPublisherConfig, logger *slog.Logger) (*PushPublis
switch {
case cfg.Store == nil:
return nil, errors.New("new push publisher: nil store")
case cfg.StreamPublisher == nil:
return nil, errors.New("new push publisher: nil stream publisher")
case strings.TrimSpace(cfg.GatewayStream) == "":
return nil, errors.New("new push publisher: gateway stream must not be empty")
case cfg.GatewayStreamMaxLen <= 0:
@@ -180,6 +189,7 @@ func NewPushPublisher(cfg PushPublisherConfig, logger *slog.Logger) (*PushPublis
encoder: cfg.Encoder,
telemetry: cfg.Telemetry,
clock: cfg.Clock,
streamPublisher: cfg.StreamPublisher,
workerToken: workerToken,
logger: logger.With("component", "push_publisher", "stream", cfg.GatewayStream),
}, nil
@@ -260,7 +270,7 @@ func (publisher *PushPublisher) publishDueRoutes(ctx context.Context) (bool, err
return progress, nil
}
func (publisher *PushPublisher) publishRoute(ctx context.Context, now time.Time, dueRoute redisstate.ScheduledRoute) (bool, error) {
func (publisher *PushPublisher) 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)
@@ -306,7 +316,19 @@ func (publisher *PushPublisher) publishRoute(ctx context.Context, now time.Time,
return publisher.recordFailure(ctx, notification, route, pushFailureClassificationPayloadEncoding, err.Error())
}
err = publisher.store.CompleteRoutePublished(ctx, redisstate.CompleteRoutePublishedInput{
xaddArgs := &redis.XAddArgs{
Stream: publisher.gatewayStream,
Values: eventValues(event),
}
if publisher.gatewayStreamMaxLen > 0 {
xaddArgs.MaxLen = publisher.gatewayStreamMaxLen
xaddArgs.Approx = true
}
if err := publisher.streamPublisher.XAdd(ctx, xaddArgs).Err(); err != nil {
return publisher.recordFailure(ctx, notification, route, pushFailureClassificationGatewayStreamWrite, err.Error())
}
err = publisher.store.CompleteRoutePublished(ctx, routestate.CompleteRoutePublishedInput{
ExpectedRoute: route,
LeaseToken: publisher.workerToken,
PublishedAt: publisher.now(),
@@ -335,7 +357,7 @@ func (publisher *PushPublisher) publishRoute(ctx context.Context, now time.Time,
logArgs = append(logArgs, logging.TraceAttrsFromContext(ctx)...)
publisher.logger.Info("push 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, pushFailureClassificationGatewayStreamWrite, err.Error())
@@ -371,7 +393,7 @@ func (publisher *PushPublisher) 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,
@@ -384,7 +406,7 @@ func (publisher *PushPublisher) recordFailure(
publisher.recordRouteDeadLetter(ctx, notification, route, classification)
publisher.logger.Warn("push 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)
@@ -392,7 +414,7 @@ func (publisher *PushPublisher) 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,
@@ -407,7 +429,7 @@ func (publisher *PushPublisher) recordFailure(
logArgs = append(logArgs, "next_attempt_at", nextAttemptAt)
publisher.logger.Warn("push 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)