Skip to content

Replace initCalled bool with sync.Once to make init-script guarantee explicit #358

@kindermax

Description

@kindermax

Problem Statement

Executor uses a raw initCalled bool field to ensure the global init script runs exactly once across multiple Execute() calls. This guarantee is invisible from the type: callers reading the struct see a boolean with no indication of why it exists or what it protects. The guard is also not safe for concurrent Execute() calls — two goroutines racing on the first call could both see initCalled == false and run init twice. The current calling pattern is single-threaded, so this hasn't caused a bug, but the guarantee is fragile and silent.

Solution

Replace the initCalled bool field with a sync.Once. The once-only init semantics are encoded in the type, documented by the standard library, and concurrency-safe without any additional locking. No observable behaviour changes; the guarantee becomes explicit at the cost of zero complexity.

User Stories

  1. As a contributor, I want to understand the init-script guarantee by reading the Executor struct definition, so that I don't have to trace through the Execute() body to find the if !e.initCalled guard.
  2. As a contributor, I want the init-script guarantee to be concurrency-safe by construction, so that a future change that calls Execute() from multiple goroutines does not silently break the guarantee.
  3. As a contributor, I want to write a test that calls Execute() twice and verifies the init script ran exactly once, so that I can pin the once-only semantics as a regression anchor.
  4. As a contributor, I want the init logic to not require resetting state between test cases, so that I can construct a fresh Executor per test without worrying about leaked boolean state.
  5. As a maintainer, I want the init-script guarantee to be self-documenting, so that a code reviewer can confirm correctness without knowing the convention around initCalled.
  6. As a maintainer, I want Executor to have no exported or unexported boolean fields that silently alter the behaviour of Execute(), so that the executor's state model is simple and auditable.
  7. As a contributor, I want the removal of the boolean guard to be a no-op refactor with a clear mechanical mapping (if !e.initCalled { e.initCalled = true; ... }e.once.Do(...)), so that I can review the change with confidence that no behaviour was altered.

Implementation Decisions

Testing Decisions

A good test constructs an Executor with a non-empty cfg.Init, calls Execute() twice with valid contexts, and asserts that the init script was invoked exactly once. The test should drive this through Executor.Execute() — the highest available seam — rather than inspecting the once field directly.

Once issue #355 lands, a RecordingRunner can count init invocations precisely. Before #355 lands, a lightweight script-counting stub (a closure over a counter) suffices for the same assertion.

Modules to test:

  • Executor.Execute() — verify init runs once on first call and is skipped on subsequent calls.
  • Executor.Execute() — verify that an Executor with an empty cfg.Init never invokes the init path (the sync.Once consumes silently or the guard inside Do skips it).

Prior art: internal/executor/dependency_error_test.go — package-internal, standard testing package, no third-party assertion library. New tests follow the same style.

Out of Scope

Further Notes

This was surfaced as Candidate 4 ("Make the init-script guarantee explicit") during an architecture review of internal/executor/ on 2026-06-13. It was included as a companion change in issue #355 but is extracted here as a standalone issue so it can be reviewed and merged independently — it is the smallest possible self-contained improvement in the executor package.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions