feat: use postgres
This commit is contained in:
@@ -5,7 +5,11 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"galaxy/notification/internal/adapters/postgres/migrations"
|
||||
"galaxy/notification/internal/adapters/postgres/notificationstore"
|
||||
"galaxy/notification/internal/adapters/postgres/routepublisher"
|
||||
redisadapter "galaxy/notification/internal/adapters/redis"
|
||||
"galaxy/notification/internal/adapters/redisstate"
|
||||
userserviceadapter "galaxy/notification/internal/adapters/userservice"
|
||||
@@ -14,10 +18,16 @@ import (
|
||||
"galaxy/notification/internal/service/acceptintent"
|
||||
"galaxy/notification/internal/telemetry"
|
||||
"galaxy/notification/internal/worker"
|
||||
"galaxy/postgres"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
// systemClock satisfies the worker.Clock contract for runtime wiring.
|
||||
type systemClock struct{}
|
||||
|
||||
func (systemClock) Now() time.Time { return time.Now() }
|
||||
|
||||
// Runtime owns the runnable Notification Service process plus the cleanup
|
||||
// functions that release runtime resources after shutdown.
|
||||
type Runtime struct {
|
||||
@@ -25,16 +35,24 @@ type Runtime struct {
|
||||
|
||||
app *App
|
||||
|
||||
probeServer *internalhttp.Server
|
||||
telemetry *telemetry.Runtime
|
||||
intentConsumer *worker.IntentConsumer
|
||||
pushPublisher *worker.PushPublisher
|
||||
emailPublisher *worker.EmailPublisher
|
||||
probeServer *internalhttp.Server
|
||||
telemetry *telemetry.Runtime
|
||||
intentConsumer *worker.IntentConsumer
|
||||
pushPublisher *worker.PushPublisher
|
||||
emailPublisher *worker.EmailPublisher
|
||||
retentionWorker *worker.SQLRetentionWorker
|
||||
|
||||
cleanupFns []func() error
|
||||
}
|
||||
|
||||
// NewRuntime constructs the runnable Notification Service process from cfg.
|
||||
//
|
||||
// PostgreSQL migrations apply strictly before any HTTP listener becomes
|
||||
// ready. The runtime opens one shared `*redis.Client` consumed by the intent
|
||||
// consumer (XREAD), the publishers (outbound XADDs), the route lease store,
|
||||
// and the persisted stream offset store. Per PG_PLAN.md §5 the durable
|
||||
// notification state lives in PostgreSQL while the lease key, the consumer
|
||||
// offset, and the streams themselves remain on Redis.
|
||||
func NewRuntime(ctx context.Context, cfg config.Config, logger *slog.Logger) (*Runtime, error) {
|
||||
if ctx == nil {
|
||||
return nil, fmt.Errorf("new notification runtime: nil context")
|
||||
@@ -91,17 +109,42 @@ func NewRuntime(ctx context.Context, cfg config.Config, logger *slog.Logger) (*R
|
||||
return cleanupOnError(fmt.Errorf("new notification runtime: %w", err))
|
||||
}
|
||||
|
||||
acceptanceStore, err := redisstate.NewAcceptanceStore(redisClient, redisstate.AcceptanceConfig{
|
||||
RecordTTL: cfg.Retry.RecordTTL,
|
||||
DeadLetterTTL: cfg.Retry.DeadLetterTTL,
|
||||
IdempotencyTTL: cfg.Retry.IdempotencyTTL,
|
||||
pgPool, err := postgres.OpenPrimary(ctx, cfg.Postgres.Conn,
|
||||
postgres.WithTracerProvider(telemetryRuntime.TracerProvider()),
|
||||
postgres.WithMeterProvider(telemetryRuntime.MeterProvider()),
|
||||
)
|
||||
if err != nil {
|
||||
return cleanupOnError(fmt.Errorf("new notification runtime: open postgres: %w", err))
|
||||
}
|
||||
runtime.cleanupFns = append(runtime.cleanupFns, pgPool.Close)
|
||||
unregisterPGStats, err := postgres.InstrumentDBStats(pgPool,
|
||||
postgres.WithMeterProvider(telemetryRuntime.MeterProvider()),
|
||||
)
|
||||
if err != nil {
|
||||
return cleanupOnError(fmt.Errorf("new notification runtime: instrument postgres: %w", err))
|
||||
}
|
||||
runtime.cleanupFns = append(runtime.cleanupFns, func() error {
|
||||
unregisterPGStats()
|
||||
return nil
|
||||
})
|
||||
if err := postgres.Ping(ctx, pgPool, cfg.Postgres.Conn.OperationTimeout); err != nil {
|
||||
return cleanupOnError(fmt.Errorf("new notification runtime: ping postgres: %w", err))
|
||||
}
|
||||
if err := postgres.RunMigrations(ctx, pgPool, migrations.FS(), "."); err != nil {
|
||||
return cleanupOnError(fmt.Errorf("new notification runtime: run postgres migrations: %w", err))
|
||||
}
|
||||
|
||||
notificationStore, err := notificationstore.New(notificationstore.Config{
|
||||
DB: pgPool,
|
||||
OperationTimeout: cfg.Postgres.Conn.OperationTimeout,
|
||||
})
|
||||
if err != nil {
|
||||
return cleanupOnError(fmt.Errorf("new notification runtime: acceptance store: %w", err))
|
||||
return cleanupOnError(fmt.Errorf("new notification runtime: notification store: %w", err))
|
||||
}
|
||||
malformedIntentStore, err := redisstate.NewMalformedIntentStore(redisClient, cfg.Retry.DeadLetterTTL)
|
||||
|
||||
leaseStore, err := redisstate.NewLeaseStore(redisClient)
|
||||
if err != nil {
|
||||
return cleanupOnError(fmt.Errorf("new notification runtime: malformed intent store: %w", err))
|
||||
return cleanupOnError(fmt.Errorf("new notification runtime: lease store: %w", err))
|
||||
}
|
||||
streamOffsetStore, err := redisstate.NewStreamOffsetStore(redisClient)
|
||||
if err != nil {
|
||||
@@ -111,8 +154,14 @@ func NewRuntime(ctx context.Context, cfg config.Config, logger *slog.Logger) (*R
|
||||
if err != nil {
|
||||
return cleanupOnError(fmt.Errorf("new notification runtime: intent stream lag reader: %w", err))
|
||||
}
|
||||
telemetryRuntime.SetRouteScheduleSnapshotReader(acceptanceStore)
|
||||
publisherStore, err := routepublisher.New(notificationStore, leaseStore)
|
||||
if err != nil {
|
||||
return cleanupOnError(fmt.Errorf("new notification runtime: route publisher store: %w", err))
|
||||
}
|
||||
|
||||
telemetryRuntime.SetRouteScheduleSnapshotReader(notificationStore)
|
||||
telemetryRuntime.SetIntentStreamLagSnapshotReader(intentStreamLagReader)
|
||||
|
||||
userDirectory, err := userserviceadapter.NewClient(userserviceadapter.Config{
|
||||
BaseURL: cfg.UserService.BaseURL,
|
||||
RequestTimeout: cfg.UserService.Timeout,
|
||||
@@ -121,8 +170,9 @@ func NewRuntime(ctx context.Context, cfg config.Config, logger *slog.Logger) (*R
|
||||
return cleanupOnError(fmt.Errorf("new notification runtime: user service client: %w", err))
|
||||
}
|
||||
runtime.cleanupFns = append(runtime.cleanupFns, userDirectory.Close)
|
||||
|
||||
acceptIntentService, err := acceptintent.New(acceptintent.Config{
|
||||
Store: acceptanceStore,
|
||||
Store: notificationStore,
|
||||
UserDirectory: userDirectory,
|
||||
Clock: nil,
|
||||
Logger: logger,
|
||||
@@ -140,7 +190,7 @@ func NewRuntime(ctx context.Context, cfg config.Config, logger *slog.Logger) (*R
|
||||
Stream: cfg.Streams.Intents,
|
||||
BlockTimeout: cfg.IntentsReadBlockTimeout,
|
||||
Acceptor: acceptIntentService,
|
||||
MalformedRecorder: malformedIntentStore,
|
||||
MalformedRecorder: notificationStore,
|
||||
OffsetStore: streamOffsetStore,
|
||||
Telemetry: telemetryRuntime,
|
||||
}, logger)
|
||||
@@ -149,7 +199,7 @@ func NewRuntime(ctx context.Context, cfg config.Config, logger *slog.Logger) (*R
|
||||
}
|
||||
runtime.intentConsumer = intentConsumer
|
||||
pushPublisher, err := worker.NewPushPublisher(worker.PushPublisherConfig{
|
||||
Store: acceptanceStore,
|
||||
Store: publisherStore,
|
||||
GatewayStream: cfg.Streams.GatewayClientEvents,
|
||||
GatewayStreamMaxLen: cfg.Streams.GatewayClientEventsStreamMaxLen,
|
||||
RouteLeaseTTL: cfg.Retry.RouteLeaseTTL,
|
||||
@@ -158,13 +208,14 @@ func NewRuntime(ctx context.Context, cfg config.Config, logger *slog.Logger) (*R
|
||||
Encoder: nil,
|
||||
Telemetry: telemetryRuntime,
|
||||
Clock: nil,
|
||||
StreamPublisher: redisClient,
|
||||
}, logger)
|
||||
if err != nil {
|
||||
return cleanupOnError(fmt.Errorf("new notification runtime: push publisher: %w", err))
|
||||
}
|
||||
runtime.pushPublisher = pushPublisher
|
||||
emailPublisher, err := worker.NewEmailPublisher(worker.EmailPublisherConfig{
|
||||
Store: acceptanceStore,
|
||||
Store: publisherStore,
|
||||
MailDeliveryCommandsStream: cfg.Streams.MailDeliveryCommands,
|
||||
RouteLeaseTTL: cfg.Retry.RouteLeaseTTL,
|
||||
RouteBackoffMin: cfg.Retry.RouteBackoffMin,
|
||||
@@ -172,12 +223,25 @@ func NewRuntime(ctx context.Context, cfg config.Config, logger *slog.Logger) (*R
|
||||
Encoder: nil,
|
||||
Telemetry: telemetryRuntime,
|
||||
Clock: nil,
|
||||
StreamPublisher: redisClient,
|
||||
}, logger)
|
||||
if err != nil {
|
||||
return cleanupOnError(fmt.Errorf("new notification runtime: email publisher: %w", err))
|
||||
}
|
||||
runtime.emailPublisher = emailPublisher
|
||||
|
||||
retentionWorker, err := worker.NewSQLRetentionWorker(worker.SQLRetentionConfig{
|
||||
Store: notificationStore,
|
||||
RecordRetention: cfg.Retention.RecordRetention,
|
||||
MalformedIntentRetention: cfg.Retention.MalformedIntentRetention,
|
||||
CleanupInterval: cfg.Retention.CleanupInterval,
|
||||
Clock: systemClock{},
|
||||
}, logger)
|
||||
if err != nil {
|
||||
return cleanupOnError(fmt.Errorf("new notification runtime: sql retention worker: %w", err))
|
||||
}
|
||||
runtime.retentionWorker = retentionWorker
|
||||
|
||||
probeServer, err := internalhttp.NewServer(internalhttp.Config{
|
||||
Addr: cfg.InternalHTTP.Addr,
|
||||
ReadHeaderTimeout: cfg.InternalHTTP.ReadHeaderTimeout,
|
||||
@@ -191,7 +255,7 @@ func NewRuntime(ctx context.Context, cfg config.Config, logger *slog.Logger) (*R
|
||||
return cleanupOnError(fmt.Errorf("new notification runtime: internal HTTP server: %w", err))
|
||||
}
|
||||
runtime.probeServer = probeServer
|
||||
runtime.app = New(cfg, probeServer, intentConsumer, pushPublisher, emailPublisher)
|
||||
runtime.app = New(cfg, probeServer, intentConsumer, pushPublisher, emailPublisher, retentionWorker)
|
||||
|
||||
return runtime, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user