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>
This commit is contained in:
Julien Pivotto
2026-03-26 12:33:49 +01:00
parent e8e223fccb
commit 3856195bb8
7 changed files with 24 additions and 18 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

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

View File

@@ -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)
}
}

View File

@@ -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()},

View File

@@ -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()