blob: 124068093ece9fa2834ab4b8f1ecdaec75475bbf [file] [log] [blame]
// 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);
}
});
}