feat: add cache_max_bytes config key with statfs-derived default
Strict int64 parsing via the startup validation framework: a SET but invalid value (negative, float, null, non-numeric) aborts startup naming the key and value. An omitted key resolves after state_dir validation to max(75% of free bytes on the filesystem containing <state_dir>/cache/, 500 MiB), measured via an injectable statfs probe; the floor never applies to explicit values. Zero is valid and means the disk cache is disabled. The effective limit is logged at startup.
This commit is contained in:
@@ -55,6 +55,12 @@ type Config struct {
|
||||
// (75% of free space on the filesystem containing
|
||||
// <state_dir>/cache/, floored at DefaultCacheMaxBytesFloor).
|
||||
CacheMaxBytes int64
|
||||
|
||||
// cacheMaxBytesExplicit records whether cache_max_bytes was
|
||||
// explicitly set in the configuration file. Explicit values are
|
||||
// used exactly as given; only an omitted key gets the computed
|
||||
// default (and its floor) in resolveCacheMaxBytes.
|
||||
cacheMaxBytesExplicit bool
|
||||
}
|
||||
|
||||
// New creates a new Config instance by loading configuration from file.
|
||||
@@ -122,6 +128,16 @@ func newFromSmartConfig(sc *smartconfig.Config) (*Config, error) {
|
||||
AllowHTTP: loader.boolVal("allow_http", false),
|
||||
UpstreamConnectionsPerHost: loader.intVal(
|
||||
"upstream_connections_per_host", DefaultUpstreamConnectionsPerHost),
|
||||
CacheMaxBytes: loader.int64Val("cache_max_bytes", 0),
|
||||
}
|
||||
|
||||
// The computed default for cache_max_bytes needs a validated
|
||||
// state_dir, so it is resolved later (resolveCacheMaxBytes); here
|
||||
// we only record whether the operator set the key explicitly.
|
||||
if sc != nil {
|
||||
if _, present := sc.Get("cache_max_bytes"); present {
|
||||
c.cacheMaxBytesExplicit = true
|
||||
}
|
||||
}
|
||||
|
||||
// Build DBURL from StateDir if not explicitly set. The derived URL
|
||||
@@ -230,7 +246,7 @@ func isKnownConfigKey(key string) bool {
|
||||
switch key {
|
||||
case "debug", "maintenance_mode", "port", "state_dir", "sentry_dsn",
|
||||
"db_url", "metrics", "signing_key", "allowlist_hosts", "allow_http",
|
||||
"upstream_connections_per_host", "env":
|
||||
"upstream_connections_per_host", "cache_max_bytes", "env":
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -300,6 +316,13 @@ func (c *Config) validate() error {
|
||||
return fmt.Errorf("config key %q: value must not be empty", "state_dir")
|
||||
}
|
||||
|
||||
// Zero is valid (it disables the disk cache); only negative
|
||||
// values are rejected. No floor applies to explicit values.
|
||||
if c.CacheMaxBytes < 0 {
|
||||
return fmt.Errorf("config key %q: value %d must not be negative",
|
||||
"cache_max_bytes", c.CacheMaxBytes)
|
||||
}
|
||||
|
||||
for _, host := range c.AllowlistHosts {
|
||||
if err := validateAllowlistHost(host); err != nil {
|
||||
return err
|
||||
@@ -423,6 +446,19 @@ func (l *strictLoader) intVal(key string, defaultVal int) int {
|
||||
return val
|
||||
}
|
||||
|
||||
func (l *strictLoader) int64Val(key string, defaultVal int64) int64 {
|
||||
if l.err != nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
val, err := getInt64(l.sc, key, defaultVal)
|
||||
if err != nil {
|
||||
l.err = err
|
||||
}
|
||||
|
||||
return val
|
||||
}
|
||||
|
||||
func (l *strictLoader) boolVal(key string, defaultVal bool) bool {
|
||||
if l.err != nil {
|
||||
return false
|
||||
@@ -503,6 +539,55 @@ func getInt(sc *smartconfig.Config, key string, defaultVal int) (int, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// getInt64 returns the 64-bit integer value for key, or defaultVal if
|
||||
// the key is omitted. A present value that is not a whole number, or
|
||||
// is explicitly null, is an error; fractional values are never
|
||||
// truncated and out-of-range values are never clamped.
|
||||
func getInt64(sc *smartconfig.Config, key string, defaultVal int64) (int64, error) {
|
||||
if sc == nil {
|
||||
return defaultVal, nil
|
||||
}
|
||||
|
||||
raw, ok := sc.Get(key)
|
||||
if !ok {
|
||||
return defaultVal, nil
|
||||
}
|
||||
|
||||
if raw == nil {
|
||||
return 0, errNullConfigValue(key)
|
||||
}
|
||||
|
||||
switch val := raw.(type) {
|
||||
case int:
|
||||
return int64(val), nil
|
||||
case int64:
|
||||
return val, nil
|
||||
case uint64:
|
||||
if val > math.MaxInt64 {
|
||||
return 0, fmt.Errorf("config key %q: value %d overflows a 64-bit integer",
|
||||
key, val)
|
||||
}
|
||||
|
||||
return int64(val), nil //nolint:gosec // G115: bounds checked above
|
||||
case float64:
|
||||
if val != math.Trunc(val) {
|
||||
return 0, fmt.Errorf("config key %q: value %v is not an integer", key, val)
|
||||
}
|
||||
|
||||
return int64(val), nil
|
||||
case string:
|
||||
parsed, err := strconv.ParseInt(strings.TrimSpace(val), 10, 64)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("config key %q: value %q is not an integer", key, val)
|
||||
}
|
||||
|
||||
return parsed, nil
|
||||
default:
|
||||
return 0, fmt.Errorf("config key %q: value %v (%T) is not an integer",
|
||||
key, raw, raw)
|
||||
}
|
||||
}
|
||||
|
||||
// getBool returns the boolean value for key, or defaultVal if the key
|
||||
// is omitted. A present value that is not a boolean (or a ParseBool-able
|
||||
// string), or is explicitly null, is an error; numbers are not accepted
|
||||
|
||||
Reference in New Issue
Block a user