A gate job's output, a run's stderr and a gate's stream pieces are cut on a character boundary, and sak stores only well-formed UTF-8 #143
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "task/3799-a-gate-job-cuts-its-output-tail-on-a-byt"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Tracker: #3799
A failed gate's output longer than 64 KiB was kept from a byte offset, so it could start inside a multi-byte character. sak stored that byte in the job's
output, copied it into the next run's step instructions, and served invalid UTF-8 from/api/jobs/<id>,/api/runs/<id>and every page of/api/runsholding that run (job 131 and run 689 on the live tracker).What this delivers
len - OUTPUT_KEEPthat is not a UTF-8 continuation byte (util.char_from, runner/job.sx).util.whole_len): the first 4 KiB of a harness's stderr on the stream's close (runner/run.sx), and a gate's stream piece once 16 KiB are pending (runner/gate.sxFeed.put). Before, the piece could end on a character's first byte and the next piece start on its second.object_body(src/api.sx) replaces each maximal ill-formed UTF-8 subsequence of a JSON body with U+FFFD before parsing it, so every string of every JSON body is well-formed whichever runner sent it. The JSON parser already refuses lone surrogates in\uescapes, so raw bytes were the only way in.apply_repair, src/db.sx). That covers run 689's step instructions and job 131's output, and any stream piece that was split.lead_of,agreeingandutf8_sequence_len, besidewell_formed,char_fromandwhole_len.Migration 45 is also the number #3724's branch takes. Whichever of the two merges second renumbers its migration to 46.
Regressions, fail-before and pass-after
Each arm is a copy of the branch's files at
b376cf1with one change reverted, run on m5 with the gate cache's sx (300086027f).tests/runner_core.sx, three new checks in
check_gate_job_tail:✓(66000 bytes). The last 65536 bytes start on a✓'s third byte (0x93, the task's case).POST /api/jobs/:id/completewithoutputstarting with a lone0x93.✓'s first byte, then its other two bytes half a second later.b376cf1object_bodyrepair removedflush(true)restored at FLUSH_BYTESThe head's other FAIL in that by-hand round was "a sak-gate killed while its gate runs has printed only the gate's start" (
check_gate_reattach). It is the timing check #3629 covers, and it failed only because five runner_core runs were going side by side. The four arms passed it in the same window, and thefullgate's runner_core passed every check at this head.tests/utf8_text.sx (new) covers four things:
well_formed: a lone continuation byte, a sequence cut short and an overlong pair each become U+FFFD, one per maximal subpart.char_fromandwhole_len.The helpers do not exist at master. With
apply_repair(db, 45)taken out ofmigrate, 3 checks fail: "the db opens at the schema head", "migration 45 reaches the schema head" and "it rewrites each ill-formed sequence it finds as U+FFFD".The stderr cut is covered only through
whole_len's checks.Migration 45 took 3.6 s on m5, under load, over a 692 MB db of 500,000 stream events of about 1 KB each, and it repaired the one ill-formed event seeded among them.
The schema head moves to 45, so every test that asserts it now says 45.
No golden is added or changed: nothing under
web/changes.Gates at the head
full(run #2, tracker gate run 387) atb376cf1: passed. Every step passed: the builds,npm run build(vitest 401/401), the libc grep, everytests/*.sx(runner_core and utf8_text among them, every check), release with step9, the deploy contract, the main Playwright config (740 passed, 1 skipped) and the runner config (10 passed).fast(run #1, tracker gate run 388) atb376cf1: passed.