| // Copyright 2025 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| const dbName = 'sweeper'; |
| const storeName = 'items'; |
| const indexName = 'version'; |
| |
| let db; |
| |
| function delay(ms) { |
| return new Promise(resolve => setTimeout(resolve, ms)); |
| } |
| |
| function createDatabaseCallback(newDb) { |
| db = newDb; |
| const store = db.createObjectStore(storeName, {keyPath: 'id'}); |
| store.createIndex(indexName, indexName, {unique: false}); |
| } |
| |
| const putItems = (versionNum, numItems) => { |
| return new Promise((resolve, reject) => { |
| const tx = db.transaction(storeName, 'readwrite'); |
| const objectStore = tx.objectStore(storeName); |
| for (let i = 0; i < numItems; i++) { |
| objectStore.put({id: i, version: versionNum}); |
| } |
| tx.oncomplete = () => { |
| console.log(`Put ${numItems} items at version ${versionNum}`); |
| resolve(); |
| }; |
| tx.onerror = reject; |
| }); |
| }; |
| |
| const getIndexCount = async (versionNum = undefined) => { |
| return new Promise((resolve, reject) => { |
| const transaction = db.transaction(storeName, 'readonly'); |
| let request = |
| transaction.objectStore(storeName).index(indexName).count(versionNum); |
| |
| request.onsuccess = event => { |
| const count = event.target.result; |
| console.log(`Index count for version ${versionNum}: ${count}`); |
| resolve(count); // resolve the promise with the int |
| }; |
| request.onerror = reject; |
| transaction.onerror = reject; |
| }); |
| }; |
| |
| // Updates the items individually, but aborts the transaction. |
| const updateAndAbortTransaction = (currentVersion, numItems, sweeperDelay) => { |
| return new Promise((resolve, reject) => { |
| const nextVersion = currentVersion + 1; |
| const updateTxn = db.transaction(storeName, 'readwrite'); |
| const objectStore = updateTxn.objectStore(storeName); |
| updateTxn.onabort = resolve; |
| updateTxn.onerror = reject; |
| const txnStartTime = Date.now(); |
| for (let i = 0; i < numItems; i++) { |
| const request = objectStore.put({id: i, version: nextVersion}); |
| if (i == numItems - 1) { |
| request.onsuccess = () => { |
| // Wait for the sweeper to run before aborting. |
| while (Date.now() - txnStartTime < sweeperDelay) { |
| } |
| updateTxn.abort(); |
| } |
| } |
| } |
| }); |
| }; |
| |
| // Regression test for crbug.com/413540372. |
| // The browser test creates an object store with indexes and generates |
| // tombstones through update operations and triggers the sweeper through a read |
| // call. While the tombstone sweeper is running, another transaction is started |
| // to update the indexes and then aborted. The sweeper, if still running along |
| // with this update call, deletes the tombstones generated by this update, |
| // but since the update transaction gets aborted, the index entries are lost. |
| async function runRollbackTest(numEntries, sweeperDelay) { |
| return new Promise(async (resolve, reject) => { |
| try { |
| await promiseDeleteThenOpenDb(dbName, createDatabaseCallback); |
| let currentVersion = 0; |
| await putItems(currentVersion, numEntries); |
| ++currentVersion; |
| await putItems(currentVersion, numEntries); |
| await getIndexCount(); |
| await delay(sweeperDelay); |
| await updateAndAbortTransaction(currentVersion, numEntries, sweeperDelay); |
| await delay(2 * sweeperDelay); |
| let count = await getIndexCount(currentVersion); |
| if (count !== numEntries) { |
| reject(new Error(`Expected ${numEntries} entries, got ${count}`)); |
| } |
| resolve(); |
| } catch (error) { |
| console.error('Error during rollback test:', error); |
| reject(error); |
| } |
| }); |
| } |
| |
| // Regression test for crbug.com/413540372. |
| // The browser test creates an object store with indexes and generates |
| // tombstones through update operations and triggers the sweeper through a read |
| // call. The tombstone sweeper completes one run and waits for any incoming |
| // calls. While the sweeper is paused, an update to the database is called with |
| // the same version. In the regression scenario, the sweeper still runs with an |
| // iterator based on an outdated snapshot of the database, it reads the stale |
| // index data and incorrectly deletes proper values as tombstones. |
| async function runInterleavedTest( |
| numEntries, delayBeforeInterleavedUpdates, delayToFinishSweeper) { |
| return new Promise(async (resolve, reject) => { |
| try { |
| await promiseDeleteThenOpenDb(dbName, createDatabaseCallback); |
| let currentVersion = 0; |
| await putItems(currentVersion, numEntries); |
| ++currentVersion; |
| await putItems(currentVersion, numEntries); |
| await getIndexCount(); |
| |
| // Allow time for the sweeper to start. |
| await delay(delayBeforeInterleavedUpdates); |
| |
| // Call put() with the same version on 100 items. |
| await putItems(currentVersion, 100); |
| |
| // Wait for the sweeper to finish. |
| await delay(delayToFinishSweeper); |
| |
| let count = await getIndexCount(currentVersion); |
| if (count !== numEntries) { |
| reject(new Error(`Expected ${numEntries} entries, got ${count}`)); |
| } |
| resolve(); |
| } catch (error) { |
| console.error('Error during interleaved operations test:', error); |
| reject(error); |
| } |
| }); |
| } |