Source code

Revision control

Copy as Markdown

Other Tools

Test Info:

/* Any copyright is dedicated to the Public Domain.
/* eslint-disable mozilla/no-arbitrary-setTimeout */
const { setTimeout } = ChromeUtils.importESModule(
"resource://gre/modules/Timer.sys.mjs"
);
function getProfileFile(name) {
let file = do_get_profile();
file.append(name);
return file;
}
function promiseAsyncDatabase(name, openOptions = 0) {
return new Promise((resolve, reject) => {
let file = getProfileFile(name);
const connOptions = Ci.mozIStorageService.CONNECTION_DEFAULT;
Services.storage.openAsyncDatabase(
file,
openOptions,
connOptions,
(status, connection) => {
if (!Components.isSuccessCode(status)) {
reject(new Error(`Failed to open database: ${status}`));
} else {
connection.QueryInterface(Ci.mozIStorageAsyncConnection);
resolve(connection);
}
}
);
});
}
function promiseClone(db, readOnly = false) {
return new Promise((resolve, reject) => {
db.asyncClone(readOnly, (status, clone) => {
if (!Components.isSuccessCode(status)) {
reject(new Error(`Failed to clone connection: ${status}`));
} else {
clone.QueryInterface(Ci.mozIStorageAsyncConnection);
resolve(clone);
}
});
});
}
function promiseClose(db) {
return new Promise((resolve, reject) => {
db.asyncClose(status => {
if (!Components.isSuccessCode(status)) {
reject(new Error(`Failed to close connection: ${status}`));
} else {
resolve();
}
});
});
}
function promiseExecuteStatement(statement) {
return new Promise((resolve, reject) => {
let rows = [];
statement.executeAsync({
handleResult(resultSet) {
let row = null;
do {
row = resultSet.getNextRow();
if (row) {
rows.push(row);
}
} while (row);
},
handleError(error) {
reject(new Error(`Failed to execute statement: ${error.message}`));
},
handleCompletion(reason) {
if (reason == Ci.mozIStorageStatementCallback.REASON_FINISHED) {
resolve(rows);
} else {
reject(new Error("Statement failed to execute or was cancelled"));
}
},
});
});
}
add_task(async function test_retry_on_busy() {
info("Open first writer in WAL mode and set up schema");
let db1 = await promiseAsyncDatabase("retry-on-busy.sqlite");
let walStmt = db1.createAsyncStatement(`PRAGMA journal_mode = WAL`);
await promiseExecuteStatement(walStmt);
let createAStmt = db1.createAsyncStatement(`CREATE TABLE a(
b INTEGER PRIMARY KEY
)`);
await promiseExecuteStatement(createAStmt);
let createPrevAStmt = db1.createAsyncStatement(`CREATE TEMP TABLE prevA(
curB INTEGER PRIMARY KEY,
prevB INTEGER NOT NULL
)`);
await promiseExecuteStatement(createPrevAStmt);
let createATriggerStmt = db1.createAsyncStatement(`
CREATE TEMP TRIGGER a_afterinsert_trigger
AFTER UPDATE ON a FOR EACH ROW
BEGIN
REPLACE INTO prevA(curB, prevB) VALUES(NEW.b, OLD.b);
END`);
await promiseExecuteStatement(createATriggerStmt);
info("Open second writer");
let db2 = await promiseClone(db1);
info("Attach second writer to new database");
let attachStmt = db2.createAsyncStatement(`ATTACH :path AS newDB`);
attachStmt.bindByName(
"path",
getProfileFile("retry-on-busy-attach.sqlite").path
);
await promiseExecuteStatement(attachStmt);
info("Create triggers on second writer");
let createCStmt = db2.createAsyncStatement(`CREATE TABLE newDB.c(
d INTEGER PRIMARY KEY
)`);
await promiseExecuteStatement(createCStmt);
let createCTriggerStmt = db2.createAsyncStatement(`
CREATE TEMP TRIGGER c_afterdelete_trigger
AFTER DELETE ON c FOR EACH ROW
BEGIN
INSERT INTO a(b) VALUES(OLD.d);
END`);
await promiseExecuteStatement(createCTriggerStmt);
info("Begin transaction on second writer");
let begin2Stmt = db2.createAsyncStatement("BEGIN IMMEDIATE");
await promiseExecuteStatement(begin2Stmt);
info(
"Begin transaction on first writer; should busy-wait until second writer is done"
);
let begin1Stmt = db1.createAsyncStatement("BEGIN IMMEDIATE");
let promise1Began = promiseExecuteStatement(begin1Stmt);
let update1Stmt = db1.createAsyncStatement(`UPDATE a SET b = 3 WHERE b = 1`);
let promise1Updated = promiseExecuteStatement(update1Stmt);
info("Wait 5 seconds");
await new Promise(resolve => setTimeout(resolve, 5000));
info("Commit transaction on second writer");
let insertIntoA2Stmt = db2.createAsyncStatement(`INSERT INTO a(b) VALUES(1)`);
await promiseExecuteStatement(insertIntoA2Stmt);
let insertIntoC2Stmt = db2.createAsyncStatement(`INSERT INTO c(d) VALUES(2)`);
await promiseExecuteStatement(insertIntoC2Stmt);
let deleteFromC2Stmt = db2.createAsyncStatement(`DELETE FROM c`);
await promiseExecuteStatement(deleteFromC2Stmt);
let commit2Stmt = db2.createAsyncStatement("COMMIT");
await promiseExecuteStatement(commit2Stmt);
info("Await and commit transaction on first writer");
await promise1Began;
await promise1Updated;
let commit1Stmt = db1.createAsyncStatement("COMMIT");
await promiseExecuteStatement(commit1Stmt);
info("Verify our writes succeeded");
let select1Stmt = db2.createAsyncStatement("SELECT b FROM a");
let rows = await promiseExecuteStatement(select1Stmt);
deepEqual(
rows.map(row => row.getResultByName("b")),
[2, 3]
);
info("Clean up");
for (let stmt of [
walStmt,
createAStmt,
createPrevAStmt,
createATriggerStmt,
attachStmt,
createCStmt,
createCTriggerStmt,
begin2Stmt,
begin1Stmt,
insertIntoA2Stmt,
insertIntoC2Stmt,
deleteFromC2Stmt,
commit2Stmt,
update1Stmt,
commit1Stmt,
select1Stmt,
]) {
stmt.finalize();
}
await promiseClose(db1);
await promiseClose(db2);
});