feat: use postgres
This commit is contained in:
+103
-102
@@ -3,15 +3,18 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"galaxy/mail/internal/telemetry"
|
||||
"galaxy/postgres"
|
||||
"galaxy/redisconn"
|
||||
)
|
||||
|
||||
const (
|
||||
envPrefix = "MAIL"
|
||||
|
||||
shutdownTimeoutEnvVar = "MAIL_SHUTDOWN_TIMEOUT"
|
||||
logLevelEnvVar = "MAIL_LOG_LEVEL"
|
||||
|
||||
@@ -20,15 +23,7 @@ const (
|
||||
internalHTTPReadTimeoutEnvVar = "MAIL_INTERNAL_HTTP_READ_TIMEOUT"
|
||||
internalHTTPIdleTimeoutEnvVar = "MAIL_INTERNAL_HTTP_IDLE_TIMEOUT"
|
||||
|
||||
redisAddrEnvVar = "MAIL_REDIS_ADDR"
|
||||
redisUsernameEnvVar = "MAIL_REDIS_USERNAME"
|
||||
redisPasswordEnvVar = "MAIL_REDIS_PASSWORD"
|
||||
redisDBEnvVar = "MAIL_REDIS_DB"
|
||||
redisTLSEnabledEnvVar = "MAIL_REDIS_TLS_ENABLED"
|
||||
redisOperationTimeoutEnvVar = "MAIL_REDIS_OPERATION_TIMEOUT"
|
||||
redisCommandStreamEnvVar = "MAIL_REDIS_COMMAND_STREAM"
|
||||
redisAttemptScheduleEnvVar = "MAIL_REDIS_ATTEMPT_SCHEDULE_KEY"
|
||||
redisDeadLetterPrefixEnvVar = "MAIL_REDIS_DEAD_LETTER_PREFIX"
|
||||
redisCommandStreamEnvVar = "MAIL_REDIS_COMMAND_STREAM"
|
||||
|
||||
smtpModeEnvVar = "MAIL_SMTP_MODE"
|
||||
smtpAddrEnvVar = "MAIL_SMTP_ADDR"
|
||||
@@ -45,8 +40,10 @@ const (
|
||||
streamBlockTimeoutEnvVar = "MAIL_STREAM_BLOCK_TIMEOUT"
|
||||
operatorRequestTimeoutEnvVar = "MAIL_OPERATOR_REQUEST_TIMEOUT"
|
||||
idempotencyTTLEnvVar = "MAIL_IDEMPOTENCY_TTL"
|
||||
deliveryTTLEnvVar = "MAIL_DELIVERY_TTL"
|
||||
attemptTTLEnvVar = "MAIL_ATTEMPT_TTL"
|
||||
|
||||
deliveryRetentionEnvVar = "MAIL_DELIVERY_RETENTION"
|
||||
malformedCommandRetentionEnvVar = "MAIL_MALFORMED_COMMAND_RETENTION"
|
||||
cleanupIntervalEnvVar = "MAIL_CLEANUP_INTERVAL"
|
||||
|
||||
otelServiceNameEnvVar = "OTEL_SERVICE_NAME"
|
||||
otelTracesExporterEnvVar = "OTEL_TRACES_EXPORTER"
|
||||
@@ -57,27 +54,24 @@ const (
|
||||
otelStdoutTracesEnabledEnvVar = "MAIL_OTEL_STDOUT_TRACES_ENABLED"
|
||||
otelStdoutMetricsEnabledEnvVar = "MAIL_OTEL_STDOUT_METRICS_ENABLED"
|
||||
|
||||
defaultShutdownTimeout = 5 * time.Second
|
||||
defaultLogLevel = "info"
|
||||
defaultInternalHTTPAddr = ":8080"
|
||||
defaultReadHeaderTimeout = 2 * time.Second
|
||||
defaultReadTimeout = 10 * time.Second
|
||||
defaultIdleTimeout = time.Minute
|
||||
defaultRedisDB = 0
|
||||
defaultRedisOperationTimeout = 250 * time.Millisecond
|
||||
defaultRedisCommandStream = "mail:delivery_commands"
|
||||
defaultRedisAttemptScheduleKey = "mail:attempt_schedule"
|
||||
defaultRedisDeadLetterPrefix = "mail:dead_letters:"
|
||||
defaultSMTPMode = SMTPModeStub
|
||||
defaultSMTPTimeout = 15 * time.Second
|
||||
defaultTemplateDir = "templates"
|
||||
defaultAttemptWorkerCount = 4
|
||||
defaultStreamBlockTimeout = 2 * time.Second
|
||||
defaultOperatorRequestTimeout = 5 * time.Second
|
||||
defaultIdempotencyTTL = 7 * 24 * time.Hour
|
||||
defaultDeliveryTTL = 30 * 24 * time.Hour
|
||||
defaultAttemptTTL = 90 * 24 * time.Hour
|
||||
defaultOTelServiceName = "galaxy-mail"
|
||||
defaultShutdownTimeout = 5 * time.Second
|
||||
defaultLogLevel = "info"
|
||||
defaultInternalHTTPAddr = ":8080"
|
||||
defaultReadHeaderTimeout = 2 * time.Second
|
||||
defaultReadTimeout = 10 * time.Second
|
||||
defaultIdleTimeout = time.Minute
|
||||
defaultRedisCommandStream = "mail:delivery_commands"
|
||||
defaultSMTPMode = SMTPModeStub
|
||||
defaultSMTPTimeout = 15 * time.Second
|
||||
defaultTemplateDir = "templates"
|
||||
defaultAttemptWorkerCount = 4
|
||||
defaultStreamBlockTimeout = 2 * time.Second
|
||||
defaultOperatorRequestTimeout = 5 * time.Second
|
||||
defaultIdempotencyTTL = 7 * 24 * time.Hour
|
||||
defaultDeliveryRetention = 30 * 24 * time.Hour
|
||||
defaultMalformedCommandRetention = 90 * 24 * time.Hour
|
||||
defaultCleanupInterval = time.Hour
|
||||
defaultOTelServiceName = "galaxy-mail"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -99,10 +93,15 @@ type Config struct {
|
||||
// InternalHTTP configures the trusted internal HTTP listener.
|
||||
InternalHTTP InternalHTTPConfig
|
||||
|
||||
// Redis configures the shared Redis client and Redis-owned keys used by the
|
||||
// runnable service skeleton.
|
||||
// Redis configures the shared Redis connection topology and the inbound
|
||||
// `mail:delivery_commands` Stream key. Durable mail state lives in
|
||||
// PostgreSQL after Stage 4 of `PG_PLAN.md`.
|
||||
Redis RedisConfig
|
||||
|
||||
// Postgres configures the PostgreSQL-backed durable store consumed via
|
||||
// `pkg/postgres`.
|
||||
Postgres PostgresConfig
|
||||
|
||||
// SMTP configures the runtime mail provider mode and provider-specific
|
||||
// connection details.
|
||||
SMTP SMTPConfig
|
||||
@@ -115,22 +114,20 @@ type Config struct {
|
||||
AttemptWorkerConcurrency int
|
||||
|
||||
// StreamBlockTimeout stores the maximum Redis Streams blocking read window
|
||||
// used by the future command consumer.
|
||||
// used by the command consumer.
|
||||
StreamBlockTimeout time.Duration
|
||||
|
||||
// OperatorRequestTimeout stores the future application-layer request budget
|
||||
// for trusted operator handlers.
|
||||
// OperatorRequestTimeout stores the application-layer request budget for
|
||||
// trusted operator handlers.
|
||||
OperatorRequestTimeout time.Duration
|
||||
|
||||
// IdempotencyTTL stores the configured retention for idempotency records.
|
||||
// IdempotencyTTL stores the per-acceptance idempotency window the service
|
||||
// layer applies to the durable idempotency_expires_at column on
|
||||
// `deliveries`.
|
||||
IdempotencyTTL time.Duration
|
||||
|
||||
// DeliveryTTL stores the configured retention for delivery records.
|
||||
DeliveryTTL time.Duration
|
||||
|
||||
// AttemptTTL stores the configured retention for attempt and dead-letter
|
||||
// records.
|
||||
AttemptTTL time.Duration
|
||||
// Retention stores the periodic SQL retention worker configuration.
|
||||
Retention RetentionConfig
|
||||
|
||||
// Telemetry configures the process-wide OpenTelemetry runtime.
|
||||
Telemetry TelemetryConfig
|
||||
@@ -176,66 +173,67 @@ func (cfg InternalHTTPConfig) Validate() error {
|
||||
}
|
||||
}
|
||||
|
||||
// RedisConfig configures the shared Redis client used by the runnable process.
|
||||
// RedisConfig configures the Mail Service Redis connection topology plus the
|
||||
// inbound `mail:delivery_commands` Stream key. Per-call timeouts live in
|
||||
// `Conn.OperationTimeout`.
|
||||
type RedisConfig struct {
|
||||
// Addr stores the Redis network address.
|
||||
Addr string
|
||||
|
||||
// Username stores the optional Redis ACL username.
|
||||
Username string
|
||||
|
||||
// Password stores the optional Redis ACL password.
|
||||
Password string
|
||||
|
||||
// DB stores the Redis logical database index.
|
||||
DB int
|
||||
|
||||
// TLSEnabled reports whether TLS must be used for Redis connections.
|
||||
TLSEnabled bool
|
||||
|
||||
// OperationTimeout bounds one Redis round trip including the startup PING.
|
||||
OperationTimeout time.Duration
|
||||
// Conn carries the connection topology (master, replicas, password, db,
|
||||
// per-call timeout). Loaded via redisconn.LoadFromEnv("MAIL").
|
||||
Conn redisconn.Config
|
||||
|
||||
// CommandStream stores the configured Redis Streams key for async command
|
||||
// intake.
|
||||
CommandStream string
|
||||
|
||||
// AttemptScheduleKey stores the configured sorted-set key of scheduled
|
||||
// attempts.
|
||||
AttemptScheduleKey string
|
||||
|
||||
// DeadLetterPrefix stores the configured Redis key prefix of dead-letter
|
||||
// entries.
|
||||
DeadLetterPrefix string
|
||||
}
|
||||
|
||||
// TLSConfig returns the conservative TLS configuration used by the Redis
|
||||
// client when TLSEnabled is true.
|
||||
func (cfg RedisConfig) TLSConfig() *tls.Config {
|
||||
if !cfg.TLSEnabled {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &tls.Config{MinVersion: tls.VersionTLS12}
|
||||
}
|
||||
|
||||
// Validate reports whether cfg stores a usable Redis configuration.
|
||||
func (cfg RedisConfig) Validate() error {
|
||||
switch {
|
||||
case strings.TrimSpace(cfg.Addr) == "":
|
||||
return fmt.Errorf("redis addr must not be empty")
|
||||
case !isTCPAddr(cfg.Addr):
|
||||
return fmt.Errorf("redis addr %q must use host:port form", cfg.Addr)
|
||||
case cfg.DB < 0:
|
||||
return fmt.Errorf("redis db must not be negative")
|
||||
case cfg.OperationTimeout <= 0:
|
||||
return fmt.Errorf("redis operation timeout must be positive")
|
||||
case strings.TrimSpace(cfg.CommandStream) == "":
|
||||
if err := cfg.Conn.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
if strings.TrimSpace(cfg.CommandStream) == "" {
|
||||
return fmt.Errorf("redis command stream must not be empty")
|
||||
case strings.TrimSpace(cfg.AttemptScheduleKey) == "":
|
||||
return fmt.Errorf("redis attempt schedule key must not be empty")
|
||||
case strings.TrimSpace(cfg.DeadLetterPrefix) == "":
|
||||
return fmt.Errorf("redis dead-letter prefix must not be empty")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// PostgresConfig configures the PostgreSQL-backed durable store.
|
||||
type PostgresConfig struct {
|
||||
// Conn stores the primary plus replica DSN topology and pool tuning.
|
||||
// Loaded via postgres.LoadFromEnv("MAIL").
|
||||
Conn postgres.Config
|
||||
}
|
||||
|
||||
// Validate reports whether cfg stores a usable PostgreSQL configuration.
|
||||
func (cfg PostgresConfig) Validate() error {
|
||||
return cfg.Conn.Validate()
|
||||
}
|
||||
|
||||
// RetentionConfig stores the durable retention windows applied by the
|
||||
// periodic SQL retention worker.
|
||||
type RetentionConfig struct {
|
||||
// DeliveryRetention bounds how long deliveries (and their cascaded
|
||||
// attempts, dead letters, recipients, payloads) survive after creation.
|
||||
DeliveryRetention time.Duration
|
||||
|
||||
// MalformedCommandRetention bounds how long malformed-command rows
|
||||
// survive after their original recorded_at.
|
||||
MalformedCommandRetention time.Duration
|
||||
|
||||
// CleanupInterval stores the wall-clock period between two retention
|
||||
// passes.
|
||||
CleanupInterval time.Duration
|
||||
}
|
||||
|
||||
// Validate reports whether cfg stores a usable retention configuration.
|
||||
func (cfg RetentionConfig) Validate() error {
|
||||
switch {
|
||||
case cfg.DeliveryRetention <= 0:
|
||||
return fmt.Errorf("%s must be positive", deliveryRetentionEnvVar)
|
||||
case cfg.MalformedCommandRetention <= 0:
|
||||
return fmt.Errorf("%s must be positive", malformedCommandRetentionEnvVar)
|
||||
case cfg.CleanupInterval <= 0:
|
||||
return fmt.Errorf("%s must be positive", cleanupIntervalEnvVar)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
@@ -356,11 +354,11 @@ func DefaultConfig() Config {
|
||||
IdleTimeout: defaultIdleTimeout,
|
||||
},
|
||||
Redis: RedisConfig{
|
||||
DB: defaultRedisDB,
|
||||
OperationTimeout: defaultRedisOperationTimeout,
|
||||
CommandStream: defaultRedisCommandStream,
|
||||
AttemptScheduleKey: defaultRedisAttemptScheduleKey,
|
||||
DeadLetterPrefix: defaultRedisDeadLetterPrefix,
|
||||
Conn: redisconn.DefaultConfig(),
|
||||
CommandStream: defaultRedisCommandStream,
|
||||
},
|
||||
Postgres: PostgresConfig{
|
||||
Conn: postgres.DefaultConfig(),
|
||||
},
|
||||
SMTP: SMTPConfig{
|
||||
Mode: defaultSMTPMode,
|
||||
@@ -373,8 +371,11 @@ func DefaultConfig() Config {
|
||||
StreamBlockTimeout: defaultStreamBlockTimeout,
|
||||
OperatorRequestTimeout: defaultOperatorRequestTimeout,
|
||||
IdempotencyTTL: defaultIdempotencyTTL,
|
||||
DeliveryTTL: defaultDeliveryTTL,
|
||||
AttemptTTL: defaultAttemptTTL,
|
||||
Retention: RetentionConfig{
|
||||
DeliveryRetention: defaultDeliveryRetention,
|
||||
MalformedCommandRetention: defaultMalformedCommandRetention,
|
||||
CleanupInterval: defaultCleanupInterval,
|
||||
},
|
||||
Telemetry: TelemetryConfig{
|
||||
ServiceName: defaultOTelServiceName,
|
||||
TracesExporter: "none",
|
||||
|
||||
Reference in New Issue
Block a user