Skip to content

Commit feca5cf

Browse files
perf(db): cap error_logs.log_snippet at 16KB + v11 migration to truncate legacy rows
1 parent 4f11345 commit feca5cf

2 files changed

Lines changed: 55 additions & 6 deletions

File tree

core/db.js

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -440,6 +440,26 @@ function runMigrations(db) {
440440
db.prepare('UPDATE schema_version SET version = 10').run();
441441
})();
442442
}
443+
444+
if (current < 11) {
445+
db.transaction(() => {
446+
// ── Migration v11: cap error_logs.log_snippet at 16 KB ──────────────────
447+
// The inline log_snippet column historically stored up to 512 KB per
448+
// failure, bloating audit.db. The full log already lives on disk in the
449+
// per-run audit-*.log file, so the DB only needs a readable snippet for
450+
// the copy-failure UX. Going forward insertErrorLog() caps at 16384 chars
451+
// from the tail; this migration truncates any legacy oversized rows to
452+
// match. substr(s, length(s) - 16384 + 1) keeps the LAST 16384 chars,
453+
// mirroring the `.slice(-16384)` tail semantics. Bounded fast UPDATE —
454+
// safe on the boot path (no file I/O here).
455+
db.prepare(`
456+
UPDATE error_logs
457+
SET log_snippet = substr(log_snippet, length(log_snippet) - 16384 + 1)
458+
WHERE log_snippet IS NOT NULL AND length(log_snippet) > 16384
459+
`).run();
460+
db.prepare('UPDATE schema_version SET version = 11').run();
461+
})();
462+
}
443463
}
444464

445465
// ─── Run Mutations ───────────────────────────────────────────────────────────
@@ -906,7 +926,7 @@ export function getRunStats(runId, which) {
906926
* @param {string} opts.stage - 'handshake'|'session'|'speedtest'|'wallet'|'rpc'|'other'
907927
* @param {string} [opts.error_code]
908928
* @param {string} [opts.error_message]
909-
* @param {string} [opts.log_snippet] - up to 512 KB of log context
929+
* @param {string} [opts.log_snippet] - a readable snippet of log context (capped at 16 KB)
910930
* @returns {number} inserted id
911931
*/
912932
export function insertErrorLog({
@@ -917,10 +937,14 @@ export function insertErrorLog({
917937
log_snippet = null,
918938
}, which) {
919939
const db = getDb(which);
920-
// Persist the full per-node tester log (capped at 512 KB to bound row
921-
// size). Pipeline already trims to ~512 KB from the tail; this is the
922-
// floor cap so a misbehaving caller can't blow the row.
923-
const snippet = log_snippet ? log_snippet.slice(-524288) : null;
940+
// Store only a readable snippet of the per-node tester log (capped at 16 KB
941+
// = 16384 chars from the tail). The FULL log is persisted to disk in the
942+
// per-run audit-*.log file; the DB column exists purely to power the
943+
// copy-failure UX, so a bounded tail is all we need. Production callers
944+
// already pre-truncate to ~4 KB via pipeline.js (_sanitizeSnippet /
945+
// _MAX_SNIPPET); this slice is the backstop so a misbehaving caller can't
946+
// blow up the row and bloat audit.db.
947+
const snippet = log_snippet ? log_snippet.slice(-16384) : null;
924948
const info = db.prepare(`
925949
INSERT INTO error_logs (result_id, stage, error_code, error_message, log_snippet, captured_at)
926950
VALUES (@result_id, @stage, @error_code, @error_message, @log_snippet, @captured_at)

test/db.smoke.test.js

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
import { getDb, useDb, insertRun, updateRunOnFinish, insertResult, insertResultsBatch,
1010
getRun, findRunByKey, listRuns, getLatestResultPerNode,
11-
getNodeHistory, getNetworkStats, closeDb } from '../core/db.js';
11+
getNodeHistory, getNetworkStats, insertErrorLog, getNodeErrors, closeDb } from '../core/db.js';
1212

1313
// ─── Use a fresh in-memory DB for this test run ───────────────────────────────
1414
// `useDb` sets the module singleton so all exported helpers target this handle.
@@ -182,6 +182,31 @@ const parsed = JSON.parse(history2[0].raw_json);
182182
eq(parsed.address, 'sentnode1aaa111', 'raw_json.address preserved');
183183
eq(parsed.moniker, 'TestNode Alpha', 'raw_json.moniker preserved');
184184

185+
// 12. schema_version is up to date (latest forward migration applied)
186+
console.log('12. schema_version...');
187+
const schemaRow = db.prepare('SELECT MAX(version) AS version FROM schema_version').get();
188+
eq(schemaRow.version, 11, 'freshly-opened DB reports schema_version 11');
189+
190+
// 13. error_logs.log_snippet is capped at 16 KB (16384 chars)
191+
console.log('13. error_logs log_snippet cap...');
192+
// Insert a fresh failed result so we have a result_id, then attach an
193+
// oversized log snippet. insertErrorLog must clamp it to the 16 KB backstop.
194+
const capRunId = insertRun({ started_at: NOW - 5_000, mode: 'p2p' });
195+
const capResultId = Number(insertResult(Number(capRunId), { ...SAMPLE_FAIL, address: 'sentnode1cap999' }));
196+
insertErrorLog({
197+
result_id: capResultId,
198+
stage: 'handshake',
199+
error_code: 'HANDSHAKE_TIMEOUT',
200+
error_message: 'oversized snippet stress',
201+
log_snippet: 'x'.repeat(50000),
202+
});
203+
const capErrors = getNodeErrors('sentnode1cap999', { limit: 1 });
204+
assert(capErrors.length >= 1, 'getNodeErrors returns the inserted error log');
205+
assert(
206+
capErrors[0].log_snippet != null && capErrors[0].log_snippet.length <= 16384,
207+
`log_snippet capped at <= 16384 chars (got ${capErrors[0].log_snippet?.length})`,
208+
);
209+
185210
// ─── Results ──────────────────────────────────────────────────────────────────
186211

187212
console.log(`\n${'='.repeat(50)}`);

0 commit comments

Comments
 (0)