Skip to content

⚡ Discover without the locks from Ceedling 1.2.1; centralize version gating - #31

Open
mkarlesky wants to merge 3 commits into
next_versionfrom
feature/lock-free-discovery
Open

mkarlesky wants to merge 3 commits into
next_versionfrom
feature/lock-free-discovery

Conversation

@mkarlesky

@mkarlesky mkarlesky commented Oct 25, 2026 •

Copy link
Copy Markdown
Member

From Ceedling 1.2.1, rediscovery no longer waits for a run in progress. Discovery's dumpconfig and files:test skip the engine's per-project locks, so the Testing view updates during a long run, e.g. after adding a test file or editing project.yml. In the latency suite on 1.2.1, a rediscovery during a 4.4-second build takes about 1.0 s instead of about 4.9 s. Earlier versions keep the locks, since their discovery rewrites the build root's vendored sources in place (Ceedling#1361).

The assessment

Prototype variations ran against real Ceedling before the implementation.

  • Ceedling alone.
    • Discovery's commands raced clobber, clean, and a first build on an empty build root, 12 trials each on 1.2.1 and 1.2.0. None failed.
    • A clobber of a build root padded to 20,000 files, overlapped by discovery, left no file outside build/vendor/. The next build recompiled all 12 files.
    • A full write trace found discovery writes nothing on 1.2.1, anywhere outside /dev, /proc, and /tmp. Ceedling's log file is the only exception, written only with --log, which discovery never passes.
    • Ceedling's clobber deletes each listed entry on its own, ignoring failures, so discovery can only recreate empty directories. The shared lock mode once planned for clean and clobber is unnecessary and isn't built.
  • The engine. The prototype passed stress (three seeds, two more with a shared build root), coverage (two seeds), gated, and layout on 1.2.1. The same prototype with its version gate removed failed stress --shared-build-root on 1.2.0: a run compiled a half-written unity.h. So the suite tells the two policies apart, and the gate is needed.

Changes

Engine (src/ceedlingEngine.ts, src/ceedlingProjectConfig.ts)

  • New capability readOnlyDiscovery, from 1.2.1. discovering() runs discovery's two commands without the locks when it holds.
  • ceedling version never loads the project file, so it can't see a :project ↳ :which_ceedling. Discovery skips the locks only when the project's last known configuration runs the installed gem (ProjectState.whichCeedling). A project's first dumpconfig therefore takes the locks. KnownIssues documents the broader limitation.
  • Fixes for races that a rediscovery no longer waiting makes reachable:
    • a build that outlives a rediscovery dropping its file no longer publishes stale diagnostics;
    • a function run's whole-file fallback reruns on the project it began with, instead of looking it up again.

Version gating, centralized. The review found version logic in several places. Now one table, FIRST_RELEASES, names each feature's first release. It is read through capabilitiesOf(), firstRelease(), ceedlingRelease() (the one place a reported version is coerced), and isSupportedRelease(). The engine no longer imports semver. The validation suites gate on the same table through capabilities() and requires(), instead of version literals. Architecture.md describes the scheme.

Validation suites

  • stress adds clobber. It checks every discovery's listing against tests/manual, and accepts the whole-file results of a --test-case substring collision, an existing behavior earlier seeds never hit. It also reports how many discoveries overlapped a build.
  • latency now checks the gate both ways, and all runs it. From 1.2.1, discovery's dumpconfig must start before the build ends. Before 1.2.1, discovery must end after the build.
  • probe:syscalls also counts writes outside build/. probe:overlap adds a slow-clobber section and counts failed discovery commands.

Vendored Ceedling (0758327)

  • A project running its own Ceedling, vendored in vendor/ceedling (as ceedling new --local makes) or named by WHICH_CEEDLING, failed to load. ceedling version reports it as Ceedling Launcher => and Ceedling App => lines, which the parser didn't read. The released extension has the same bug.
  • parseCeedlingVersions() reads both lines. Before Ceedling 1.2.2 a launcher reports another installation as its own release, with that installation's commit (:which_ceedling across versions: ceedling version can't see it, the application's version is misreported, and a 1.2.1 launcher can't run a 1.2.0 application Ceedling#1365). applicationRelease() trusts the application's release only from a 1.2.2 launcher (new reportsApplicationVersion capability), or when both lines report the same build. Otherwise the launcher's release stands in, a warning is logged, and discovery keeps the locks.
  • latency --vendored, now in all, repeats the gate check with the image's own Ceedling copied into vendor/ceedling. It fails on the previous engine and passes on 1.2.1, 1.2.0, and 1.0.0.
  • Version handling gets another look once Ceedling 1.2.2's fixes are complete.

Docs: Architecture.md (Locks, Invariants, Ceedling versions, and "Discovery without the locks from 1.2.1"), Development.md (suites, probes, and a checklist for a new Ceedling release), CHANGELOG, and KnownIssues.

Review

A three-way review (engine, suites, docs) ran before pushing, and every finding was checked against the code. Fixed:

  • the :which_ceedling gap;
  • stale diagnostics;
  • the fallback's project lookup;
  • missing tests for a lock-skipping listing before 1.2.1 and for capabilities going stale across versions;
  • tests that failed by timeout;
  • stale test names and comments;
  • latency's speed-dependent check;
  • overlap.sh ignoring failed discovery commands;
  • doc claims that overstated the probes.

Not taken: a single image with two Ceedling versions, to test mixed versions sharing a build root against real Ceedling. Unit tests cover that case.

Verification

  • Test-first. The new engine tests failed before the feature. Mutants of each review fix, and of the gate itself (ungated, gated from 1.2.0, never lock-free, capabilities from the last discovery), each fail the intended tests.
  • just test: 499 unit and 18 integration tests pass. just lint: 0 findings. Changed code is fully covered.
  • just validate <version> all 45 passes on 1.2.1, 1.2.0, 1.1.10, and 1.0.0, in 191–216 s each, with 0 violations.
    • latency: on 1.2.1, discovery during a build takes about 990 ms and ends about 3.1 s before the build. On the earlier versions it waits, ending 0.6–0.9 s after the build.
    • stress overlapped 11–25 discoveries per run with builds, cleans, or clobbers.
  • probe:syscalls: 0 writes on 1.2.1, 8 on 1.2.0. probe:overlap: no failed build or discovery command, and no file left by clobber, on either version.

🤖 Generated with Claude Code

mkarlesky and others added 3 commits October 25, 2026 13:05
… version gate

From Ceedling 1.2.1, discovery's dumpconfig and files:test write nothing, so
they skip the project's locks and a rediscovery no longer waits for a run.
Earlier versions rewrite the build root's vendored sources and keep the locks.
`ceedling version` never loads the project file, so discovery skips the locks
only when the project's last known configuration runs the installed gem, and a
project's first dumpconfig takes them.

A rediscovery that no longer waits makes two races reachable: a build that
outlives a rediscovery dropping its file no longer publishes stale diagnostics,
and a function run's whole-file fallback reruns on the project it began with.

Every version gate now reads one table, FIRST_RELEASES, through
capabilitiesOf(), firstRelease(), ceedlingRelease(), and isSupportedRelease().
Architecture.md describes the scheme and the evidence for the gate.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…bes trace more

latency now checks both sides of the gate and runs in `all`: from 1.2.1,
discovery's dumpconfig starts before the build ends; before, discovery ends
after it. stress adds clobber, checks every discovery's listing against
tests/manual, accepts the whole-file results of a --test-case substring
collision, and reports how many discoveries overlapped a build. probe:syscalls
counts writes outside build/ too, and probe:overlap adds a slow-clobber section
that counts failed discovery commands. Suites gate on the engine's capability
table instead of version literals. Development.md lists the checks to rerun on
each new Ceedling release.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…from 1.2.2

`ceedling version` reports a project running another installation, e.g. one in
vendor/ceedling or named by WHICH_CEEDLING, as a "Ceedling Launcher" and a
"Ceedling App" line. The parser read neither, so every such project failed to
load, `ceedling new --local` projects included.

parseCeedlingVersions() reads both. Before Ceedling 1.2.2 a launcher reports
another installation as its own release with that installation's commit
(ThrowTheSwitch/Ceedling#1365). applicationRelease() counts the application's
release as exact only from a 1.2.2 launcher, the new reportsApplicationVersion
capability, or when both report the same build. Otherwise the launcher's
release stands in, a warning is logged, and discovery keeps the locks.

`latency --vendored`, now in `all`, checks a vendored copy of the image's build
against real Ceedling. It fails on the previous engine and passes on 1.2.1,
1.2.0, and 1.0.0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant