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
+76 -65
View File
@@ -8,10 +8,13 @@ import (
"time"
"galaxy/mail/internal/adapters/id"
"galaxy/mail/internal/adapters/postgres/mailstore"
"galaxy/mail/internal/adapters/postgres/migrations"
"galaxy/mail/internal/adapters/redisstate"
templatedir "galaxy/mail/internal/adapters/templates"
"galaxy/mail/internal/api/internalhttp"
"galaxy/mail/internal/config"
"galaxy/mail/internal/ports"
"galaxy/mail/internal/service/acceptauthdelivery"
"galaxy/mail/internal/service/acceptgenericdelivery"
"galaxy/mail/internal/service/executeattempt"
@@ -22,7 +25,7 @@ import (
"galaxy/mail/internal/service/resenddelivery"
"galaxy/mail/internal/telemetry"
"galaxy/mail/internal/worker"
"galaxy/mail/internal/ports"
"galaxy/postgres"
"github.com/redis/go-redis/v9"
)
@@ -47,11 +50,11 @@ type runtimeClock interface {
type runtimeProviderFactory func(config.SMTPConfig, *slog.Logger) (ports.Provider, error)
type runtimeDependencies struct {
clock runtimeClock
providerFactory runtimeProviderFactory
schedulerPoll time.Duration
schedulerRecovery time.Duration
schedulerGrace time.Duration
clock runtimeClock
providerFactory runtimeProviderFactory
schedulerPoll time.Duration
schedulerRecovery time.Duration
schedulerGrace time.Duration
}
func (deps runtimeDependencies) withDefaults() runtimeDependencies {
@@ -112,17 +115,58 @@ func newRuntime(ctx context.Context, cfg config.Config, logger *slog.Logger, dep
return telemetryRuntime.Shutdown(shutdownCtx)
})
// Open one shared Redis master client. The command consumer, the stream
// offset store, and the malformed-command recorder all borrow it.
redisClient := newRedisClient(cfg.Redis)
if err := instrumentRedisClient(redisClient, telemetryRuntime); err != nil {
return cleanupOnError(fmt.Errorf("new mail runtime: %w", err))
}
runtime.cleanupFns = append(runtime.cleanupFns, func() error {
return redisClient.Close()
if err := redisClient.Close(); err != nil && !errors.Is(err, redis.ErrClosed) {
return err
}
return nil
})
if err := pingRedis(ctx, cfg.Redis, redisClient); err != nil {
return cleanupOnError(fmt.Errorf("new mail runtime: %w", err))
}
// Open the PostgreSQL pool, attach instrumentation, ping it, run embedded
// migrations strictly before any HTTP listener opens. A failure at any of
// these steps is fatal.
pgPool, err := postgres.OpenPrimary(ctx, cfg.Postgres.Conn,
postgres.WithTracerProvider(telemetryRuntime.TracerProvider()),
postgres.WithMeterProvider(telemetryRuntime.MeterProvider()),
)
if err != nil {
return cleanupOnError(fmt.Errorf("new mail runtime: open postgres primary: %w", err))
}
runtime.cleanupFns = append(runtime.cleanupFns, pgPool.Close)
unregisterDBStats, err := postgres.InstrumentDBStats(pgPool,
postgres.WithMeterProvider(telemetryRuntime.MeterProvider()),
)
if err != nil {
return cleanupOnError(fmt.Errorf("new mail runtime: instrument postgres db stats: %w", err))
}
runtime.cleanupFns = append(runtime.cleanupFns, unregisterDBStats)
if err := postgres.Ping(ctx, pgPool, cfg.Postgres.Conn.OperationTimeout); err != nil {
return cleanupOnError(fmt.Errorf("new mail runtime: %w", err))
}
if err := postgres.RunMigrations(ctx, pgPool, migrations.FS(), "."); err != nil {
return cleanupOnError(fmt.Errorf("new mail runtime: run postgres migrations: %w", err))
}
store, err := mailstore.New(mailstore.Config{
DB: pgPool,
OperationTimeout: cfg.Postgres.Conn.OperationTimeout,
})
if err != nil {
return cleanupOnError(fmt.Errorf("new mail runtime: postgres mail store: %w", err))
}
if err := store.Ping(ctx); err != nil {
return cleanupOnError(fmt.Errorf("new mail runtime: ping postgres mail store: %w", err))
}
templateCatalog, err := newTemplateCatalog(cfg.Templates)
if err != nil {
return cleanupOnError(fmt.Errorf("new mail runtime: %w", err))
@@ -135,47 +179,35 @@ func newRuntime(ctx context.Context, cfg config.Config, logger *slog.Logger, dep
}
runtime.cleanupFns = append(runtime.cleanupFns, provider.Close)
acceptanceStore, err := redisstate.NewAcceptanceStore(redisClient)
if err != nil {
return cleanupOnError(fmt.Errorf("new mail runtime: auth acceptance store: %w", err))
}
authAcceptanceService, err := acceptauthdelivery.New(acceptauthdelivery.Config{
Store: acceptanceStore,
Store: store,
DeliveryIDGenerator: id.Generator{},
Clock: deps.clock,
Telemetry: telemetryRuntime,
TracerProvider: telemetryRuntime.TracerProvider(),
Logger: logger,
IdempotencyTTL: redisstate.IdempotencyTTL,
IdempotencyTTL: cfg.IdempotencyTTL,
SuppressOutbound: cfg.SMTP.Mode == config.SMTPModeStub,
})
if err != nil {
return cleanupOnError(fmt.Errorf("new mail runtime: auth acceptance service: %w", err))
}
genericAcceptanceStore, err := redisstate.NewGenericAcceptanceStore(redisClient)
if err != nil {
return cleanupOnError(fmt.Errorf("new mail runtime: generic acceptance store: %w", err))
}
genericAcceptanceService, err := acceptgenericdelivery.New(acceptgenericdelivery.Config{
Store: genericAcceptanceStore,
Store: store.GenericAcceptance(),
Clock: deps.clock,
Telemetry: telemetryRuntime,
TracerProvider: telemetryRuntime.TracerProvider(),
Logger: logger,
IdempotencyTTL: redisstate.IdempotencyTTL,
IdempotencyTTL: cfg.IdempotencyTTL,
})
if err != nil {
return cleanupOnError(fmt.Errorf("new mail runtime: generic acceptance service: %w", err))
}
renderStore, err := redisstate.NewRenderStore(redisClient)
if err != nil {
return cleanupOnError(fmt.Errorf("new mail runtime: render store: %w", err))
}
renderDeliveryService, err := renderdelivery.New(renderdelivery.Config{
Catalog: templateCatalog,
Store: renderStore,
Store: store.RenderDelivery(),
Clock: deps.clock,
Telemetry: telemetryRuntime,
TracerProvider: telemetryRuntime.TracerProvider(),
@@ -186,27 +218,18 @@ func newRuntime(ctx context.Context, cfg config.Config, logger *slog.Logger, dep
}
runtime.renderDeliveryService = renderDeliveryService
malformedCommandStore, err := redisstate.NewMalformedCommandStore(redisClient)
if err != nil {
return cleanupOnError(fmt.Errorf("new mail runtime: malformed command store: %w", err))
}
streamOffsetStore, err := redisstate.NewStreamOffsetStore(redisClient)
if err != nil {
return cleanupOnError(fmt.Errorf("new mail runtime: stream offset store: %w", err))
}
attemptExecutionStore, err := redisstate.NewAttemptExecutionStore(redisClient)
if err != nil {
return cleanupOnError(fmt.Errorf("new mail runtime: attempt execution store: %w", err))
}
attemptExecutionStore := store.AttemptExecution()
telemetryRuntime.SetAttemptScheduleSnapshotReader(attemptExecutionStore)
operatorStore, err := redisstate.NewOperatorStore(redisClient)
if err != nil {
return cleanupOnError(fmt.Errorf("new mail runtime: operator store: %w", err))
}
attemptExecutionService, err := executeattempt.New(executeattempt.Config{
Renderer: renderDeliveryService,
Provider: provider,
PayloadLoader: attemptExecutionStore,
PayloadLoader: store,
Store: attemptExecutionStore,
Clock: deps.clock,
Telemetry: telemetryRuntime,
@@ -217,26 +240,27 @@ func newRuntime(ctx context.Context, cfg config.Config, logger *slog.Logger, dep
if err != nil {
return cleanupOnError(fmt.Errorf("new mail runtime: attempt execution service: %w", err))
}
listDeliveriesService, err := listdeliveries.New(listdeliveries.Config{
Store: operatorStore,
Store: store,
})
if err != nil {
return cleanupOnError(fmt.Errorf("new mail runtime: list deliveries service: %w", err))
}
getDeliveryService, err := getdelivery.New(getdelivery.Config{
Store: operatorStore,
Store: store,
})
if err != nil {
return cleanupOnError(fmt.Errorf("new mail runtime: get delivery service: %w", err))
}
listAttemptsService, err := listattempts.New(listattempts.Config{
Store: operatorStore,
Store: store,
})
if err != nil {
return cleanupOnError(fmt.Errorf("new mail runtime: list attempts service: %w", err))
}
resendDeliveryService, err := resenddelivery.New(resenddelivery.Config{
Store: operatorStore,
Store: store,
DeliveryIDGenerator: id.Generator{},
Clock: deps.clock,
Telemetry: telemetryRuntime,
@@ -247,21 +271,6 @@ func newRuntime(ctx context.Context, cfg config.Config, logger *slog.Logger, dep
return cleanupOnError(fmt.Errorf("new mail runtime: resend delivery service: %w", err))
}
commandConsumerRedisClient := newRedisClient(cfg.Redis)
if err := instrumentRedisClient(commandConsumerRedisClient, telemetryRuntime); err != nil {
return cleanupOnError(fmt.Errorf("new mail runtime: %w", err))
}
runtime.cleanupFns = append(runtime.cleanupFns, func() error {
err := commandConsumerRedisClient.Close()
if errors.Is(err, redis.ErrClosed) {
return nil
}
return err
})
if err := pingRedis(ctx, cfg.Redis, commandConsumerRedisClient); err != nil {
return cleanupOnError(fmt.Errorf("new mail runtime: %w", err))
}
httpServer, err := internalhttp.NewServer(internalhttp.Config{
Addr: cfg.InternalHTTP.Addr,
ReadHeaderTimeout: cfg.InternalHTTP.ReadHeaderTimeout,
@@ -282,11 +291,11 @@ func newRuntime(ctx context.Context, cfg config.Config, logger *slog.Logger, dep
}
commandConsumer, err := worker.NewCommandConsumer(worker.CommandConsumerConfig{
Client: commandConsumerRedisClient,
Client: redisClient,
Stream: cfg.Redis.CommandStream,
BlockTimeout: cfg.StreamBlockTimeout,
Acceptor: genericAcceptanceService,
MalformedRecorder: malformedCommandStore,
MalformedRecorder: store,
OffsetStore: streamOffsetStore,
Telemetry: telemetryRuntime,
Clock: deps.clock,
@@ -317,16 +326,18 @@ func newRuntime(ctx context.Context, cfg config.Config, logger *slog.Logger, dep
if err != nil {
return cleanupOnError(fmt.Errorf("new mail runtime: attempt worker pool: %w", err))
}
indexCleaner, err := redisstate.NewIndexCleaner(redisClient)
retentionWorker, err := worker.NewSQLRetentionWorker(worker.SQLRetentionConfig{
Store: store,
DeliveryRetention: cfg.Retention.DeliveryRetention,
MalformedCommandRetention: cfg.Retention.MalformedCommandRetention,
CleanupInterval: cfg.Retention.CleanupInterval,
Clock: deps.clock,
}, logger)
if err != nil {
return cleanupOnError(fmt.Errorf("new mail runtime: cleanup index cleaner: %w", err))
}
cleanupWorker, err := worker.NewCleanupWorker(indexCleaner, logger)
if err != nil {
return cleanupOnError(fmt.Errorf("new mail runtime: cleanup worker: %w", err))
return cleanupOnError(fmt.Errorf("new mail runtime: sql retention worker: %w", err))
}
runtime.app = New(cfg, httpServer, commandConsumer, scheduler, attemptWorkers, cleanupWorker)
runtime.app = New(cfg, httpServer, commandConsumer, scheduler, attemptWorkers, retentionWorker)
return runtime, nil
}