Skip to content

Commit a74e111

Browse files
authored
feat: improved config and env override handling (#27383)
* feat: improved config and env override handling - The following config parameters now support size suffixes: - query-initial-memory-bytes - query-memory-bytes - query-max-memory-bytes - The following parameters are now range checked properly to prevent compaction issues and log corruption with values that overflow internal size limits: - aggressive-points-per-block - compact-throughput - compact-throughput-burst - max-index-log-file-size - Fix potential loss of accuracy when displaying default parameter values when displaying help - Fix potential panics for default parameter values when displaying help
1 parent 533e9f7 commit a74e111

6 files changed

Lines changed: 188 additions & 7 deletions

File tree

cmd/influxd/launcher/cmd.go

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
"github.com/influxdata/influxdb/v2/pprof"
1818
"github.com/influxdata/influxdb/v2/sqlite"
1919
"github.com/influxdata/influxdb/v2/storage"
20+
"github.com/influxdata/influxdb/v2/toml"
2021
"github.com/influxdata/influxdb/v2/v1/coordinator"
2122
"github.com/influxdata/influxdb/v2/vault"
2223
"github.com/spf13/cobra"
@@ -182,9 +183,9 @@ type InfluxdOpts struct {
182183

183184
// Query options.
184185
ConcurrencyQuota int32
185-
InitialMemoryBytesQuotaPerQuery int64
186-
MemoryBytesQuotaPerQuery int64
187-
MaxMemoryBytes int64
186+
InitialMemoryBytesQuotaPerQuery toml.SSize
187+
MemoryBytesQuotaPerQuery toml.SSize
188+
MaxMemoryBytes toml.SSize
188189
QueueSize int32
189190
CoordinatorConfig coordinator.Config
190191

@@ -473,6 +474,9 @@ func (o *InfluxdOpts) BindCliOpts() []cli.Opt {
473474
Default: o.ConcurrencyQuota,
474475
Desc: "the number of queries that are allowed to execute concurrently. Set to 0 to allow an unlimited number of concurrent queries",
475476
},
477+
// Default on the next three Opts is documentary: for a pflag.Value
478+
// DestP, pflag.Var already uses destP's NewOpts-set value as the help
479+
// default, so omitting Default would not change behavior.
476480
{
477481
DestP: &o.InitialMemoryBytesQuotaPerQuery,
478482
Flag: "query-initial-memory-bytes",

cmd/influxd/launcher/launcher.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -434,9 +434,9 @@ func (m *Launcher) run(ctx context.Context, opts *InfluxdOpts) (err error) {
434434

435435
m.queryController, err = control.New(control.Config{
436436
ConcurrencyQuota: opts.ConcurrencyQuota,
437-
InitialMemoryBytesQuotaPerQuery: opts.InitialMemoryBytesQuotaPerQuery,
438-
MemoryBytesQuotaPerQuery: opts.MemoryBytesQuotaPerQuery,
439-
MaxMemoryBytes: opts.MaxMemoryBytes,
437+
InitialMemoryBytesQuotaPerQuery: int64(opts.InitialMemoryBytesQuotaPerQuery),
438+
MemoryBytesQuotaPerQuery: int64(opts.MemoryBytesQuotaPerQuery),
439+
MaxMemoryBytes: int64(opts.MaxMemoryBytes),
440440
QueueSize: opts.QueueSize,
441441
ExecutorDependencies: dependencyList,
442442
FluxLogEnabled: opts.FluxLogEnabled,

kit/cli/viper.go

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"fmt"
55
"os"
66
"path"
7+
"reflect"
78
"strings"
89
"time"
910

@@ -296,7 +297,26 @@ func BindOptions(v *viper.Viper, cmd *cobra.Command, opts []Opt) error {
296297
flagset.Var(destP, o.Flag, o.Desc)
297298
}
298299
if o.Default != nil {
299-
_ = destP.Set(o.Default.(string))
300+
// Prefer a direct value copy when Default's concrete type
301+
// matches destP's pointee type. This bypasses the Stringer →
302+
// Set round-trip, which is lossy for types whose String() goes
303+
// through humanize.IBytes (toml.Size / toml.SSize emit a
304+
// rounded form like "24 MiB" for non-power-of-2 byte counts,
305+
// which then parses back to a different value). Falling
306+
// through to cast.ToStringE keeps Defaults like a plain string
307+
// or untyped int working for callers whose Default differs in
308+
// type from DestP — e.g. Default: "on" for a customFlag.
309+
dv := reflect.ValueOf(o.Default)
310+
pv := reflect.ValueOf(destP)
311+
if dv.IsValid() && pv.Kind() == reflect.Ptr && dv.Type() == pv.Elem().Type() {
312+
pv.Elem().Set(dv)
313+
} else {
314+
s, err := cast.ToStringE(o.Default)
315+
if err != nil {
316+
return fmt.Errorf("flag %q: cannot resolve Default of type %T: %w", o.Flag, o.Default, err)
317+
}
318+
_ = destP.Set(s)
319+
}
300320
}
301321
if err := v.BindPFlag(o.Flag, flagset.Lookup(o.Flag)); err != nil {
302322
return fmt.Errorf("failed to bind flag %q: %w", o.Flag, err)

kit/cli/viper_test.go

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,87 @@ func yamlConfigWriter(shortExt bool) configWriter {
315315
}
316316
}
317317

318+
// Test_PFlagValueTypedDefault confirms the pflag.Value binding path
319+
// resolves Defaults of various concrete types into the destP value: a
320+
// string Default goes through the cast.ToStringE → Set path, a typed
321+
// Default whose type matches destP takes the direct-copy fast path, and
322+
// a Default whose type neither matches destP nor casts to a string is
323+
// rejected at bind time with an error naming the flag and offending type.
324+
func Test_PFlagValueTypedDefault(t *testing.T) {
325+
tests := []struct {
326+
name string
327+
dflt interface{}
328+
want customFlag
329+
wantBindErrParts []string // non-empty means NewCommand is expected to fail
330+
}{
331+
{name: "string default still works", dflt: "on", want: customFlag(true)},
332+
{name: "typed Stringer default", dflt: customFlag(true), want: customFlag(true)},
333+
{
334+
name: "non-stringable default is rejected",
335+
dflt: struct{}{},
336+
wantBindErrParts: []string{`flag "fancy-bool"`, "cannot resolve Default", "struct {}"},
337+
},
338+
}
339+
340+
for _, tt := range tests {
341+
t.Run(tt.name, func(t *testing.T) {
342+
var got customFlag
343+
cmd, err := NewCommand(viper.New(), &Program{
344+
Name: "test",
345+
Run: func() error { return nil },
346+
Opts: []Opt{
347+
{DestP: &got, Flag: "fancy-bool", Default: tt.dflt},
348+
},
349+
})
350+
if len(tt.wantBindErrParts) > 0 {
351+
require.Error(t, err)
352+
for _, p := range tt.wantBindErrParts {
353+
require.Contains(t, err.Error(), p)
354+
}
355+
return
356+
}
357+
require.NoError(t, err)
358+
cmd.SetArgs([]string{})
359+
require.NoError(t, cmd.Execute())
360+
require.Equal(t, tt.want, got)
361+
})
362+
}
363+
}
364+
365+
// lossyValue is a pflag.Value whose String() form is deliberately not
366+
// reversible by Set(): String returns a constant token, and Set ignores its
367+
// input and assigns a sentinel value. This makes any Stringer→Set round-trip
368+
// observable: if the kit ever regresses to round-tripping typed Defaults
369+
// through cast.ToStringE, dest will land on the sentinel instead of the
370+
// caller's value.
371+
type lossyValue uint64
372+
373+
func (v lossyValue) String() string { return "lossy" }
374+
func (v *lossyValue) Set(string) error { *v = 999; return nil }
375+
func (v *lossyValue) Type() string { return "lossy" }
376+
377+
// Test_PFlagValueTypedDefault_BypassesLossyStringer pins down that when
378+
// Default's concrete type matches destP's pointee type, the kit copies the
379+
// value directly rather than going through Stringer→Set. Without that
380+
// bypass, the Default would be silently corrupted for any pflag.Value type
381+
// whose String() is lossy (the motivating real-world case is toml.Size and
382+
// toml.SSize, whose humanize.IBytes formatting drifts non-power-of-2 byte
383+
// counts across a marshal/unmarshal cycle).
384+
func Test_PFlagValueTypedDefault_BypassesLossyStringer(t *testing.T) {
385+
var got lossyValue
386+
cmd, err := NewCommand(viper.New(), &Program{
387+
Name: "test",
388+
Run: func() error { return nil },
389+
Opts: []Opt{
390+
{DestP: &got, Flag: "lossy", Default: lossyValue(42)},
391+
},
392+
})
393+
require.NoError(t, err)
394+
cmd.SetArgs([]string{})
395+
require.NoError(t, cmd.Execute())
396+
require.Equal(t, lossyValue(42), got, "typed Default should be copied directly, not round-tripped through Stringer/Set")
397+
}
398+
318399
func Test_RequiredFlag(t *testing.T) {
319400
var testVar string
320401
program := &Program{

tsdb/config.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,5 +245,22 @@ func (c *Config) Validate() error {
245245
return fmt.Errorf("unrecognized index %s", c.Index)
246246
}
247247

248+
// These sizes are uint64 on disk but are passed to rate limiters (int) and
249+
// the tsi1 log file writer (int64); an oversized config value would
250+
// otherwise silently wrap to a negative number and disable rate limiting
251+
// or corrupt log-file size accounting.
252+
if _, err := c.CompactThroughput.ToInt(); err != nil {
253+
return fmt.Errorf("compact-throughput: %w", err)
254+
}
255+
if _, err := c.CompactThroughputBurst.ToInt(); err != nil {
256+
return fmt.Errorf("compact-throughput-burst: %w", err)
257+
}
258+
if _, err := c.AggressivePointsPerBlock.ToInt(); err != nil {
259+
return fmt.Errorf("aggressive-points-per-block: %w", err)
260+
}
261+
if _, err := c.MaxIndexLogFileSize.ToInt64(); err != nil {
262+
return fmt.Errorf("max-index-log-file-size: %w", err)
263+
}
264+
248265
return nil
249266
}

tsdb/config_test.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
package tsdb_test
22

33
import (
4+
"math"
45
"testing"
56
"time"
67

78
"github.com/BurntSushi/toml"
89
"github.com/stretchr/testify/require"
910

11+
itoml "github.com/influxdata/influxdb/v2/toml"
1012
"github.com/influxdata/influxdb/v2/tsdb"
1113
)
1214

@@ -64,6 +66,63 @@ func TestConfig_Validate_Error(t *testing.T) {
6466
}
6567
}
6668

69+
// TestConfig_Validate_SizeOverflow exercises the overflow guards added to
70+
// Validate() for the toml.Size fields that downstream code casts to int or
71+
// int64. Without these guards, an oversized config value would silently wrap
72+
// (e.g., disabling rate limiting or corrupting log-file accounting).
73+
//
74+
// The intKind assertion pins down which conversion helper each field is
75+
// wired to: ToInt produces "exceeds maximum int value" while ToInt64
76+
// produces "exceeds maximum int64 value". Cross-wiring the two would still
77+
// trip the validator on most inputs but would fail to catch values in the
78+
// (math.MaxInt, math.MaxInt64] range on 32-bit platforms.
79+
func TestConfig_Validate_SizeOverflow(t *testing.T) {
80+
tests := []struct {
81+
name string
82+
mutate func(*tsdb.Config)
83+
fieldPrefix string
84+
intKind string
85+
}{
86+
{
87+
name: "compact-throughput",
88+
mutate: func(c *tsdb.Config) { c.CompactThroughput = math.MaxUint64 },
89+
fieldPrefix: "compact-throughput:",
90+
intKind: "maximum int value",
91+
},
92+
{
93+
name: "compact-throughput-burst",
94+
mutate: func(c *tsdb.Config) { c.CompactThroughputBurst = math.MaxUint64 },
95+
fieldPrefix: "compact-throughput-burst:",
96+
intKind: "maximum int value",
97+
},
98+
{
99+
name: "aggressive-points-per-block",
100+
mutate: func(c *tsdb.Config) { c.AggressivePointsPerBlock = math.MaxUint64 },
101+
fieldPrefix: "aggressive-points-per-block:",
102+
intKind: "maximum int value",
103+
},
104+
{
105+
name: "max-index-log-file-size",
106+
mutate: func(c *tsdb.Config) { c.MaxIndexLogFileSize = math.MaxUint64 },
107+
fieldPrefix: "max-index-log-file-size:",
108+
intKind: "maximum int64 value",
109+
},
110+
}
111+
for _, tt := range tests {
112+
t.Run(tt.name, func(t *testing.T) {
113+
c := tsdb.NewConfig()
114+
c.Dir = "/var/lib/influxdb/data"
115+
c.WALDir = "/var/lib/influxdb/wal"
116+
tt.mutate(&c)
117+
err := c.Validate()
118+
require.Error(t, err)
119+
require.Contains(t, err.Error(), tt.fieldPrefix)
120+
require.Contains(t, err.Error(), tt.intKind)
121+
require.ErrorIs(t, err, itoml.ErrSizeOutOfRange)
122+
})
123+
}
124+
}
125+
67126
func TestConfig_ByteSizes(t *testing.T) {
68127
// Parse configuration.
69128
c := tsdb.NewConfig()

0 commit comments

Comments
 (0)