From 24612002cb955f5e2b949e78f3810b0ccc071683 Mon Sep 17 00:00:00 2001 From: Tim Black Date: Wed, 4 Feb 2026 17:47:28 -0800 Subject: [PATCH] fix radisk (#1416) Previously, when a file split occurred, the operations to write the new split file and update (truncate) the existing file were initiated in parallel. This created a race condition: if writing the new file failed but updating the existing file succeeded, the data intended for the new file would be permanently lost. This change strictly sequences the operations. The existing file is now only updated after the new split file has been successfully written and acknowledged, ensuring data integrity even in the event of partial write failures. --- lib/radisk.js | 14 +++--- test/panic/radisk_split_failure.js | 73 ++++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+), 6 deletions(-) create mode 100644 test/panic/radisk_split_failure.js diff --git a/lib/radisk.js b/lib/radisk.js index fd83fddb..2075d3e8 100644 --- a/lib/radisk.js +++ b/lib/radisk.js @@ -189,12 +189,14 @@ f.sub = Radix(); Radix.map(rad, f.slice, {reverse: 1}); // IMPORTANT: DO THIS IN REVERSE, SO LAST HALF OF DATA MOVED TO NEW FILE BEFORE DROPPING FROM CURRENT FILE. DBG && (DBG.wf2 = +new Date); - r.write(f.end, f.sub, f.both, o); - DBG && (DBG.wf3 = +new Date); - f.hub = Radix(); - Radix.map(rad, f.stop); - DBG && (DBG.wf4 = +new Date); - r.write(rad.file, f.hub, f.both, o); + r.write(f.end, f.sub, function(err, ok){ + if(err){ return cb(err) } + DBG && (DBG.wf3 = +new Date); + f.hub = Radix(); + Radix.map(rad, f.stop); + DBG && (DBG.wf4 = +new Date); + r.write(rad.file, f.hub, cb, o); + }, o); DBG && (DBG.wf5 = +new Date); console.STAT && console.STAT(S, +new Date - S, "rad split", ename(rad.file), SC); return true; diff --git a/test/panic/radisk_split_failure.js b/test/panic/radisk_split_failure.js new file mode 100644 index 00000000..b393dfd4 --- /dev/null +++ b/test/panic/radisk_split_failure.js @@ -0,0 +1,73 @@ +var Radisk = require('../../lib/radisk.js'); +var Radix = require('../../lib/radix.js'); + +// Mock options +var opt = { + file: 'radata_test_split_failure', + chunk: 10, // Very small chunk to force split + store: { + get: function (file, cb) { + cb(null, null); + }, + put: function (file, data, cb) { + cb(null, 'ok'); + }, + }, + log: function () {}, // Silence logs +}; + +var r = Radisk(opt); + +// Construct a radix tree that needs splitting +// Key size + value size + overhead > 10 +// Entry: len#key:val\n +// 3#key:val\n -> ~10 chars +var tree = Radix(); +// Add enough keys to ensure split happens (need > 1 key) +for (var i = 10; i < 20; i++) { + tree('k' + i, 'val' + i); +} +// tree file +tree.file = 'root_file'; + +console.log('--- TEST: Split Failure Safety ---'); + +var putCalls = []; +opt.store.put = function (file, data, cb) { + putCalls.push(file); + console.log('Store.put called for:', file); + + if (file === 'root_file') { + // This is the old file being overwritten/truncated + cb(null, 'ok'); + } else { + // This is the new split file + console.log('Simulating FAILURE for new file:', file); + cb('MockWriteError'); + } +}; + +r.write('root_file', tree, function (err, ok) { + console.log('Callback received:', err); + + // Check results + var newFileCalls = putCalls.filter(function (f) { + return f !== 'root_file'; + }); + var oldFileCalls = putCalls.filter(function (f) { + return f === 'root_file'; + }); + + if (newFileCalls.length === 0) { + console.log('FAILURE: Did not attempt to write new file (Did not split?)'); + } else if (oldFileCalls.length > 0) { + console.log( + 'FAILURE: Old file was written despite new file failure! DATA LOSS RISK.', + ); + console.log('Writes:', putCalls); + } else { + console.log( + 'SUCCESS: Old file was NOT written after new file failure. Data is safe.', + ); + } +});