Files
AILang/ail-embed/tests/swarm.rs
T
Brummel b724cd17a1 iter embedding-abi-m5.2 (PARTIAL 2/3): data-server adapter + symbol-fan swarm; leak-proof surfaced a non-atomic global g_rc_* stats race
M5 iteration 2 (spec ae905de, plan 9cc9d9c). Tasks 1+2 clean and
committable; Task 3 BLOCKED on a real finding → M5 bounce-back.

Shipped:
- data-server promoted [dev-dependencies] -> [dependencies]
  (Invariant-1 sanctioned: ail-embed is the sole meeting point; the
  AILang workspace graph still has data-server count 0; zero
  compiler-surface diff — not even root Cargo.toml this iter).
- additive `adapter` module (ail-embed/src/adapter.rs): tick_to_px,
  MidPriceStream (lazy Iterator<f64> over SymbolChunkIter),
  fold_symbol; RED-first (E0432 -> GREEN). m5.1 core untouched
  except `pub mod adapter;`.
- swarm_runner [[bin]]: one thread per symbol, each owning its
  Kernel. The clean compile IS the compile-time per-thread-ctx
  proof (Ctx: !Send => a shared-ctx swarm is E0277).
- tests/swarm.rs: real-data symbol-fan E2E. `symbol_fan_swarm_bit_exact`
  is GREEN and live — per-symbol kernel (acc,n) bit-exact vs an
  independent same-order host reference (EURUSD/GER40/XAUUSD, ~4s).

The Task 3 finding (Boss independently confirmed by reading
runtime/rc.c): the global Σallocs==Σfrees leak measurement is an
INSTRUMENTATION race, not a memory bug. No box crosses a thread
(Ctx: !Send), the real refcount/free is correct (bit-exact GREEN
every run); only the global g_rc_* stat counters (rc.c:90-91,
161,212) are non-atomic BY rc.c's own documented single-threaded
design, losing ++s when 3 worker threads hit the null-__ail_tls_ctx
host-side path. M5 is AILang's first concurrent consumer; rc.c's
header (rc.c:44-49) explicitly defers exactly this atomic-vs-non-
atomic decision to "when it acquires concurrency primitives".

The leak assertion was Boss-split into `symbol_fan_swarm_leak_free`
(#[ignore], body preserved VERBATIM — quarantined not weakened;
un-ignore = the runtime fix's acceptance criterion) so main stays
green and the finding is pinned as a regression marker. The earlier
plan/journal claim that embed_swarm_tsan.rs covers this path was
wrong and is corrected on the record (that test uses the scalar
kernel — zero box allocs — so it never exercised the host-side
global-counter path).

Escalated as an M5 bounce-back: resolution touches M3-frozen
runtime/ and re-frames M5's "zero runtime change" commitment;
multiple substantive options, not unilaterally Boss's to pick in
frozen-runtime territory. Boss recommendation on the record =
Option A (make only the global-fallback g_rc_* counters atomic, as
a standalone RED-first runtime micro-iteration; M5 framing amended).
Time-shard + friction-harvest remain m5.3.

Includes the per-iter journal (with Boss disposition), stats, and
the INDEX.md line.
2026-05-19 01:37:50 +02:00

165 lines
7.0 KiB
Rust

//! Symbol-fan swarm E2E (spec Testing §2). Runs the `swarm_runner`
//! bin as a subprocess under `AILANG_RC_STATS=1` (the C-runtime
//! atexit stat line, runtime/rc.c:124-138, is only observable from a
//! separate process that exits). Skips when /mnt tick data is absent
//! — mirroring data-server's own tests/data_server.rs::skip_if_no_data().
//!
//! Two assertions, deliberately SPLIT into two tests:
//!
//! - `symbol_fan_swarm_bit_exact` — the existence proof. Per symbol,
//! the kernel `(acc,n)` is BIT-EXACT vs an independent same-order
//! host reference fold. This is GREEN and live: it proves the
//! real-data-server → adapter → M3-kernel swarm computes correctly.
//!
//! - `symbol_fan_swarm_leak_free` — `#[ignore]`d pending an escalated
//! design decision. The global `Σallocs==Σfrees` measurement
//! (ported from crates/ail/tests/embed_tick_e2e.rs:94-104) is
//! *invalid under a multi-threaded host*: host-side box allocs run
//! with `__ail_tls_ctx == NULL` and fall through to the
//! **non-atomic** global `g_rc_*` counters (runtime/rc.c:90-91,
//! 161,212), which rc.c's own header explicitly documents as
//! single-threaded/non-atomic ("when it acquires [concurrency],
//! atomic-vs-non-atomic becomes a separate decision"). The swarm is
//! *actually* leak-free (no box crosses a thread — `Ctx: !Send`;
//! bit-exact is GREEN every run); only the global *counter* races,
//! losing increments. The assertion body is preserved verbatim and
//! will be re-armed (un-`#[ignore]`d) by the runtime iteration that
//! makes the global fallback counters atomic. See
//! docs/journals/2026-05-19-iter-embedding-abi-m5.2.md and the M5
//! bounce-back. Quarantined, NOT weakened — what it asserts is
//! unchanged; it is gated on a decision the Boss escalated.
use std::process::Command;
use std::sync::Arc;
use data_server::records::DataFormat;
use data_server::{DataServer, DEFAULT_DATA_PATH};
// Must match swarm_runner.rs.
const MAX_TICKS: usize = 2_000_000;
const N_SYMBOLS: usize = 4;
fn skip_if_no_data() -> bool {
!std::path::Path::new(DEFAULT_DATA_PATH).exists()
}
/// Independent host reference: same symbols, same order, same cap,
/// pure-Rust fold (`acc += mid; n += 1`). Bit-exact with the kernel
/// fold because the addition order is identical.
fn reference(symbol: &str) -> (f64, i64) {
let server = Arc::new(DataServer::new(DEFAULT_DATA_PATH));
let mut it = server.stream_tick(symbol).expect("tick stream");
let (mut acc, mut n) = (0.0_f64, 0_i64);
'outer: while let Some(chunk) = it.next_chunk() {
for r in chunk.iter() {
if n as usize >= MAX_TICKS {
break 'outer;
}
acc += (r.ask + r.bid) / 2.0;
n += 1;
}
}
(acc, n)
}
/// Spawn `swarm_runner` under `AILANG_RC_STATS=1`. `None` on the
/// skip-if-absent path (no /mnt data); otherwise `(stdout, stderr)`
/// after asserting a clean child exit.
fn run_swarm_or_skip() -> Option<(String, String)> {
if skip_if_no_data() {
eprintln!("skipping: {DEFAULT_DATA_PATH} absent (mirrors data-server)");
return None;
}
let out = Command::new(env!("CARGO_BIN_EXE_swarm_runner"))
.arg(DEFAULT_DATA_PATH)
.env("AILANG_RC_STATS", "1")
.output()
.expect("spawn swarm_runner");
let stdout = String::from_utf8_lossy(&out.stdout).into_owned();
let stderr = String::from_utf8_lossy(&out.stderr).into_owned();
assert!(
out.status.success(),
"swarm_runner exited {:?}\nstdout:\n{stdout}\nstderr:\n{stderr}",
out.status.code()
);
Some((stdout, stderr))
}
/// Existence proof (GREEN, live): each symbol's kernel `(acc,n)` is
/// bit-exact vs an independent same-order host reference fold. Real
/// data-server → adapter → M3-frozen kernel, one symbol per thread.
#[test]
fn symbol_fan_swarm_bit_exact() {
let Some((stdout, _stderr)) = run_swarm_or_skip() else { return };
let mut symbols_checked = 0usize;
for line in stdout.lines().filter(|l| l.starts_with("RESULT ")) {
let mut t = line.split_whitespace();
let _ = t.next(); // "RESULT"
let sym = t.next().expect("symbol field");
let acc_bits = u64::from_str_radix(
t.next().expect("acc-bits field"), 16,
).expect("acc-bits hex");
let n: i64 = t.next().expect("n field").parse().expect("n int");
let kernel_acc = f64::from_bits(acc_bits);
let (ref_acc, ref_n) = reference(sym);
assert_eq!(
kernel_acc.to_bits(), ref_acc.to_bits(),
"{sym}: kernel acc bit-exact vs same-order host reference"
);
assert_eq!(n, ref_n, "{sym}: tick count matches reference");
assert!(ref_n > 0, "{sym}: non-empty stream");
symbols_checked += 1;
}
assert!(
(2..=N_SYMBOLS).contains(&symbols_checked),
"expected 2..={N_SYMBOLS} symbols fanned, got {symbols_checked}"
);
}
/// Global leak-freedom: Σallocs == Σfrees across ALL `ailang_rc_stats:`
/// lines (the M2 dual-stat-line model, ported from
/// embed_tick_e2e.rs:94-104). The invariant is the global Σ, not
/// per-line balance (M2 TLS-ctx cross-attribution).
///
/// `#[ignore]`d: this measurement is invalid under a multi-threaded
/// host. Host-side box allocs run with `__ail_tls_ctx == NULL` and
/// hit the non-atomic global `g_rc_*` counters (runtime/rc.c:90-91,
/// 161,212) — single-threaded/non-atomic *by rc.c's own documented
/// design*. 3 worker threads race the counter, losing increments;
/// the *counter* is unreliable even though the swarm is actually
/// leak-free (no box crosses a thread; `symbol_fan_swarm_bit_exact`
/// is GREEN every run). Un-`#[ignore]` this — body unchanged — once
/// the runtime iteration makes the global fallback counters atomic
/// (the deferred "concurrency arrived" decision rc.c:44-49 names).
/// See docs/journals/2026-05-19-iter-embedding-abi-m5.2.md.
#[test]
#[ignore = "blocked: global g_rc_* counter is non-atomic by rc.c design; \
invalid under the multi-thread host. Swarm is actually \
leak-free (bit-exact is green; no box crosses a thread). \
Un-ignore when the runtime makes the global fallback \
counters atomic — see the m5.2 journal + M5 bounce-back."]
fn symbol_fan_swarm_leak_free() {
let Some((_stdout, stderr)) = run_swarm_or_skip() else { return };
let mut allocs: u64 = 0;
let mut frees: u64 = 0;
let mut seen = 0usize;
for line in stderr.lines().filter(|l| l.starts_with("ailang_rc_stats:")) {
seen += 1;
for tok in line.split_whitespace() {
if let Some(v) = tok.strip_prefix("allocs=") {
allocs += v.parse::<u64>().expect("allocs= u64");
} else if let Some(v) = tok.strip_prefix("frees=") {
frees += v.parse::<u64>().expect("frees= u64");
}
}
}
assert!(seen > 0, "no ailang_rc_stats line; stderr:\n{stderr}");
assert_eq!(
allocs, frees,
"globally leak-free: Σallocs==Σfrees across {seen} stat line(s)"
);
}