From c734f47952d199858034413eda801effe6445bb5 Mon Sep 17 00:00:00 2001 From: agra Date: Fri, 12 Jun 2026 22:46:12 +0300 Subject: [PATCH] tests: pin pooled-dispatch concurrency; sqlite pins follow THREADSAFE=1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit server_http gains the case that caught the live crash: 1000 keep-alive requests at c=10 against /api/apps (full SQLite load per request, on the 4-worker pool) must complete with 0 failures and leave the server answering. sqlite_api's threadsafe pins flip to guard the NEW invariant — a regression to THREADSAFE=0 reintroduces heap corruption under the pool (free-of-unallocated inside yy_reduce, caught under ab -c20). --- tests/server_http.sx | 12 ++++++++++++ tests/sqlite_api.sx | 9 +++++---- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/tests/server_http.sx b/tests/server_http.sx index 8e34cf4..33660e4 100644 --- a/tests/server_http.sx +++ b/tests/server_http.sx @@ -206,6 +206,18 @@ main :: () -> i32 { "two requests on one connection both answer"); print(" keep-alive reuse ok\n"); + // ── concurrent load must not corrupt anything (PLAN-HTTPZ A2) ──── + // distd handlers run on a 4-worker pool; /api/apps does a full + // SQLite load per request. 1000 keep-alive requests at c=10 caught + // the SQLITE_THREADSAFE=0 heap corruption live — this pins the + // threadsafe build and the pooled dispatch path end to end. + abr := process.run(concat(concat("ab -q -n 1000 -c 10 -k ", BASE), "/api/apps 2>/dev/null | grep -c 'Failed requests: 0'")); + process.assert(abr != null and abr!.exit_code == 0, "ab spawn failed (concurrency case)"); + process.assert(contains(abr!.stdout, "1"), "1000 concurrent requests complete with 0 failures"); + alive := process.run(concat(concat("curl -s -m 2 -o /dev/null -w '%{http_code}' ", BASE), "/healthz")); + process.assert(alive != null and alive!.stdout == "200", "server alive after concurrent load"); + print(" pooled handlers: 1000 concurrent requests, 0 failures, server alive\n"); + // ── freshness: publish B while the server runs ──────────────────── rb := process.run(publish_cmd(path_join(MDIR, "b.json"))); process.assert(rb != null and rb!.exit_code == 0, "publish B must exit 0"); diff --git a/tests/sqlite_api.sx b/tests/sqlite_api.sx index 7d7fa5b..0a9594e 100644 --- a/tests/sqlite_api.sx +++ b/tests/sqlite_api.sx @@ -2,8 +2,9 @@ // every wrapper family beyond the P5.1 smoke (which keeps owning the // vendored-version pin): // -// * library: version_number, sourceid, threadsafe=false (built -// THREADSAFE=0), compileoption_used, complete, errstr, strglob/ +// * library: version_number, sourceid, threadsafe=true (built +// THREADSAFE=1 since std.thread pools share the library), +// compileoption_used, complete, errstr, strglob/ // strlike/stricmp, randomness. // * connections: open_v2 flag behavior (READONLY on a missing file // refuses; CREATE works; db_readonly reflects the mode), @@ -37,8 +38,8 @@ run_case :: (label: string, ok: bool) -> i32 { check_library_info :: () -> bool { if sq.sqlite_version_number() < 3053000 { return false; } if sq.sqlite_sourceid().len < 10 { return false; } - if sq.sqlite_threadsafe() { return false; } // THREADSAFE=0 build - if !sq.sqlite_compileoption_used("THREADSAFE=0") { return false; } + if !sq.sqlite_threadsafe() { return false; } // THREADSAFE=1: std.thread-era pools share the library (PLAN-HTTPZ A2) + if !sq.sqlite_compileoption_used("THREADSAFE=1") { return false; } if sq.sqlite_compileoption_get(0).len == 0 { return false; } return true; }