Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 26 additions & 11 deletions cmd/litestream/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,17 +50,18 @@ var errStop = errors.New("stop")

// Sentinel errors for configuration validation
var (
ErrInvalidSnapshotInterval = errors.New("snapshot interval must be greater than 0")
ErrInvalidSnapshotRetention = errors.New("snapshot retention must be greater than 0")
ErrInvalidCompactionInterval = errors.New("compaction interval must be greater than 0")
ErrInvalidSyncInterval = errors.New("sync interval must be greater than 0")
ErrInvalidL0Retention = errors.New("l0 retention must not be negative")
ErrInvalidL0RetentionCheckInterval = errors.New("l0 retention check interval must be greater than 0")
ErrInvalidShutdownSyncTimeout = errors.New("shutdown-sync-timeout must be >= 0")
ErrInvalidShutdownSyncInterval = errors.New("shutdown sync interval must be greater than 0")
ErrInvalidHeartbeatURL = errors.New("heartbeat URL must be a valid HTTP or HTTPS URL")
ErrInvalidHeartbeatInterval = errors.New("heartbeat interval must be at least 1 minute")
ErrConfigFileNotFound = errors.New("config file not found")
ErrInvalidSnapshotInterval = errors.New("snapshot interval must be greater than 0")
ErrInvalidSnapshotRetention = errors.New("snapshot retention must be greater than 0")
ErrInvalidSnapshotRetentionInterval = errors.New("snapshot retention must not be shorter than snapshot interval")
ErrInvalidCompactionInterval = errors.New("compaction interval must be greater than 0")
ErrInvalidSyncInterval = errors.New("sync interval must be greater than 0")
ErrInvalidL0Retention = errors.New("l0 retention must not be negative")
ErrInvalidL0RetentionCheckInterval = errors.New("l0 retention check interval must be greater than 0")
ErrInvalidShutdownSyncTimeout = errors.New("shutdown-sync-timeout must be >= 0")
ErrInvalidShutdownSyncInterval = errors.New("shutdown sync interval must be greater than 0")
ErrInvalidHeartbeatURL = errors.New("heartbeat URL must be a valid HTTP or HTTPS URL")
ErrInvalidHeartbeatInterval = errors.New("heartbeat interval must be at least 1 minute")
ErrConfigFileNotFound = errors.New("config file not found")
)

// ConfigValidationError wraps a validation error with additional context
Expand Down Expand Up @@ -432,6 +433,13 @@ func (c *Config) Validate() error {
Value: *c.Snapshot.Retention,
}
}
if c.Snapshot.Interval != nil && c.Snapshot.Retention != nil && *c.Snapshot.Retention < *c.Snapshot.Interval {
return &ConfigValidationError{
Err: ErrInvalidSnapshotRetentionInterval,
Field: "snapshot.retention",
Value: *c.Snapshot.Retention,
}
}
if c.L0Retention != nil && *c.L0Retention < 0 {
return &ConfigValidationError{
Err: ErrInvalidL0Retention,
Expand Down Expand Up @@ -532,6 +540,13 @@ func (c *Config) Validate() error {
Value: *db.Snapshot.Retention,
}
}
if db.Snapshot.Interval != nil && db.Snapshot.Retention != nil && *db.Snapshot.Retention < *db.Snapshot.Interval {
return &ConfigValidationError{
Err: ErrInvalidSnapshotRetentionInterval,
Field: fmt.Sprintf("dbs[%s].snapshot.retention", dbIdentifier),
Value: *db.Snapshot.Retention,
}
}

// Validate sync intervals for replicas
if db.Replica != nil && db.Replica.SyncInterval != nil && *db.Replica.SyncInterval <= 0 {
Expand Down
32 changes: 32 additions & 0 deletions cmd/litestream/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -711,6 +711,38 @@ snapshot:
}
})

t.Run("RetentionShorterThanInterval", func(t *testing.T) {
yaml := `
snapshot:
interval: 1h
retention: 30m
`
_, err := main.ParseConfig(strings.NewReader(yaml), false)
if err == nil {
t.Fatal("expected error for retention shorter than snapshot interval")
}
if !errors.Is(err, main.ErrInvalidSnapshotRetentionInterval) {
t.Errorf("expected ErrInvalidSnapshotRetentionInterval, got %v", err)
}
})

t.Run("DBRetentionShorterThanInterval", func(t *testing.T) {
yaml := `
dbs:
- path: /tmp/test.db
snapshot:
interval: 1h
retention: 30m
`
_, err := main.ParseConfig(strings.NewReader(yaml), false)
if err == nil {
t.Fatal("expected error for database retention shorter than snapshot interval")
}
if !errors.Is(err, main.ErrInvalidSnapshotRetentionInterval) {
t.Errorf("expected ErrInvalidSnapshotRetentionInterval, got %v", err)
}
})

t.Run("NegativeInterval", func(t *testing.T) {
yaml := `
snapshot:
Expand Down
Loading