Fail startup on an unparseable RETENTION_SWEEP_INTERVAL
All checks were successful
check / check (push) Successful in 5s

This commit is contained in:
2026-08-07 20:45:40 +07:00
parent f6abc9b392
commit d0e73c7e7f
2 changed files with 132 additions and 22 deletions

View File

@@ -106,19 +106,26 @@ func envInt(key string, defaultValue int) int {
// envDuration returns the value of the named environment variable // envDuration returns the value of the named environment variable
// parsed as a Go duration (e.g. "1h", "30m"). Returns defaultValue if // parsed as a Go duration (e.g. "1h", "30m"). Returns defaultValue if
// not set or unparseable. // not set. If the variable is set but cannot be parsed, it returns a
// wrapped error naming the key and the bad value, so startup fails
// loudly rather than silently falling back to the default.
func envDuration( func envDuration(
key string, key string,
defaultValue time.Duration, defaultValue time.Duration,
) time.Duration { ) (time.Duration, error) {
if v := os.Getenv(key); v != "" { v := os.Getenv(key)
d, err := time.ParseDuration(v) if v == "" {
if err == nil { return defaultValue, nil
return d
}
} }
return defaultValue d, err := time.ParseDuration(v)
if err != nil {
return 0, fmt.Errorf(
"invalid duration for %s: %q: %w", key, v, err,
)
}
return d, nil
} }
// New creates a Config by reading environment variables. // New creates a Config by reading environment variables.
@@ -144,6 +151,17 @@ func New(lc fx.Lifecycle, params ConfigParams) (*Config, error) {
) )
} }
// Parse the retention sweep interval; a set-but-unparseable value
// is a hard error so fx aborts startup rather than silently using
// the default.
retentionSweepInterval, err := envDuration(
"RETENTION_SWEEP_INTERVAL",
defaultRetentionSweepInterval,
)
if err != nil {
return nil, err
}
// Load configuration values from environment variables // Load configuration values from environment variables
s := &Config{ s := &Config{
DataDir: envString("DATA_DIR"), DataDir: envString("DATA_DIR"),
@@ -154,10 +172,7 @@ func New(lc fx.Lifecycle, params ConfigParams) (*Config, error) {
MetricsPassword: envString("METRICS_PASSWORD"), MetricsPassword: envString("METRICS_PASSWORD"),
Port: envInt("PORT", defaultPort), Port: envInt("PORT", defaultPort),
SentryDSN: envString("SENTRY_DSN"), SentryDSN: envString("SENTRY_DSN"),
RetentionSweepInterval: envDuration( RetentionSweepInterval: retentionSweepInterval,
"RETENTION_SWEEP_INTERVAL",
defaultRetentionSweepInterval,
),
log: log, log: log,
params: &params, params: &params,
} }

View File

@@ -3,6 +3,7 @@ package config_test
import ( import (
"os" "os"
"testing" "testing"
"time"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@@ -120,6 +121,100 @@ func testEnvironmentConfigSuccess(
assert.Equal(t, isProd, cfg.IsProd()) assert.Equal(t, isProd, cfg.IsProd())
} }
func TestRetentionSweepInterval(t *testing.T) {
tests := []struct {
name string
set bool
value string
expectError bool
expected time.Duration
}{
{
name: "unset uses default",
set: false,
expected: time.Hour,
},
{
name: "valid value is parsed",
set: true,
value: "15m",
expected: 15 * time.Minute,
},
{
name: "unparseable value fails startup",
set: true,
value: "not-a-duration",
expectError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Cannot use t.Parallel() here because t.Setenv
// is incompatible with parallel subtests.
t.Setenv("WEBHOOKER_ENVIRONMENT", "dev")
if tt.set {
t.Setenv("RETENTION_SWEEP_INTERVAL", tt.value)
} else {
require.NoError(t, os.Unsetenv(
"RETENTION_SWEEP_INTERVAL",
))
}
if tt.expectError {
testRetentionSweepIntervalError(t)
} else {
testRetentionSweepIntervalSuccess(t, tt.expected)
}
})
}
}
func testRetentionSweepIntervalError(t *testing.T) {
t.Helper()
var cfg *config.Config
app := fx.New(
fx.NopLogger,
fx.Provide(
globals.New,
logger.New,
config.New,
),
fx.Populate(&cfg),
)
assert.Error(t, app.Err())
}
func testRetentionSweepIntervalSuccess(
t *testing.T,
expected time.Duration,
) {
t.Helper()
var cfg *config.Config
app := fxtest.New(
t,
fx.Provide(
globals.New,
logger.New,
config.New,
),
fx.Populate(&cfg),
)
require.NoError(t, app.Err())
app.RequireStart()
defer app.RequireStop()
assert.Equal(t, expected, cfg.RetentionSweepInterval)
}
func TestDefaultDataDir(t *testing.T) { func TestDefaultDataDir(t *testing.T) {
for _, env := range []string{"", "dev", "prod"} { for _, env := range []string{"", "dev", "prod"} {
name := env name := env