Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion cmd/benchmarkoor/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,16 @@ func runBuild(_ *cobra.Command, _ []string) error {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

// Apply the global builder timeout if configured.
if buildTimeout := cfg.GetBuilderRunTimeout(); buildTimeout > 0 {
log.WithField("timeout", buildTimeout).Info("Global builder timeout configured")

var timeoutCancel context.CancelFunc

ctx, timeoutCancel = context.WithTimeout(ctx, buildTimeout)
defer timeoutCancel()
}

installSignalHandler(cancel)

builders, stop, err := buildBuilders(ctx, cfg)
Expand All @@ -102,7 +112,18 @@ func runBuild(_ *cobra.Command, _ []string) error {
return fmt.Errorf("all configured builders were skipped; nothing to build")
}

return runBuilders(ctx, builders)
if err := runBuilders(ctx, builders); err != nil {
// Surface a configured builder.run_timeout clearly rather than a bare
// "context deadline exceeded" (a build has no per-target status record,
// so a friendlier error is the realistic parity with the runner).
if errors.Is(ctx.Err(), context.DeadlineExceeded) {
return fmt.Errorf("build timed out (builder.run_timeout): %w", err)
}

return err
}

return nil
}

// installSignalHandler cancels the context on the first SIGINT/SIGTERM and
Expand Down
3 changes: 3 additions & 0 deletions config.example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,9 @@ runner:
# consumed by `run` through the normal datadir.method providers.
# See https://github.com/ethereum/state-actor for spec authoring.
# builder:
# # Global timeout capping the entire `benchmarkoor build` (all builders and
# # targets). Go duration; empty = no timeout. Env: BENCHMARKOOR_BUILDER_RUN_TIMEOUT.
# # run_timeout: 2h
# state_actor:
# # Per-client docker images. Every active target's client needs an entry —
# # state-actor needs a different cgo build per EL.
Expand Down
5 changes: 5 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ Configuration values can also be overridden via environment variables with the `
| Config Path | Environment Variable |
|-------------|---------------------|
| `global.log_level` | `BENCHMARKOOR_GLOBAL_LOG_LEVEL` |
| `builder.run_timeout` | `BENCHMARKOOR_BUILDER_RUN_TIMEOUT` |
| `runner.run_timeout` | `BENCHMARKOOR_RUNNER_RUN_TIMEOUT` |
| `runner.benchmark.results_dir` | `BENCHMARKOOR_RUNNER_BENCHMARK_RESULTS_DIR` |
| `runner.client.config.jwt` | `BENCHMARKOOR_RUNNER_CLIENT_CONFIG_JWT` |
Expand Down Expand Up @@ -1457,6 +1458,10 @@ The `builder` section configures tools that pre-populate benchmark inputs on dis

Builds are **decoupled from `benchmarkoor run`**: invoke `benchmarkoor build` to materialise the artifacts, then run benchmarks against them via the regular `datadir.method: copy|zfs|schelk|…` providers and test-source config. A missing datadir at `run` time is an error — it is never auto-built. When both builders are configured, they run in declaration order (`state_actor` before `eest_payloads`) so a fixture build can consume a datadir produced earlier in the same `benchmarkoor build` invocation.

| Option | Type | Default | Description |
|---|---|---|---|
| `run_timeout` | string | – | Global timeout capping the entire `benchmarkoor build` (all builders and targets), as a Go duration (e.g. `2h`, `90m`). Empty means no timeout. Overridable via `BENCHMARKOOR_BUILDER_RUN_TIMEOUT`. The analogue of [`runner.run_timeout`](#runner-run-timeout) for builds. |

### `builder.state_actor` options

```yaml
Expand Down
28 changes: 28 additions & 0 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,10 @@ type Config struct {
// stateful EEST benchmark fixtures against such a datadir). Future
// builders plug in alongside.
type BuilderConfig struct {
// RunTimeout caps the entire build (all builders and targets) as a Go
// duration string (e.g. "2h"). Empty means no timeout. Overridable via
// BENCHMARKOOR_BUILDER_RUN_TIMEOUT.
RunTimeout string `yaml:"run_timeout,omitempty" mapstructure:"run_timeout"`
StateActor *StateActorConfig `yaml:"state_actor,omitempty" mapstructure:"state_actor"`
EESTPayloads *EESTPayloadsConfig `yaml:"eest_payloads,omitempty" mapstructure:"eest_payloads"`
}
Expand Down Expand Up @@ -1515,6 +1519,8 @@ func bindEnvKeys(v *viper.Viper) {
// Global settings
"global.log_level",
"global.directories.cachedir",
// Builder settings
"builder.run_timeout",
// Runner settings
"runner.container_runtime",
"runner.client_logs_to_stdout",
Expand Down Expand Up @@ -2011,6 +2017,13 @@ func (c *Config) validateBuilder() error {
return nil
}

if c.Builder.RunTimeout != "" {
if _, err := time.ParseDuration(c.Builder.RunTimeout); err != nil {
return fmt.Errorf("invalid builder.run_timeout %q: %w",
c.Builder.RunTimeout, err)
}
}

if err := c.validateStateActor(); err != nil {
return err
}
Expand Down Expand Up @@ -2739,6 +2752,21 @@ func (c *Config) GetRunnerRunTimeout() time.Duration {
return d
}

// GetBuilderRunTimeout returns the global builder-level timeout that caps the
// entire build (all builders and targets). Returns 0 if not set.
func (c *Config) GetBuilderRunTimeout() time.Duration {
if c.Builder == nil || c.Builder.RunTimeout == "" {
return 0
}

d, err := time.ParseDuration(c.Builder.RunTimeout)
if err != nil {
return 0
}

return d
}

// GetRunTimeout returns the maximum duration for test execution.
// Instance-level config takes precedence over global defaults. Returns 0 if not set.
func (c *Config) GetRunTimeout(instance *ClientInstance) time.Duration {
Expand Down
42 changes: 42 additions & 0 deletions pkg/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,48 @@ func TestBuildsFillImage(t *testing.T) {
}
}

func TestGetBuilderRunTimeout(t *testing.T) {
tests := []struct {
name string
cfg *Config
want time.Duration
}{
{"nil builder", &Config{}, 0},
{"empty", &Config{Builder: &BuilderConfig{}}, 0},
{"valid", &Config{Builder: &BuilderConfig{RunTimeout: "2h"}}, 2 * time.Hour},
{"invalid falls back to 0", &Config{Builder: &BuilderConfig{RunTimeout: "nope"}}, 0},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.want, tt.cfg.GetBuilderRunTimeout())
})
}
}

func TestValidateBuilder_RunTimeout(t *testing.T) {
require.NoError(t, (&Config{Builder: &BuilderConfig{RunTimeout: "30m"}}).validateBuilder())

err := (&Config{Builder: &BuilderConfig{RunTimeout: "5 hours"}}).validateBuilder()
require.Error(t, err)
assert.Contains(t, err.Error(), "builder.run_timeout")
}

func TestLoad_BuilderRunTimeoutEnv(t *testing.T) {
// The key is absent from the file — the env binding must still populate it.
f := filepath.Join(t.TempDir(), "config.yaml")
require.NoError(t, os.WriteFile(f, []byte(
"builder:\n state_actor:\n images: {geth: img}\n targets:\n - client: geth\n output_dir: /tmp/x\n"), 0o600))

t.Setenv("BENCHMARKOOR_BUILDER_RUN_TIMEOUT", "3h")

c, err := Load(f)
require.NoError(t, err)
require.NotNil(t, c.Builder)
assert.Equal(t, "3h", c.Builder.RunTimeout)
assert.Equal(t, 3*time.Hour, c.GetBuilderRunTimeout())
}

func TestLoad_InlineAddressStubsKeyCasing(t *testing.T) {
// Viper is case-insensitive and lowercases all map keys; EEST resolves stub
// names by exact match, so Load must restore the original casing.
Expand Down
Loading