From 3856195bb8ecddb20cc77b9285180eb66806829a Mon Sep 17 00:00:00 2001 From: Julien Pivotto <291750+roidelapluie@users.noreply.github.com> Date: Thu, 26 Mar 2026 12:33:49 +0100 Subject: [PATCH] tsdb: use float64 for retention percentage The retention.percentage config field was typed as uint, which silently truncated fractional values. Setting percentage: 1.5 in prometheus.yml resulted in a retention of 1%, with no warning or error. Remove the redundant MaxPercentage > 100 clamp in main.go; the config UnmarshalYAML already returns an error for out-of-range values before this code is reached. Signed-off-by: Julien Pivotto <291750+roidelapluie@users.noreply.github.com> --- cmd/prometheus/main.go | 8 ++------ config/config.go | 4 ++-- config/config_test.go | 8 +++++++- .../testdata/tsdb_retention_percentage_float.good.yml | 4 ++++ tsdb/db.go | 10 +++++----- tsdb/db_test.go | 4 ++-- web/web.go | 4 ++-- 7 files changed, 24 insertions(+), 18 deletions(-) create mode 100644 config/testdata/tsdb_retention_percentage_float.good.yml diff --git a/cmd/prometheus/main.go b/cmd/prometheus/main.go index 516a6c6d11..864799cf54 100644 --- a/cmd/prometheus/main.go +++ b/cmd/prometheus/main.go @@ -797,16 +797,12 @@ func main() { logger.Warn("Time retention value is too high. Limiting to: " + y.String()) } - if cfg.tsdb.MaxPercentage > 100 { - cfg.tsdb.MaxPercentage = 100 - logger.Warn("Percentage retention value is too high. Limiting to: 100%") - } if cfg.tsdb.MaxPercentage > 0 { if cfg.tsdb.MaxBytes > 0 { logger.Warn("storage.tsdb.retention.size is ignored, because storage.tsdb.retention.percentage is specified") } if prom_runtime.FsSize(localStoragePath) == 0 { - fmt.Fprintln(os.Stderr, fmt.Errorf("unable to detect total capacity of metric storage at %s, please disable retention percentage (%d%%)", localStoragePath, cfg.tsdb.MaxPercentage)) + fmt.Fprintln(os.Stderr, fmt.Errorf("unable to detect total capacity of metric storage at %s, please disable retention percentage (%g%%)", localStoragePath, cfg.tsdb.MaxPercentage)) os.Exit(2) } } @@ -2014,7 +2010,7 @@ type tsdbOptions struct { MaxBlockChunkSegmentSize units.Base2Bytes RetentionDuration model.Duration MaxBytes units.Base2Bytes - MaxPercentage uint + MaxPercentage float64 NoLockfile bool WALCompressionType compression.Type HeadChunksWriteQueueSize int diff --git a/config/config.go b/config/config.go index 2082743b0d..c813cd2837 100644 --- a/config/config.go +++ b/config/config.go @@ -1111,7 +1111,7 @@ type TSDBRetentionConfig struct { Size units.Base2Bytes `yaml:"size,omitempty"` // Maximum percentage of disk used for TSDB storage. - Percentage uint `yaml:"percentage,omitempty"` + Percentage float64 `yaml:"percentage,omitempty"` } // UnmarshalYAML implements the yaml.Unmarshaler interface. @@ -1124,7 +1124,7 @@ func (t *TSDBRetentionConfig) UnmarshalYAML(unmarshal func(any) error) error { 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 { + if t.Percentage < 0 || t.Percentage > 100 { return fmt.Errorf("'storage.tsdb.retention.percentage' must be in the range [0, 100], got %v", t.Percentage) } return nil diff --git a/config/config_test.go b/config/config_test.go index 8d4df86be6..1bae23d9d4 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -2640,7 +2640,7 @@ var expectedErrors = []struct { }, { filename: "tsdb_retention_percentage_negative.bad.yml", - errMsg: "cannot unmarshal !!int `-1` into uint", + errMsg: "'storage.tsdb.retention.percentage' must be in the range [0, 100]", }, } @@ -2652,6 +2652,12 @@ func TestBadConfigs(t *testing.T) { } } +func TestTSDBRetentionPercentageFloat(t *testing.T) { + c, err := LoadFile("testdata/tsdb_retention_percentage_float.good.yml", false, promslog.NewNopLogger()) + require.NoError(t, err) + require.Equal(t, 0.5, c.StorageConfig.TSDBConfig.Retention.Percentage) +} + func TestBadStaticConfigsYML(t *testing.T) { content, err := os.ReadFile("testdata/static_config.bad.yml") require.NoError(t, err) diff --git a/config/testdata/tsdb_retention_percentage_float.good.yml b/config/testdata/tsdb_retention_percentage_float.good.yml new file mode 100644 index 0000000000..cf82ebf5fa --- /dev/null +++ b/config/testdata/tsdb_retention_percentage_float.good.yml @@ -0,0 +1,4 @@ +storage: + tsdb: + retention: + percentage: 0.5 diff --git a/tsdb/db.go b/tsdb/db.go index fde5c5e22c..5b45779ee4 100644 --- a/tsdb/db.go +++ b/tsdb/db.go @@ -130,7 +130,7 @@ type Options struct { // Maximum % of disk space to use for blocks to be retained. // 0 or less means disabled. // If both MaxBytes and MaxPercentage are set, percentage prevails. - MaxPercentage uint + MaxPercentage float64 // NoLockfile disables creation and consideration of a lock file. NoLockfile bool @@ -1105,7 +1105,7 @@ func open(dir string, l *slog.Logger, r prometheus.Registerer, opts *Options, rn db.metrics = newDBMetrics(db, r) maxBytes := max(opts.MaxBytes, 0) db.metrics.maxBytes.Set(float64(maxBytes)) - db.metrics.maxPercentage.Set(float64(max(opts.MaxPercentage, 0))) + db.metrics.maxPercentage.Set(max(opts.MaxPercentage, 0)) db.metrics.retentionDuration.Set((time.Duration(opts.RetentionDuration) * time.Millisecond).Seconds()) // Calling db.reload() calls db.reloadBlocks() which requires cmtx to be locked. @@ -1295,7 +1295,7 @@ func (db *DB) ApplyConfig(conf *config.Config) error { 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.metrics.maxPercentage.Set(db.opts.MaxPercentage) db.retentionMtx.Unlock() } } else { @@ -1342,7 +1342,7 @@ func (db *DB) getRetentionDuration() int64 { } // getRetentionSettings returns max bytes and max percentage settings in a thread-safe manner. -func (db *DB) getRetentionSettings() (int64, uint) { +func (db *DB) getRetentionSettings() (int64, float64) { db.retentionMtx.RLock() defer db.retentionMtx.RUnlock() return db.opts.MaxBytes, db.opts.MaxPercentage @@ -2018,7 +2018,7 @@ func BeyondSizeRetention(db *DB, blocks []*Block) (deletable map[ulid.ULID]struc if diskSize <= 0 { db.logger.Warn("Unable to retrieve filesystem size of database directory, skip percentage limitation and default to fixed size limitation", "dir", db.dir) } else { - maxBytes = int64(uint64(maxPercentage) * diskSize / 100) + maxBytes = int64(float64(diskSize) * maxPercentage / 100) } } diff --git a/tsdb/db_test.go b/tsdb/db_test.go index 21b2c08124..2ca2f70529 100644 --- a/tsdb/db_test.go +++ b/tsdb/db_test.go @@ -9663,8 +9663,8 @@ func TestBeyondSizeRetentionWithPercentage(t *testing.T) { db := newTestDB(t, withOpts(opts)) require.Zero(t, db.Head().Size()) - blocks := make([]*Block, 0, opts.MaxPercentage+1) - for range opts.MaxPercentage { + blocks := make([]*Block, 0, int(opts.MaxPercentage)+1) + for range int(opts.MaxPercentage) { blocks = append(blocks, &Block{ numBytesChunks: numBytesChunks, meta: BlockMeta{ULID: ulid.Make()}, diff --git a/web/web.go b/web/web.go index c4fcfdb2c4..65da00ed18 100644 --- a/web/web.go +++ b/web/web.go @@ -268,7 +268,7 @@ type Options struct { TSDBRetentionDuration model.Duration TSDBDir string TSDBMaxBytes units.Base2Bytes - TSDBMaxPercentage uint + TSDBMaxPercentage float64 LocalStorage LocalStorage Storage storage.Storage ExemplarStorage storage.ExemplarQueryable @@ -889,7 +889,7 @@ func (h *Handler) runtimeInfo() (api_v1.RuntimeInfo, error) { if status.StorageRetention != "" { status.StorageRetention += " or " } - status.StorageRetention = status.StorageRetention + strconv.FormatUint(uint64(tsdbMaxPercentage), 10) + "%" + status.StorageRetention = status.StorageRetention + strconv.FormatFloat(tsdbMaxPercentage, 'g', -1, 64) + "%" } metrics, err := h.gatherer.Gather()