Verify document deletion recovery across forced process termination
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {spawn} from 'node:child_process';
|
||||
import {mkdtempSync,rmSync,existsSync,readFileSync} from 'node:fs';
|
||||
import {tmpdir} from 'node:os';
|
||||
import {join} from 'node:path';
|
||||
|
||||
// All fault injection is confined to a disposable child process and temp volume.
|
||||
async function killedDeletion(phase,dir) {
|
||||
const moduleUrl=new URL('../server/user-store.mjs',import.meta.url).href;
|
||||
const code=`
|
||||
import fs from 'node:fs';
|
||||
import {DatabaseSync} from 'node:sqlite';
|
||||
const store=await import(${JSON.stringify(moduleUrl)});
|
||||
const email='crash@example.test';
|
||||
const bytes=Buffer.from('Kesinti sırasında korunacak gerçek belge baytları.');
|
||||
const id=store.saveDocumentFile(email,{ad:'crash.txt'},bytes);
|
||||
const path=store.BELGE_DIR+'/'+store.getDocument(email,id).dosya;
|
||||
const pause=()=>{process.send({phase:${JSON.stringify(phase)},id,path});Atomics.wait(new Int32Array(new SharedArrayBuffer(4)),0,0);};
|
||||
if(${JSON.stringify(phase)}==='before-commit') {
|
||||
const original=DatabaseSync.prototype.prepare;
|
||||
DatabaseSync.prototype.prepare=function(sql){
|
||||
const statement=original.call(this,sql);
|
||||
if(sql==='DELETE FROM belgeler WHERE id=? AND email=?') {
|
||||
const run=statement.run.bind(statement);
|
||||
statement.run=(...args)=>{const result=run(...args);pause();return result;};
|
||||
}
|
||||
return statement;
|
||||
};
|
||||
} else {
|
||||
const original=fs.unlinkSync;
|
||||
fs.unlinkSync=function(candidate){if(candidate===path)pause();return original(candidate);};
|
||||
}
|
||||
store.deleteDocument(email,id);
|
||||
process.exit(3);
|
||||
`;
|
||||
const child=spawn(process.execPath,['--input-type=module','-e',code],{
|
||||
env:{...process.env,IHALEIST_USER_DATA_DIR:dir},stdio:['ignore','ignore','pipe','ipc'],
|
||||
});
|
||||
let stderr='';child.stderr.on('data',chunk=>{stderr+=chunk;});
|
||||
let timer;
|
||||
try {
|
||||
const message=await new Promise((resolve,reject)=>{
|
||||
child.once('message',resolve);
|
||||
child.once('error',reject);
|
||||
child.once('exit',(code,signal)=>reject(new Error(`Child exited before checkpoint: ${code}/${signal} ${stderr}`)));
|
||||
timer=setTimeout(()=>reject(new Error('Child did not reach deletion checkpoint')),10000);
|
||||
});
|
||||
clearTimeout(timer);
|
||||
const stopped=new Promise(resolve=>child.once('exit',(code,signal)=>resolve({code,signal})));
|
||||
child.kill('SIGKILL');
|
||||
assert.equal((await stopped).signal,'SIGKILL');
|
||||
return message;
|
||||
} finally {clearTimeout(timer);if(child.exitCode===null && child.signalCode===null)child.kill('SIGKILL');}
|
||||
}
|
||||
|
||||
for(const phase of ['before-commit','after-commit']) {
|
||||
test(`document deletion recovers after real SIGKILL ${phase}`,async()=>{
|
||||
const dir=mkdtempSync(join(tmpdir(),'ihaleist-crash-'));
|
||||
try {
|
||||
const checkpoint=await killedDeletion(phase,dir);
|
||||
assert.equal(existsSync(checkpoint.path),true,'file must still exist at the forced crash checkpoint');
|
||||
// Inspect recovery in another process: no cached connection can mask WAL recovery.
|
||||
const code=`
|
||||
const store=await import(${JSON.stringify(new URL('../server/user-store.mjs',import.meta.url).href)});
|
||||
const record=store.getDocument('crash@example.test',${JSON.stringify(checkpoint.id)});
|
||||
const db=store.openDb();
|
||||
const pending=db.prepare('SELECT COUNT(*) n FROM document_file_cleanup').get().n;
|
||||
const integrity=db.prepare('PRAGMA integrity_check').get().integrity_check;
|
||||
db.close();console.log(JSON.stringify({exists:!!record,pending,integrity}));
|
||||
`;
|
||||
const child=spawn(process.execPath,['--input-type=module','-e',code],{env:{...process.env,IHALEIST_USER_DATA_DIR:dir},stdio:['ignore','pipe','pipe']});
|
||||
let output='',errors='';child.stdout.on('data',chunk=>output+=chunk);child.stderr.on('data',chunk=>errors+=chunk);
|
||||
const exit=await new Promise((resolve,reject)=>{child.once('error',reject);child.once('exit',resolve);});
|
||||
assert.equal(exit,0,errors);
|
||||
const result=JSON.parse(output);
|
||||
assert.equal(result.integrity,'ok');assert.equal(result.pending,0);
|
||||
assert.equal(result.exists,phase==='before-commit');
|
||||
assert.equal(existsSync(checkpoint.path),phase==='before-commit');
|
||||
if(phase==='before-commit')assert.equal(readFileSync(checkpoint.path,'utf8'),'Kesinti sırasında korunacak gerçek belge baytları.');
|
||||
} finally {rmSync(dir,{recursive:true,force:true});}
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user