config: validate TSDB retention settings during config parsing

Move retention validation from tsdb/db.go into a TSDBRetentionConfig
UnmarshalYAML method so that invalid values are rejected at config
load/reload time rather than at apply time.

- Reject negative retention size values.
- Reject retention percentage values above 100.
- Simplify ApplyConfig to assign retention values unconditionally,
  enabling setting a value back to 0 to disable it.

Signed-off-by: Julien Pivotto <291750+roidelapluie@users.noreply.github.com>
This commit is contained in:
Julien Pivotto 2026-02-26 15:16:09 +01:00
parent d66944d856
commit dcfa1b96c6
7 changed files with 54 additions and 12 deletions

View file

@ -1097,6 +1097,22 @@ type TSDBRetentionConfig struct {
Percentage uint `yaml:"percentage,omitempty"`
}
// UnmarshalYAML implements the yaml.Unmarshaler interface.
func (t *TSDBRetentionConfig) UnmarshalYAML(unmarshal func(any) error) error {
*t = TSDBRetentionConfig{}
type plain TSDBRetentionConfig
if err := unmarshal((*plain)(t)); err != nil {
return err
}
if t.Size < 0 {
return fmt.Errorf("'storage.tsdb.retention.size' must be greater than or equal to 0, got %v", t.Size)
}
if t.Percentage > 100 {
return fmt.Errorf("'storage.tsdb.retention.percentage' must be in the range [0, 100], got %v", t.Percentage)
}
return nil
}
// TSDBConfig configures runtime reloadable configuration options.
type TSDBConfig struct {
// OutOfOrderTimeWindow sets how long back in time an out-of-order sample can be inserted

View file

@ -2626,6 +2626,22 @@ var expectedErrors = []struct {
filename: "stackit_endpoint.bad.yml",
errMsg: "invalid endpoint",
},
{
filename: "tsdb_retention_time.bad.yml",
errMsg: `not a valid duration string: "-1h"`,
},
{
filename: "tsdb_retention_size.bad.yml",
errMsg: `'storage.tsdb.retention.size' must be greater than or equal to 0`,
},
{
filename: "tsdb_retention_percentage.bad.yml",
errMsg: `'storage.tsdb.retention.percentage' must be in the range [0, 100]`,
},
{
filename: "tsdb_retention_percentage_negative.bad.yml",
errMsg: "cannot unmarshal !!int `-1` into uint",
},
}
func TestBadConfigs(t *testing.T) {

View file

@ -0,0 +1,4 @@
storage:
tsdb:
retention:
percentage: 101

View file

@ -0,0 +1,4 @@
storage:
tsdb:
retention:
percentage: -1

View file

@ -0,0 +1,4 @@
storage:
tsdb:
retention:
size: -1GB

View file

@ -0,0 +1,4 @@
storage:
tsdb:
retention:
time: -1h

View file

@ -1277,18 +1277,12 @@ func (db *DB) ApplyConfig(conf *config.Config) error {
// Update retention configuration if provided.
if conf.StorageConfig.TSDBConfig.Retention != nil {
db.retentionMtx.Lock()
if conf.StorageConfig.TSDBConfig.Retention.Time > 0 {
db.opts.RetentionDuration = int64(conf.StorageConfig.TSDBConfig.Retention.Time)
db.metrics.retentionDuration.Set((time.Duration(db.opts.RetentionDuration) * time.Millisecond).Seconds())
}
if conf.StorageConfig.TSDBConfig.Retention.Size > 0 {
db.opts.MaxBytes = int64(conf.StorageConfig.TSDBConfig.Retention.Size)
db.metrics.maxBytes.Set(float64(db.opts.MaxBytes))
}
if conf.StorageConfig.TSDBConfig.Retention.Percentage > 0 {
db.opts.MaxPercentage = conf.StorageConfig.TSDBConfig.Retention.Percentage
db.metrics.maxPercentage.Set(float64(db.opts.MaxPercentage))
}
db.opts.RetentionDuration = int64(conf.StorageConfig.TSDBConfig.Retention.Time)
db.metrics.retentionDuration.Set((time.Duration(db.opts.RetentionDuration) * time.Millisecond).Seconds())
db.opts.MaxBytes = int64(conf.StorageConfig.TSDBConfig.Retention.Size)
db.metrics.maxBytes.Set(float64(db.opts.MaxBytes))
db.opts.MaxPercentage = conf.StorageConfig.TSDBConfig.Retention.Percentage
db.metrics.maxPercentage.Set(float64(db.opts.MaxPercentage))
db.retentionMtx.Unlock()
}
} else {