From e547850b3e35320f2cd8e8ee9bcfc286e4ad0c85 Mon Sep 17 00:00:00 2001 From: Henrique Costa Date: Sun, 26 Apr 2026 10:30:33 +0200 Subject: [PATCH] feat(executor): allow MXCLI_EXEC_TIMEOUT to override the per-statement timeout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Long-running audit runs against large projects (1k+ microflows) can hit the hard-coded 5-minute per-statement ceiling on commands like `describe project`, `describe module`, or bulk `show callers`. Currently the only options are to skip those commands or to fork mxcli. Add an MXCLI_EXEC_TIMEOUT env var that overrides the default. Accepts: - Go duration strings: "12m", "2h30m", "45s". - Bare seconds: "900" → 15 minutes. When unset, empty, or unparseable, falls back to the existing 5-minute default. Read on every Execute() call so an operator can adjust mid-run without restarting. Refactor: - Replace the `executeTimeout = 5 * time.Minute` const with `defaultExecuteTimeout` and a `configuredExecuteTimeout()` helper. - `Execute()` reads the timeout via the helper instead of the const. - Update the existing comment on `Execute()` to reference "the configured timeout" rather than the const name. Tests: - `TestConfiguredExecuteTimeoutUsesDurationEnv` — "12m" → 12*time.Minute. - `TestConfiguredExecuteTimeoutUsesSecondEnv` — "900" → 15*time.Minute. - `TestConfiguredExecuteTimeoutFallsBackForInvalidEnv` — "invalid" → defaultExecuteTimeout. Tests use `t.Setenv` so they cannot leak across runs and are parallel-safe with the rest of the executor test suite. --- mdl/executor/executor.go | 31 +++++++++++++++++++++++--- mdl/executor/executor_timeout_test.go | 32 +++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 3 deletions(-) create mode 100644 mdl/executor/executor_timeout_test.go diff --git a/mdl/executor/executor.go b/mdl/executor/executor.go index 6f60f23a..272c4c2f 100644 --- a/mdl/executor/executor.go +++ b/mdl/executor/executor.go @@ -7,6 +7,8 @@ import ( "context" "fmt" "io" + "os" + "strconv" "sync" "time" @@ -160,10 +162,32 @@ const ( // maxOutputLines is the per-statement line limit. Statements that produce more // lines than this are aborted to prevent runaway output from infinite loops. maxOutputLines = 10_000 - // executeTimeout is the maximum wall-clock time allowed for a single statement. - executeTimeout = 5 * time.Minute + // defaultExecuteTimeout is the maximum wall-clock time allowed for a single + // statement when MXCLI_EXEC_TIMEOUT is not set. + defaultExecuteTimeout = 5 * time.Minute ) +// configuredExecuteTimeout returns the per-statement wall-clock timeout. The +// value is read from the MXCLI_EXEC_TIMEOUT environment variable on every call +// so long-running audits can opt into a higher ceiling without recompiling. +// +// Accepts either a Go duration ("12m", "2h30m") or a bare number of seconds +// ("900"). Falls back to defaultExecuteTimeout when the variable is unset, +// empty, or fails to parse. +func configuredExecuteTimeout() time.Duration { + raw := os.Getenv("MXCLI_EXEC_TIMEOUT") + if raw == "" { + return defaultExecuteTimeout + } + if d, err := time.ParseDuration(raw); err == nil && d > 0 { + return d + } + if seconds, err := strconv.Atoi(raw); err == nil && seconds > 0 { + return time.Duration(seconds) * time.Second + } + return defaultExecuteTimeout +} + // BackendFactory creates a new backend instance for connecting to a project. type BackendFactory func() backend.FullBackend @@ -221,7 +245,7 @@ func (e *Executor) SetLogger(l *diaglog.Logger) { // Execute runs a single MDL statement with output-line and wall-clock guards. // Each statement gets a fresh line budget. If the statement exceeds maxOutputLines -// lines of output or runs longer than executeTimeout, it is aborted with an error. +// lines of output or runs longer than the configured timeout, it is aborted with an error. func (e *Executor) Execute(stmt ast.Statement) error { start := time.Now() @@ -233,6 +257,7 @@ func (e *Executor) Execute(stmt ast.Statement) error { // Enforce wall-clock timeout via context.WithTimeout. // The goroutine pattern is retained because handlers are not yet // context-aware; threading context through handlers is a follow-up. + executeTimeout := configuredExecuteTimeout() ctx, cancel := context.WithTimeout(context.Background(), executeTimeout) defer cancel() diff --git a/mdl/executor/executor_timeout_test.go b/mdl/executor/executor_timeout_test.go new file mode 100644 index 00000000..8f82f63d --- /dev/null +++ b/mdl/executor/executor_timeout_test.go @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: Apache-2.0 + +package executor + +import ( + "testing" + "time" +) + +func TestConfiguredExecuteTimeoutUsesDurationEnv(t *testing.T) { + t.Setenv("MXCLI_EXEC_TIMEOUT", "12m") + + if got := configuredExecuteTimeout(); got != 12*time.Minute { + t.Fatalf("configured timeout = %v, want 12m", got) + } +} + +func TestConfiguredExecuteTimeoutUsesSecondEnv(t *testing.T) { + t.Setenv("MXCLI_EXEC_TIMEOUT", "900") + + if got := configuredExecuteTimeout(); got != 15*time.Minute { + t.Fatalf("configured timeout = %v, want 15m", got) + } +} + +func TestConfiguredExecuteTimeoutFallsBackForInvalidEnv(t *testing.T) { + t.Setenv("MXCLI_EXEC_TIMEOUT", "invalid") + + if got := configuredExecuteTimeout(); got != defaultExecuteTimeout { + t.Fatalf("configured timeout = %v, want default %v", got, defaultExecuteTimeout) + } +}