test_retry_on_busy.js (5987B)
1 /* Any copyright is dedicated to the Public Domain. 2 http://creativecommons.org/publicdomain/zero/1.0/ */ 3 4 /* eslint-disable mozilla/no-arbitrary-setTimeout */ 5 6 const { setTimeout } = ChromeUtils.importESModule( 7 "resource://gre/modules/Timer.sys.mjs" 8 ); 9 10 function getProfileFile(name) { 11 let file = do_get_profile(); 12 file.append(name); 13 return file; 14 } 15 16 function promiseAsyncDatabase(name, openOptions = 0) { 17 return new Promise((resolve, reject) => { 18 let file = getProfileFile(name); 19 const connOptions = Ci.mozIStorageService.CONNECTION_DEFAULT; 20 Services.storage.openAsyncDatabase( 21 file, 22 openOptions, 23 connOptions, 24 (status, connection) => { 25 if (!Components.isSuccessCode(status)) { 26 reject(new Error(`Failed to open database: ${status}`)); 27 } else { 28 connection.QueryInterface(Ci.mozIStorageAsyncConnection); 29 resolve(connection); 30 } 31 } 32 ); 33 }); 34 } 35 36 function promiseClone(db, readOnly = false) { 37 return new Promise((resolve, reject) => { 38 db.asyncClone(readOnly, (status, clone) => { 39 if (!Components.isSuccessCode(status)) { 40 reject(new Error(`Failed to clone connection: ${status}`)); 41 } else { 42 clone.QueryInterface(Ci.mozIStorageAsyncConnection); 43 resolve(clone); 44 } 45 }); 46 }); 47 } 48 49 function promiseClose(db) { 50 return new Promise((resolve, reject) => { 51 db.asyncClose(status => { 52 if (!Components.isSuccessCode(status)) { 53 reject(new Error(`Failed to close connection: ${status}`)); 54 } else { 55 resolve(); 56 } 57 }); 58 }); 59 } 60 61 function promiseExecuteStatement(statement) { 62 return new Promise((resolve, reject) => { 63 let rows = []; 64 statement.executeAsync({ 65 handleResult(resultSet) { 66 let row = null; 67 do { 68 row = resultSet.getNextRow(); 69 if (row) { 70 rows.push(row); 71 } 72 } while (row); 73 }, 74 handleError(error) { 75 reject(new Error(`Failed to execute statement: ${error.message}`)); 76 }, 77 handleCompletion(reason) { 78 if (reason == Ci.mozIStorageStatementCallback.REASON_FINISHED) { 79 resolve(rows); 80 } else { 81 reject(new Error("Statement failed to execute or was cancelled")); 82 } 83 }, 84 }); 85 }); 86 } 87 88 add_task(async function test_retry_on_busy() { 89 info("Open first writer in WAL mode and set up schema"); 90 let db1 = await promiseAsyncDatabase("retry-on-busy.sqlite"); 91 92 let walStmt = db1.createAsyncStatement(`PRAGMA journal_mode = WAL`); 93 await promiseExecuteStatement(walStmt); 94 let createAStmt = db1.createAsyncStatement(`CREATE TABLE a( 95 b INTEGER PRIMARY KEY 96 )`); 97 await promiseExecuteStatement(createAStmt); 98 let createPrevAStmt = db1.createAsyncStatement(`CREATE TEMP TABLE prevA( 99 curB INTEGER PRIMARY KEY, 100 prevB INTEGER NOT NULL 101 )`); 102 await promiseExecuteStatement(createPrevAStmt); 103 let createATriggerStmt = db1.createAsyncStatement(` 104 CREATE TEMP TRIGGER a_afterinsert_trigger 105 AFTER UPDATE ON a FOR EACH ROW 106 BEGIN 107 REPLACE INTO prevA(curB, prevB) VALUES(NEW.b, OLD.b); 108 END`); 109 await promiseExecuteStatement(createATriggerStmt); 110 111 info("Open second writer"); 112 let db2 = await promiseClone(db1); 113 114 info("Attach second writer to new database"); 115 let attachStmt = db2.createAsyncStatement(`ATTACH :path AS newDB`); 116 attachStmt.bindByName( 117 "path", 118 getProfileFile("retry-on-busy-attach.sqlite").path 119 ); 120 await promiseExecuteStatement(attachStmt); 121 122 info("Create triggers on second writer"); 123 let createCStmt = db2.createAsyncStatement(`CREATE TABLE newDB.c( 124 d INTEGER PRIMARY KEY 125 )`); 126 await promiseExecuteStatement(createCStmt); 127 let createCTriggerStmt = db2.createAsyncStatement(` 128 CREATE TEMP TRIGGER c_afterdelete_trigger 129 AFTER DELETE ON c FOR EACH ROW 130 BEGIN 131 INSERT INTO a(b) VALUES(OLD.d); 132 END`); 133 await promiseExecuteStatement(createCTriggerStmt); 134 135 info("Begin transaction on second writer"); 136 let begin2Stmt = db2.createAsyncStatement("BEGIN IMMEDIATE"); 137 await promiseExecuteStatement(begin2Stmt); 138 139 info( 140 "Begin transaction on first writer; should busy-wait until second writer is done" 141 ); 142 let begin1Stmt = db1.createAsyncStatement("BEGIN IMMEDIATE"); 143 let promise1Began = promiseExecuteStatement(begin1Stmt); 144 let update1Stmt = db1.createAsyncStatement(`UPDATE a SET b = 3 WHERE b = 1`); 145 let promise1Updated = promiseExecuteStatement(update1Stmt); 146 147 info("Wait 5 seconds"); 148 await new Promise(resolve => setTimeout(resolve, 5000)); 149 150 info("Commit transaction on second writer"); 151 let insertIntoA2Stmt = db2.createAsyncStatement(`INSERT INTO a(b) VALUES(1)`); 152 await promiseExecuteStatement(insertIntoA2Stmt); 153 let insertIntoC2Stmt = db2.createAsyncStatement(`INSERT INTO c(d) VALUES(2)`); 154 await promiseExecuteStatement(insertIntoC2Stmt); 155 let deleteFromC2Stmt = db2.createAsyncStatement(`DELETE FROM c`); 156 await promiseExecuteStatement(deleteFromC2Stmt); 157 let commit2Stmt = db2.createAsyncStatement("COMMIT"); 158 await promiseExecuteStatement(commit2Stmt); 159 160 info("Await and commit transaction on first writer"); 161 await promise1Began; 162 await promise1Updated; 163 164 let commit1Stmt = db1.createAsyncStatement("COMMIT"); 165 await promiseExecuteStatement(commit1Stmt); 166 167 info("Verify our writes succeeded"); 168 let select1Stmt = db2.createAsyncStatement("SELECT b FROM a"); 169 let rows = await promiseExecuteStatement(select1Stmt); 170 deepEqual( 171 rows.map(row => row.getResultByName("b")), 172 [2, 3] 173 ); 174 175 info("Clean up"); 176 for (let stmt of [ 177 walStmt, 178 createAStmt, 179 createPrevAStmt, 180 createATriggerStmt, 181 attachStmt, 182 createCStmt, 183 createCTriggerStmt, 184 begin2Stmt, 185 begin1Stmt, 186 insertIntoA2Stmt, 187 insertIntoC2Stmt, 188 deleteFromC2Stmt, 189 190 commit2Stmt, 191 update1Stmt, 192 commit1Stmt, 193 select1Stmt, 194 ]) { 195 stmt.finalize(); 196 } 197 await promiseClose(db1); 198 await promiseClose(db2); 199 });