You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix(cleanup/db): harden raw_json offload, reclaim orphan raw dirs, keep all unfinished batches, correct char-vs-byte labels
Closes the 4 defects from the adversarial review of the DB slim-down /
disk-hardening work:
FIX 1 (MED) — cleanup.mjs raw_json backfill now validates a JSON round-trip
before NULLing the column. Action 1 previously only stat-checked size>0; a
non-empty file can still be truncated/corrupt. It now reads the written file
back and JSON.parse()s it, only adding the id to the to-be-NULLed set when the
parse SUCCEEDS. Any read/parse failure leaves the column intact, logs a warning
naming run_id/result_id, and counts toward the skipped total reported in fix
output.
FIX 2 (MED/LOW) — orphan raw dirs are now reclaimable. Added a namespace-correct
orphan-raw-dir sweep to cleanup.mjs (new report section 7 + a --fix reclaim
gated on the existing backup+checkpoint guard): run-<id> dirs whose <id> has no
row in the runs table are orphans; reported with reclaimable bytes and rm -rf'd
under --fix. Handles RAW_DIR absent; stray non-run-<int> entries are reported
but never deleted. deleteRun() in server.js is deliberately UNCHANGED (only a
clarifying comment added) — it keys on the file-index run NUMBER, a different
namespace from the runs-table id that keys raw dirs, and no mapping holds for
every path (dbRunId is null for legacy runs and absent for the
interrupted/brand-new run case, where the only id available is the LIVE run's).
A wrong-id rm would destroy live raw data; correctness over completeness.
FIX 3 (LOW) — pruneBatchResults() now keeps ALL unfinished batches, not just
getActiveBatch's single newest one. Keep-set step (a) selects every id from
batches WHERE finished_at IS NULL, so a second unfinished batch's batch_results
can no longer be stripped (which would orphan its parent row, since the parent
DELETE guards finished_at IS NOT NULL). Added a :memory: test asserting both
unfinished batches survive while old finished ones are pruned, with no orphaned
children and a clean foreign_key_check.
FIX 4 (LOW) — corrected "16KB"/"16 KB"/byte wording to "16384 chars (UTF-8 byte
size varies)" at every mislabeled site in core/db.js (insertErrorLog cap site +
v11 migration block) and scripts/cleanup.mjs (LOG_SNIPPET_CAP + report/fix
messages). Numeric value and behavior are unchanged.
The :memory: raw_json INLINE behavior, TEST RUN paths, and ETA code are
untouched. npm test + npm run test:integration both fully green.
// (and, under --fix, performs) the one-time migration of that legacy data.
235
237
section('6. DB slim-down (legacy backfill)');
236
238
letrawJsonRows=0;// results rows still holding an inline raw_json blob
237
-
letlogSnippetRows=0;// error_logs rows whose snippet exceeds the 16 KB cap
239
+
letlogSnippetRows=0;// error_logs rows whose snippet exceeds the 16384-char cap (UTF-8 byte size varies)
238
240
letbatchCount=0;// total batches
239
241
letbatchResultRows=0;// total batch_results rows
240
242
if(rdb){
@@ -249,7 +251,7 @@ if (rdb) {
249
251
try{
250
252
logSnippetRows=rdb.prepare(`SELECT COUNT(*) AS c FROM error_logs WHERE log_snippet IS NOT NULL AND length(log_snippet) > ${LOG_SNIPPET_CAP}`).get().c;
251
253
logSnippetRows>0
252
-
? repairable(`${logSnippetRows.toLocaleString()} error_logs row(s) exceed the 16 KB log_snippet cap (would truncate to tail)`)
254
+
? repairable(`${logSnippetRows.toLocaleString()} error_logs row(s) exceed the 16384-char log_snippet cap (UTF-8 byte size varies; would truncate to tail)`)
repairable(`orphan raw dir ${ent.name} (no runs row for id=${id}, ${humanBytes(bytes)} reclaimable)`);
316
+
}
317
+
if(!orphanRawDirs.length)ok('no orphan raw dirs (every run-<id> maps to a runs row)');
318
+
else{repairable(`${orphanRawDirs.length} orphan raw dir(s), ${humanBytes(orphanRawBytes)} total reclaimable`);if(!FIX)console.log(C.dim(' (pass --fix to rm -rf these orphan dirs)'));}
319
+
}
320
+
266
321
// Close our OWN read-only handle before mutating: if it stays open, the WAL
267
322
// checkpoint below can report `busy` and falsely abort the whole --fix on a
268
323
// perfectly healthy, server-stopped box.
@@ -379,11 +434,14 @@ if (FIX) {
379
434
try{
380
435
mkdirSync(dir,{recursive: true});
381
436
writeFileSync(file,row.raw_json);
382
-
// VERIFY before we agree to null: file exists and is non-empty.
383
-
constst=statSync(file);
384
-
if(st.size>0)ready.push(row.id);
385
-
else{skippedWrite++;hard(`raw_json offload: ${file} wrote 0 bytes — left column intact for result ${row.id}`);}
386
-
}catch(e){skippedWrite++;hard(`raw_json offload: write failed for result ${row.id} — left column intact: ${e.message}`);}
437
+
// VERIFY before we agree to null: read the file back and JSON.parse
438
+
// it. A non-empty file can still be truncated/corrupt — size>0 is not
439
+
// enough. Only NULL the column when the round-trip parse SUCCEEDS;
440
+
// any read/parse failure leaves the column intact (no data loss).
441
+
constback=readFileSync(file,'utf8');
442
+
JSON.parse(back);// throws on truncated/corrupt JSON
443
+
ready.push(row.id);
444
+
}catch(e){skippedWrite++;hard(`raw_json offload: round-trip verify failed for run-${row.run_id}/result ${row.id} (${file}) — left column intact: ${e.message}`);}
`UPDATE error_logs SET log_snippet = substr(log_snippet, length(log_snippet) - ${LOG_SNIPPET_CAP} + 1) WHERE log_snippet IS NOT NULL AND length(log_snippet) > ${LOG_SNIPPET_CAP}`,
403
461
).run();
404
462
capRes.changes>0
405
-
? fixd(`capped ${capRes.changes.toLocaleString()} oversized log_snippet row(s) to 16 KB tail`)
463
+
? fixd(`capped ${capRes.changes.toLocaleString()} oversized log_snippet row(s) to the 16384-char tail (UTF-8 byte size varies)`)
0 commit comments