feat: make C API exports thread-safe - #30
Conversation
7a612d2 to
6f49f39
Compare
6f49f39 to
d7df0f8
Compare
Greptile SummaryThis PR adds process-wide serialization for runner FIFO request/response exchanges, per-handle locking for environment data, atomic feature flags, concurrency tests, regenerated C distribution artifacts, and an explicit thread-safety contract. Two synchronization gaps remain:
Confidence Score: 3/5The PR is not safe to merge until shared FIFO teardown is synchronized and the concurrent benchmark-lifecycle guarantee is corrected or implemented. Per-command locking prevents request/reply pairing between normal exchanges, but an unlocked reader teardown can still consume another handle's reply. Additionally, arbitrary concurrent start and stop operations are serialized in nondeterministic order against a runner with one global lifecycle state, causing dropped boundaries and incorrect benchmark attribution. Files Needing Attention: src/runner_fifo.zig, CUSTOM_HARNESS.md
|
| Filename | Overview |
|---|---|
| src/runner_fifo.zig | Adds process-global request/response serialization, but leaves shared acknowledgement-FIFO draining outside that synchronization. |
| src/lock.zig | Introduces an acquire/release yield-spinlock and process-global lock instance with focused mutual-exclusion tests. |
| src/environment/root.zig | Serializes environment setters and reporting on each handle without introducing a confirmed contract violation. |
| src/features.zig | Replaces the non-atomic feature bit set with atomic bitwise updates and reads. |
| src/c.zig | Adds a concurrent multi-handle FIFO test, though its scheduling does not deterministically cover teardown draining an in-flight reply. |
| CUSTOM_HARNESS.md | Adds a thread-safety guarantee that overstates the safety of concurrently interleaved benchmark lifecycle calls. |
| dist/core.c | Regenerates the distributed Linux and macOS C implementations with the new locks and atomic feature state. |
Sequence Diagram
sequenceDiagram
participant A as Handle A
participant L as Global exchange lock
participant C as Control FIFO
participant R as Runner
participant K as Shared acknowledgement FIFO
participant B as Handle B teardown
A->>L: acquire
A->>C: send command
C->>R: command
R-->>K: reply
B->>K: drain during deinit (unlocked)
K--xB: consumes A's reply
A->>K: wait for reply
K--xA: timeout or partial frame
A->>L: release
Prompt To Fix All With AI
### Issue 1
src/runner_fifo.zig:56-57
**Teardown Can Steal Replies**
The global lock protects only `sendCmd` and its response wait, while `RunnerFifo.deinit` drains the process-wide acknowledgement FIFO without taking that lock. If one handle is destroyed—or an instrument probe fails and tears itself down—while another handle is waiting for a reply, the drain can consume that reply from the shared pipe. The in-flight request can then time out or decode a partial or stale frame. Reader teardown and draining need the same process-wide synchronization as normal exchanges.
### Issue 2
CUSTOM_HARNESS.md:224
**Concurrent Lifecycles Lose Boundaries**
This promises that per-thread benchmark iterations may call `start_benchmark` and `stop_benchmark` concurrently, but the lock only serializes individual commands and does not preserve each thread's lifecycle. The runner keeps one process-wide `benchmark_started` flag, so it drops a second start as a duplicate and drops a stop received before a start. Concurrent iterations can therefore leave measurement windows unbalanced or associate identities and markers with the wrong benchmark. Either restrict the guarantee to calls whose semantic order is externally coordinated or implement per-iteration lifecycle coordination. See the runner's [shared lifecycle state and command handling](https://github.com/codspeedhq/codspeed/blob/HEAD/src/executor/shared/fifo.rs#L182-L238).
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.Reviews (1): Last reviewed commit: 6f49f39 | Re-trigger Greptile
Divan's threads feature calls the hooks concurrently from several threads, which interleaved writes to the runner control FIFO and let a reader consume another thread's ack, hanging the runner. Serialize the exports behind a process-global lock. The lock is global rather than per-instance because the FIFO paths are fixed, so all InstrumentHooks in a process share one transport and one ack stream. The lock is a CAS yield-spinlock in Zig instead of std.Thread.Mutex or pthread_mutex_t: the sources are transpiled to C once per OS, so a futex mutex would bake x86_64 syscall asm into the output and pthread_mutex_t would bake in its transpile-time size, both wrong on other architectures.
The macos-latest image now ships an SDK whose libSystem Zig 0.14 cannot link against, so every job that links natively fails with undefined libc symbols. setup-zig resolves the toolchain from build.zig.zon's minimum_zig_version, and 0.14.1 fails the same way, so the runner image is pinned instead. This reproduces on an unmodified main, so it is independent of the changes in this branch.
91a0e25 to
4c76dbb
Compare
NOTE: Didn't put the spin-lock into the C exports as we still may want to have it be lock-free in simulation/other instruments. Handling the synchronization at the exact place where it's needed, rather than at the boundary is better imo.