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.
This commit is contained in:
Tim Black
2026-02-04 17:47:28 -08:00
committed by GitHub
parent 58d0318f3d
commit 24612002cb
2 changed files with 81 additions and 6 deletions

View File

@@ -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;

View File

@@ -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.',
);
}
});