diff --git a/packages/shared/pkg/featureflags/flags.go b/packages/shared/pkg/featureflags/flags.go index feb46140c5..c7452da2d9 100644 --- a/packages/shared/pkg/featureflags/flags.go +++ b/packages/shared/pkg/featureflags/flags.go @@ -432,6 +432,25 @@ func (f IntFlag) Fallback() int { return f.fallback } +// envIntOr reads key as an int, falling back when it is unset, unparseable, or +// not positive. It exists for the same reason as envBoolOr: on a cluster with no +// LaunchDarkly an int flag resolves to a value only a rebuild can change, and +// max-sandboxes-per-node's fallback is sized for cloud node types rather than +// for the host it runs on. A non-positive value keeps the fallback so a typo +// cannot silently stop a node from accepting sandboxes. +func envIntOr(key string, fallback int) int { + raw := env.GetEnv(key, "") + if raw == "" { + return fallback + } + parsed, err := strconv.Atoi(raw) + if err != nil || parsed <= 0 { + return fallback + } + + return parsed +} + func NewIntFlag(name string, fallback int) IntFlag { flag := IntFlag{name: name, fallback: fallback} builder := launchDarklyOfflineStore.Flag(flag.name).ValueForAll(ldvalue.Int(fallback)) @@ -441,7 +460,7 @@ func NewIntFlag(name string, fallback int) IntFlag { } var ( - MaxSandboxesPerNode = NewIntFlag("max-sandboxes-per-node", 200) + MaxSandboxesPerNode = NewIntFlag("max-sandboxes-per-node", envIntOr("MAX_SANDBOXES_PER_NODE", 200)) // The LD keys keep the legacy "gcloud-" prefix, but the limits apply to uploads on all storage providers. StorageConcurrentUploadLimit = NewIntFlag("gcloud-concurrent-upload-limit", 8) StorageMaxUploadTasks = NewIntFlag("gcloud-max-tasks", 16) @@ -599,7 +618,7 @@ var ( // MaxStartingInstancesPerNode limits concurrent sandbox start/resume operations on a single orchestrator node. // Must be > 0. - MaxStartingInstancesPerNode = NewIntFlag("max-starting-instances-per-node", 3) + MaxStartingInstancesPerNode = NewIntFlag("max-starting-instances-per-node", envIntOr("MAX_STARTING_INSTANCES_PER_NODE", 3)) // MaxConcurrentEvictions caps the number of sandbox evictions that can run // in parallel per API instance. Excess items remain expired in the store diff --git a/packages/shared/pkg/featureflags/flags_envint_test.go b/packages/shared/pkg/featureflags/flags_envint_test.go new file mode 100644 index 0000000000..0c98388651 --- /dev/null +++ b/packages/shared/pkg/featureflags/flags_envint_test.go @@ -0,0 +1,35 @@ +package featureflags + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +//nolint:paralleltest,tparallel // t.Setenv +func TestEnvIntOr(t *testing.T) { + const key = "MAX_SANDBOXES_PER_NODE_TEST_ONLY" + + for _, tc := range []struct { + name string + set bool + value string + fallback int + want int + }{ + {name: "unset keeps the fallback", fallback: 200, want: 200}, + {name: "empty keeps the fallback", set: true, value: "", fallback: 200, want: 200}, + {name: "a value overrides the fallback", set: true, value: "1200", fallback: 200, want: 1200}, + {name: "garbage keeps the fallback", set: true, value: "many", fallback: 200, want: 200}, + {name: "zero keeps the fallback", set: true, value: "0", fallback: 200, want: 200}, + {name: "negative keeps the fallback", set: true, value: "-1", fallback: 200, want: 200}, + } { + t.Run(tc.name, func(t *testing.T) { + if tc.set { + t.Setenv(key, tc.value) + } + + require.Equal(t, tc.want, envIntOr(key, tc.fallback)) + }) + } +}