diff --git a/moli-benchmark/wpt-cross-current/passed-cases.txt b/moli-benchmark/wpt-cross-current/passed-cases.txt index 55ab3dfb6..0319a505d 100644 --- a/moli-benchmark/wpt-cross-current/passed-cases.txt +++ b/moli-benchmark/wpt-cross-current/passed-cases.txt @@ -130,8 +130,14 @@ IndexedDB/transaction-abort-request-error.any.js?moli-wpt-any=dedicatedworker IndexedDB/transaction-abort-request-error.any.js?moli-wpt-any=window IndexedDB/transaction-create_in_versionchange.any.js?moli-wpt-any=dedicatedworker IndexedDB/transaction-create_in_versionchange.any.js?moli-wpt-any=window +IndexedDB/transaction-lifetime-empty.any.js?moli-wpt-any=dedicatedworker +IndexedDB/transaction-lifetime-empty.any.js?moli-wpt-any=window IndexedDB/transaction-lifetime.any.js?moli-wpt-any=dedicatedworker IndexedDB/transaction-lifetime.any.js?moli-wpt-any=window +IndexedDB/transaction-scheduling-mixed-scopes.any.js?moli-wpt-any=dedicatedworker +IndexedDB/transaction-scheduling-mixed-scopes.any.js?moli-wpt-any=window +IndexedDB/transaction-scheduling-ro-waits-for-rw.any.js?moli-wpt-any=dedicatedworker +IndexedDB/transaction-scheduling-ro-waits-for-rw.any.js?moli-wpt-any=window IndexedDB/transaction_bubble-and-capture.any.js?moli-wpt-any=dedicatedworker IndexedDB/transaction_bubble-and-capture.any.js?moli-wpt-any=window IndexedDB/upgrade-transaction-lifecycle-backend-aborted.any.js?moli-wpt-any=dedicatedworker diff --git a/moli-core/tests/web_apis/fixtures/indexeddb-transaction-scheduling.js b/moli-core/tests/web_apis/fixtures/indexeddb-transaction-scheduling.js new file mode 100644 index 000000000..dd78ca9a5 --- /dev/null +++ b/moli-core/tests/web_apis/fixtures/indexeddb-transaction-scheduling.js @@ -0,0 +1,272 @@ +globalThis.schedulingChecks = []; + +async function schedulingProbe(prefix = 'transaction-scheduling-' + Math.random()) { + const checks = globalThis.schedulingChecks; + const check = (name, pass, detail) => checks.push({name, pass: !!pass, detail}); + const request = req => new Promise(resolve => { + req.onsuccess = () => resolve({value: req.result}); + req.onerror = event => { event.preventDefault(); resolve({error: req.error.name}); }; + }); + const done = (tx, log, label) => new Promise(resolve => { + tx.addEventListener('complete', () => { if (log) log.push(label); resolve(true); }); + tx.addEventListener('abort', () => { if (log) log.push(label + ' abort'); resolve(false); }); + }); + const open = name => { + const req = indexedDB.open(name, 1); + req.onupgradeneeded = () => { + for (const name of ['a', 'b', 'c']) req.result.createObjectStore(name).put('old', 'key'); + }; + return request(req).then(result => { + if (result.error) throw new Error(result.error); + return result.value; + }); + }; + const run = async (label, body) => { + const name = prefix + '-' + label, connections = []; + try { + const db = await open(name); + connections.push(db); + const other = await open(name); + connections.push(other); + await body(db, other, name); + } catch (error) { + check(label + ' unexpected exception', false, String(error)); + } finally { + for (const db of connections) db.close(); + } + }; + const read = async (db, stores) => { + const tx = db.transaction(stores), completion = done(tx); + const values = await Promise.all(stores.map(name => request(tx.objectStore(name).get('key')))); + await completion; + return values.map(result => result.value); + }; + + for (const crossConnection of [false, true]) { + for (const firstMode of ['readonly', 'readwrite']) { + for (const secondMode of ['readonly', 'readwrite']) { + for (const [firstScope, secondScope] of [ + [['a'], ['a']], [['a', 'b'], ['b', 'c']], [['a'], ['b']] + ]) { + const label = [crossConnection, firstMode, secondMode, firstScope.join(''), secondScope.join('')].join('-'); + await run(label, async (db, other) => { + const log = []; + const first = db.transaction(firstScope, firstMode), firstDone = done(first, log, 'first'); + const second = (crossConnection ? other : db).transaction(secondScope, secondMode); + const secondDone = done(second, log, 'second'); + if (firstMode === 'readwrite') { + for (const name of firstScope) first.objectStore(name).put('first', 'key'); + } + // Keep the older transaction active through several request events. + // An overlapping younger writer must not execute during these events. + const firstValues = []; + const pump = left => { + const req = first.objectStore(firstScope[0]).get('key'); + req.onsuccess = () => { + firstValues.push(req.result); + if (left) pump(left - 1); + }; + }; + pump(5); + const observations = secondScope.map(name => { + const req = second.objectStore(name).get('key'); + return request(req).then(result => { log.push('read ' + name); return result; }); + }); + if (secondMode === 'readwrite') { + for (const name of secondScope) second.objectStore(name).put('second', 'key'); + } + const results = await Promise.all(observations); + check(label + ' completes', await firstDone && await secondDone); + const overlap = firstScope.some(name => secondScope.includes(name)); + if (overlap && (firstMode === 'readwrite' || secondMode === 'readwrite')) { + check(label + ' waits for predecessor', log.indexOf('first') < log.indexOf('read ' + secondScope[0]), log); + } + check(label + ' stable older snapshot', firstValues.length === 6 && + firstValues.every(value => value === (firstMode === 'readwrite' ? 'first' : 'old')), firstValues); + for (const [index, name] of secondScope.entries()) { + const expected = firstMode === 'readwrite' && firstScope.includes(name) ? 'first' : 'old'; + check(label + ' observes ' + name, !results[index].error && results[index].value === expected, results[index]); + } + const final = await read(db, ['a', 'b', 'c']); + const expected = ['a', 'b', 'c'].map(name => + secondMode === 'readwrite' && secondScope.includes(name) ? 'second' : + firstMode === 'readwrite' && firstScope.includes(name) ? 'first' : 'old'); + check(label + ' preserves every committed store', JSON.stringify(final) === JSON.stringify(expected), final); + }); + } + } + } + } + + await run('pending-writer-fairness', async db => { + const log = []; + const first = db.transaction('a'), firstDone = done(first, log, 'first'); + const writer = db.transaction(['a', 'b'], 'readwrite'), writerDone = done(writer, log, 'writer'); + const last = db.transaction('b'), lastDone = done(last, log, 'last'); + const pump = left => { + first.objectStore('a').get('key').onsuccess = () => { if (left) pump(left - 1); }; + }; + pump(8); + writer.objectStore('b').put('writer', 'key'); + const result = await request(last.objectStore('b').get('key')); + check('pending writer blocks a later reader of its other store', result.value === 'writer', result); + check('pending fairness completes', await firstDone && await writerDone && await lastDone); + check('pending fairness order', log.join(',') === 'first,writer,last', log); + }); + + for (const pending of [false, true]) { + await run('abort-' + pending, async db => { + const blocker = pending ? db.transaction('a') : null; + const blockerDone = blocker ? done(blocker) : Promise.resolve(true); + const writer = db.transaction(['a', 'b'], 'readwrite'), writerDone = done(writer); + const write = request(writer.objectStore('b').put('aborted', 'key')); + const later = db.transaction('b'), laterDone = done(later); + const value = request(later.objectStore('b').get('key')); + // Pending work has no backend handle; both paths must release admission. + writer.abort(); + check('abort ' + pending + ' rolls back and unblocks reader', (await value).value === 'old'); + check('abort ' + pending + ' dispatches request error', (await write).error === 'AbortError'); + check('abort ' + pending + ' finishes', !(await writerDone) && await blockerDone && await laterDone); + }); + } + + await run('immutable-start-scope', async db => { + const writer = db.transaction('a', 'readwrite'), writerDone = done(writer); + const reader = db.transaction('a'), readerDone = done(reader); + const value = request(reader.objectStore('a').get('key')); + writer.objectStore('a').put('new', 'key'); + for (const name of ['db', 'mode', 'objectStoreNames']) { + Object.defineProperty(reader, name, {get() { throw new Error('scheduler read ' + name); }}); + } + check('deferred start uses immutable native scope', (await value).value === 'new'); + check('deferred readonly completes', await writerDone && await readerDone); + }); + + await run('close-with-queued-reader', async (db, other) => { + const writer = db.transaction('a', 'readwrite'), writerDone = done(writer); + const reader = other.transaction('a'), readerDone = done(reader); + const value = request(reader.objectStore('a').get('key')); + writer.objectStore('a').put('new', 'key'); + other.close(); + let error; + try { other.transaction('a'); } catch (caught) { error = caught.name; } + check('close rejects new transactions', error === 'InvalidStateError', error); + check('close lets accepted reader observe earlier write', (await value).value === 'new'); + check('close drains accepted transactions', await writerDone && await readerDone); + }); + + await run('empty-readers', async db => { + const log = [], empty = []; + const writer = db.transaction('a', 'readwrite'), writerDone = done(writer, log, 'writer'); + const store = writer.objectStore('a'); + store.put('one', 'key').onsuccess = () => { + log.push('one'); + empty.push(done(db.transaction('a'), log, 'reader1'), done(db.transaction('a'), log, 'reader2')); + store.put('two', 'key').onsuccess = () => log.push('two'); + }; + await writerDone; + check('empty readers complete', (await Promise.all(empty)).every(Boolean)); + check('empty readers wait for writer', log.join(',') === 'one,two,writer,reader1,reader2', log); + }); + + for (const [mode, laterMode, action] of [ + ['readwrite', 'readonly', 'commit'], + ['readwrite', 'readwrite', 'abort'], + ['readonly', 'readwrite', 'commit'], + ['readwrite', 'readonly', 'terminate'] + ]) { + const label = [mode, laterMode, action].join('-'); + await run('worker-' + label, async (db, other, name) => { + const source = '(' + schedulingWorker.toString() + ')()'; + const url = URL.createObjectURL(new Blob([source], {type: 'text/javascript'})); + const worker = new Worker(url), messages = [], waiters = []; + worker.onmessage = event => { + if (waiters.length) waiters.shift()(event.data); else messages.push(event.data); + }; + const next = () => messages.length ? Promise.resolve(messages.shift()) : new Promise(resolve => waiters.push(resolve)); + try { + worker.postMessage({name, mode}); + check(label + ' worker holds transaction', (await next()).state === 'holding'); + const tx = db.transaction('a', laterMode), completion = done(tx); + const req = tx.objectStore('a').get('key'), value = request(req); + if (laterMode === 'readwrite') tx.objectStore('a').put('parent', 'key'); + worker.postMessage({checkpoint: true}); + check(label + ' worker checkpoint', (await next()).state === 'checkpoint'); + check(label + ' waits across agents', req.readyState === 'pending', req.readyState); + if (action === 'terminate') worker.terminate(); + else worker.postMessage({release: action}); + const expected = mode === 'readwrite' && action === 'commit' ? 'worker' : 'old'; + check(label + ' observes committed snapshot', (await value).value === expected); + check(label + ' parent completes', await completion); + if (action !== 'terminate') { + check(label + ' worker finishes', (await next()).state === (action === 'abort' ? 'aborted' : 'complete')); + } + check(label + ' final value', (await read(other, ['a']))[0] === (laterMode === 'readwrite' ? 'parent' : expected)); + } finally { + worker.terminate(); + URL.revokeObjectURL(url); + } + }); + } + await run('terminate-pending-worker', async (db, other, name) => { + const url = URL.createObjectURL(new Blob(['(' + schedulingWorker.toString() + ')()'], {type: 'text/javascript'})); + const worker = new Worker(url); + let keepAlive = true; + const writer = db.transaction('a', 'readwrite'), writerDone = done(writer); + writer.objectStore('a').put('parent', 'key'); + const pump = () => { + writer.objectStore('a').get('key').onsuccess = () => { if (keepAlive) pump(); }; + }; + pump(); + try { + const queued = new Promise(resolve => { worker.onmessage = event => resolve(event.data); }); + worker.postMessage({name, mode: 'readwrite', notifyQueued: true}); + check('pending worker accepts transaction', (await queued).state === 'queued'); + const reader = other.transaction('a'), readerDone = done(reader); + const value = request(reader.objectStore('a').get('key')); + worker.terminate(); + keepAlive = false; + check('terminating pending worker unblocks its successor', (await value).value === 'parent'); + check('pending worker teardown preserves live transactions', await writerDone && await readerDone); + } finally { + keepAlive = false; + worker.terminate(); + URL.revokeObjectURL(url); + } + }); + return {state: checks.every(entry => entry.pass) ? 'pass' : 'fail', checks}; +} + +// Executed in its own agent. Request chaining keeps the transaction live until +// an explicit handshake releases it; no timeout is used to infer completion. +function schedulingWorker() { + let release, checkpoint = false; + onmessage = event => { + const message = event.data; + if (message.release) { release = message.release; return; } + if (message.checkpoint) { checkpoint = true; return; } + const req = indexedDB.open(message.name, 1); + req.onerror = () => postMessage({state: 'error', error: req.error.name}); + req.onsuccess = () => { + const db = req.result; + let tx; + try { tx = db.transaction('a', message.mode); } + catch (error) { db.close(); postMessage({state: 'error', error: error.name}); return; } + const store = tx.objectStore('a'); + if (message.notifyQueued) postMessage({state: 'queued'}); + tx.oncomplete = () => { db.close(); postMessage({state: 'complete'}); }; + tx.onabort = () => { db.close(); postMessage({state: 'aborted'}); }; + const first = message.mode === 'readwrite' ? store.put('worker', 'key') : store.get('key'); + const pump = () => { + store.get('key').onsuccess = () => { + if (release === 'abort') tx.abort(); + else if (!release) { + pump(); + if (checkpoint) { checkpoint = false; postMessage({state: 'checkpoint'}); } + } + }; + }; + first.onsuccess = () => { pump(); postMessage({state: 'holding'}); }; + }; + }; +} diff --git a/moli-core/tests/web_apis/indexed_db_transaction.rs b/moli-core/tests/web_apis/indexed_db_transaction.rs index 34294650d..f0741fdfb 100644 --- a/moli-core/tests/web_apis/indexed_db_transaction.rs +++ b/moli-core/tests/web_apis/indexed_db_transaction.rs @@ -1,6 +1,52 @@ use super::event_dispatch::run_probe; use super::*; +#[tokio::test(flavor = "multi_thread")] +async fn indexed_db_schedules_overlapping_transactions_across_connections_and_agents() -> Result<()> +{ + let server = FixtureServer::spawn().await?; + let browser = Browser::new(AppConfig::default())?; + let fixture = include_str!("fixtures/indexeddb-transaction-scheduling.js"); + for target in ["window", "child", "worker"] { + let source = format!( + "{fixture}\nschedulingProbe('scheduling-{target}').then(finish, error => finish({{state: 'error', error: String(error), checks: schedulingChecks}}));" + ); + let result = run_probe(&browser, &server, target, &source).await?; + assert_eq!(result["state"], "pass", "{target}: {result}"); + assert_eq!( + result["checks"].as_array().unwrap().len(), + 162, + "{target}: {result}" + ); + } + server.shutdown().await; + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn indexed_db_queries_snapshot_arguments_and_delete_key_ranges() -> Result<()> { + let server = FixtureServer::spawn().await?; + let browser = Browser::new(AppConfig::default())?; + let fixture = include_str!("fixtures/indexeddb-query-snapshots.js"); + for target in ["window", "child", "worker"] { + let source = format!( + "{fixture}\nquerySnapshotProbe('query-snapshots-{target}').then(finish, error => finish({{state: 'error', error: String(error), checks: queryChecks}}));" + ); + let result = tokio::time::timeout( + Duration::from_secs(30), + run_probe(&browser, &server, target, &source), + ).await??; + assert_eq!(result["state"], "pass", "{target}: {result}"); + assert_eq!( + result["checks"].as_array().unwrap().len(), + 548, + "{target}: {result}" + ); + } + server.shutdown().await; + Ok(()) +} + #[tokio::test(flavor = "multi_thread")] async fn indexed_db_keys_preserve_types_order_and_binary_snapshots() -> Result<()> { let server = FixtureServer::spawn().await?; @@ -106,31 +152,6 @@ async fn indexed_db_events_propagate_and_abort_only_active_transactions() -> Res Ok(()) } -#[tokio::test(flavor = "multi_thread")] -async fn indexed_db_queries_snapshot_arguments_and_delete_key_ranges() -> Result<()> { - let server = FixtureServer::spawn().await?; - let browser = Browser::new(AppConfig::default())?; - let fixture = include_str!("fixtures/indexeddb-query-snapshots.js"); - for target in ["window", "child", "worker"] { - let source = format!( - "{fixture}\nquerySnapshotProbe('query-snapshots-{target}').then(finish, error => finish({{state: 'error', error: String(error), checks: queryChecks}}));" - ); - let result = tokio::time::timeout( - Duration::from_secs(30), - run_probe(&browser, &server, target, &source), - ) - .await??; - assert_eq!(result["state"], "pass", "{target}: {result}"); - assert_eq!( - result["checks"].as_array().unwrap().len(), - 548, - "{target}: {result}" - ); - } - server.shutdown().await; - Ok(()) -} - #[tokio::test(flavor = "multi_thread")] async fn indexed_db_transaction_validates_arguments_and_upgrade_boundaries() -> Result<()> { let server = FixtureServer::spawn().await?; diff --git a/moli-indexeddb/src/lib.rs b/moli-indexeddb/src/lib.rs index c5c09b50c..dbeb612d8 100644 --- a/moli-indexeddb/src/lib.rs +++ b/moli-indexeddb/src/lib.rs @@ -16,6 +16,7 @@ mod state; #[cfg(test)] mod tests; mod transaction; +mod transaction_queue; mod types; mod usage; @@ -37,6 +38,7 @@ pub use options::{ validate_index_options, validate_object_store_options, }; pub use state::IndexedDbManager; +pub use transaction_queue::{TransactionRequestHandle, TransactionRequestLease}; pub use types::{ DatabaseHandle, DatabaseInfo, DatabaseNameAndVersion, IndexInfo, IndexOptions, IndexedDbExternalObject, IndexedDbFileSystemHandleBucket, IndexedDbFileSystemHandleKind, diff --git a/moli-indexeddb/src/manager.rs b/moli-indexeddb/src/manager.rs index b10b6242e..4fe38f441 100644 --- a/moli-indexeddb/src/manager.rs +++ b/moli-indexeddb/src/manager.rs @@ -17,7 +17,7 @@ use crate::{ transaction::{ ensure_writeable, next_generated_key, resolve_key, transaction_store, transaction_store_mut, }, - usage::{database_usage_bytes, origin_usage_bytes, sum_usage}, + usage::{database_usage_bytes, database_usage_bytes_for_stores, origin_usage_bytes, sum_usage}, validate_index_options, validate_object_store_options, }; @@ -29,6 +29,7 @@ impl IndexedDbManager { pub fn new_in_memory() -> Self { Self { + transaction_requests: Default::default(), connection_requests: Default::default(), connection_notifications: Default::default(), backend: IndexedDbPersistenceBackend::InMemory, @@ -45,6 +46,7 @@ impl IndexedDbManager { fs::create_dir_all(&storage_root) .map_err(|err| IndexedDbError::Io(format!("failed to create storage root: {err}")))?; Ok(Self { + transaction_requests: Default::default(), connection_requests: Default::default(), connection_notifications: Default::default(), backend: IndexedDbPersistenceBackend::JsonFiles { storage_root }, @@ -222,8 +224,14 @@ impl IndexedDbManager { } pub fn clear_origins_with_prefix(&mut self, origin_prefix: &str) -> Result<(), IndexedDbError> { - self.databases - .retain(|_, db| !db.origin.starts_with(origin_prefix)); + let handles: Vec<_> = self + .databases + .iter() + .filter_map(|(handle, db)| db.origin.starts_with(origin_prefix).then_some(*handle)) + .collect(); + for handle in handles { + self.force_close_database(handle)?; + } self.transactions .retain(|_, tx| !tx.origin.starts_with(origin_prefix)); self.origins @@ -295,6 +303,7 @@ impl IndexedDbManager { self.databases.remove(&handle).ok_or_else(|| { IndexedDbError::InvalidState(format!("database handle {:?} is invalid", handle)) })?; + self.transaction_requests.cancel_connection(handle); self.connection_notifications.unregister(handle); Ok(()) } @@ -393,6 +402,42 @@ impl IndexedDbManager { }) } + pub fn queue_transaction_start( + &self, + database: DatabaseHandle, + store_names: &[String], + mode: TransactionMode, + wake: crate::ConnectionRequestWake, + ) -> Result { + let db = self.database_state(database)?; + if db.closed || mode == TransactionMode::VersionChange { + return Err(IndexedDbError::InvalidState( + "regular transaction requires an open connection".to_owned(), + )); + } + let stores = store_names.iter().cloned().collect(); + self.ensure_store_names_exist(self.database_data(&db.origin, &db.name)?, &stores)?; + Ok(self.transaction_requests.enqueue( + (db.origin.clone(), db.name.clone()), + database, + stores, + mode, + wake, + )) + } + + pub fn start_queued_transaction( + &mut self, + request: &crate::TransactionRequestHandle, + ) -> Result { + if !request.belongs_to(&self.transaction_requests) || !request.is_ready() { + return Err(IndexedDbError::InvalidState( + "transaction is not ready to start".to_owned(), + )); + } + self.begin_transaction(request.database(), &request.store_names(), request.mode()) + } + pub fn begin_transaction( &mut self, database: DatabaseHandle, @@ -415,16 +460,15 @@ impl IndexedDbManager { let store_set = store_names.iter().cloned().collect::>(); self.ensure_store_names_exist(¤t, &store_set)?; - if mode == TransactionMode::ReadWrite - && self.transactions.values().any(|tx| { - tx.state == TransactionLifecycle::Active - && tx.mode == TransactionMode::ReadWrite - && tx.origin == db.origin - && tx.db_name == db.name - }) - { + if self.transactions.values().any(|tx| { + tx.state == TransactionLifecycle::Active + && tx.origin == db.origin + && tx.db_name == db.name + && (mode != TransactionMode::ReadOnly || tx.mode != TransactionMode::ReadOnly) + && !tx.stores.is_disjoint(&store_set) + }) { return Err(IndexedDbError::InvalidState( - "concurrent readwrite transactions are not supported in the MVP backend".to_owned(), + "an overlapping transaction has not finished".to_owned(), )); } @@ -770,11 +814,9 @@ impl IndexedDbManager { add_only: bool, quota: Option, ) -> Result { - let (origin, db_name, working_copy_usage, resolved_key, previous_store) = { + let (resolved_key, previous_store) = { let tx = self.active_transaction_mut(transaction)?; ensure_writeable(tx)?; - let db_name = tx.db_name.clone(); - let origin = tx.origin.clone(); let store = transaction_store_mut(tx, store_name)?; let previous_store = quota.map(|_| store.clone()); let previous_counter = store.auto_increment_counter; @@ -786,21 +828,17 @@ impl IndexedDbManager { )); } store.records.insert(resolved_key.clone(), value); - let working_copy_usage = database_usage_bytes(&db_name, &tx.working_copy); - ( - origin, - db_name, - working_copy_usage, - resolved_key, - previous_store, - ) + (resolved_key, previous_store) }; if let Some(quota) = quota { + let tx = self.active_transaction(transaction)?; let requested = quota .non_indexed_db_usage - .saturating_add(self.committed_origin_usage_except_database(&origin, &db_name)) - .saturating_add(working_copy_usage); + .saturating_add( + self.committed_origin_usage_except_database(&tx.origin, &tx.db_name), + ) + .saturating_add(self.transaction_commit_usage(tx)?); if requested > quota.quota { if let Some(previous_store) = previous_store { let tx = self.active_transaction_mut(transaction)?; @@ -847,22 +885,39 @@ impl IndexedDbManager { &mut self, transaction: TransactionHandle, ) -> Result<(), IndexedDbError> { - let (database, origin, db_name, working_copy) = { + let (database, origin, db_name, mode) = { let tx = self.active_transaction_mut(transaction)?; tx.state = TransactionLifecycle::Committed; - ( - tx.database, - tx.origin.clone(), - tx.db_name.clone(), - tx.working_copy.clone(), - ) + (tx.database, tx.origin.clone(), tx.db_name.clone(), tx.mode) + }; + let tx = self + .transactions + .remove(&transaction) + .expect("active transaction"); + let result = if mode == TransactionMode::ReadOnly { + // Readers never publish their snapshot, even when another store + // in the same database has changed since the reader started. + Ok(()) + } else { + let origin_state = self.origins.get_mut(&origin).ok_or_else(|| { + IndexedDbError::NotFound(format!("origin `{origin}` was not found during commit")) + })?; + if mode == TransactionMode::VersionChange { + origin_state.databases.insert(db_name, tx.working_copy); + } else { + let current = origin_state.databases.get_mut(&db_name).ok_or_else(|| { + IndexedDbError::NotFound(format!( + "database `{db_name}` was not found during commit" + )) + })?; + for (name, store) in tx.working_copy.stores { + if tx.stores.contains(&name) { + current.stores.insert(name, store); + } + } + } + self.persist_origin(&origin) }; - let origin_state = self.origins.get_mut(&origin).ok_or_else(|| { - IndexedDbError::NotFound(format!("origin `{origin}` was not found during commit")) - })?; - origin_state.databases.insert(db_name, working_copy); - self.transactions.remove(&transaction); - let result = self.persist_origin(&origin); self.finish_pending_database_close(database); result } @@ -872,19 +927,15 @@ impl IndexedDbManager { transaction: TransactionHandle, quota: IndexedDbQuotaCheck, ) -> Result<(), IndexedDbError> { - let (database, origin, db_name, working_copy_usage) = { - let tx = self.active_transaction_mut(transaction)?; - ( - tx.database, - tx.origin.clone(), - tx.db_name.clone(), - database_usage_bytes(&tx.db_name, &tx.working_copy), - ) - }; + let tx = self.active_transaction(transaction)?; + if tx.mode == TransactionMode::ReadOnly { + return self.commit_transaction(transaction); + } + let database = tx.database; let requested = quota .non_indexed_db_usage - .saturating_add(self.committed_origin_usage_except_database(&origin, &db_name)) - .saturating_add(working_copy_usage); + .saturating_add(self.committed_origin_usage_except_database(&tx.origin, &tx.db_name)) + .saturating_add(self.transaction_commit_usage(tx)?); if requested > quota.quota { self.transactions.remove(&transaction); self.finish_pending_database_close(database); @@ -896,6 +947,24 @@ impl IndexedDbManager { self.commit_transaction(transaction) } + fn transaction_commit_usage(&self, tx: &TransactionState) -> Result { + if tx.mode == TransactionMode::VersionChange { + return Ok(database_usage_bytes(&tx.db_name, &tx.working_copy)); + } + let current = self.database_data(&tx.origin, &tx.db_name)?; + Ok(database_usage_bytes_for_stores( + &tx.db_name, + current.stores.iter().map(|(name, store)| { + let store = if tx.stores.contains(name) { + &tx.working_copy.stores[name] + } else { + store + }; + (name, store) + }), + )) + } + pub fn abort_transaction( &mut self, transaction: TransactionHandle, @@ -974,6 +1043,20 @@ impl IndexedDbManager { }) } + fn active_transaction( + &self, + handle: TransactionHandle, + ) -> Result<&TransactionState, IndexedDbError> { + self.transactions + .get(&handle) + .filter(|tx| tx.state == TransactionLifecycle::Active) + .ok_or_else(|| { + IndexedDbError::TransactionInactive(format!( + "transaction handle {handle:?} is not active" + )) + }) + } + pub(crate) fn active_transaction_mut( &mut self, handle: TransactionHandle, diff --git a/moli-indexeddb/src/state.rs b/moli-indexeddb/src/state.rs index 33f95af3b..9889a52a2 100644 --- a/moli-indexeddb/src/state.rs +++ b/moli-indexeddb/src/state.rs @@ -63,6 +63,8 @@ pub(crate) enum TransactionLifecycle { pub struct IndexedDbManager { pub(crate) connection_notifications: std::sync::Arc, pub(crate) connection_requests: std::sync::Arc, + pub(crate) transaction_requests: + std::sync::Arc, pub(crate) backend: IndexedDbPersistenceBackend, pub(crate) origins: BTreeMap, pub(crate) databases: BTreeMap, diff --git a/moli-indexeddb/src/tests.rs b/moli-indexeddb/src/tests.rs index e634a859b..038089f57 100644 --- a/moli-indexeddb/src/tests.rs +++ b/moli-indexeddb/src/tests.rs @@ -16,6 +16,8 @@ struct TestDir { path: PathBuf, } +mod transaction_scheduling; + #[test] fn schema_validation_preserves_exception_order_and_does_not_create_invalid_metadata() { let mut manager = IndexedDbManager::new_in_memory(); diff --git a/moli-indexeddb/src/tests/transaction_scheduling.rs b/moli-indexeddb/src/tests/transaction_scheduling.rs new file mode 100644 index 000000000..1144f713e --- /dev/null +++ b/moli-indexeddb/src/tests/transaction_scheduling.rs @@ -0,0 +1,237 @@ +use super::*; +use std::sync::Arc; + +fn open_database(manager: &mut IndexedDbManager) -> DatabaseHandle { + let open = manager + .open(OpenOptions { + origin: "origin".into(), + name: "db".into(), + version: None, + }) + .unwrap(); + if let Some(upgrade) = open.upgrade_transaction { + for store in ["a", "b", "c"] { + manager + .create_object_store( + upgrade, + store, + ObjectStoreOptions { + auto_increment: true, + ..Default::default() + }, + ) + .unwrap(); + } + manager.commit_transaction(upgrade).unwrap(); + } + open.database +} + +fn queue( + manager: &IndexedDbManager, + database: DatabaseHandle, + stores: &[&str], + mode: TransactionMode, +) -> TransactionRequestLease { + manager + .queue_transaction_start( + database, + &stores.iter().map(|s| (*s).into()).collect::>(), + mode, + Arc::new(|| {}), + ) + .unwrap() +} + +#[test] +fn queued_transactions_take_their_snapshot_after_overlapping_predecessors_finish() { + let mut manager = IndexedDbManager::new_in_memory(); + let first_db = open_database(&mut manager); + let second_db = open_database(&mut manager); + let first = queue(&manager, first_db, &["a"], TransactionMode::ReadWrite); + let first_tx = manager.start_queued_transaction(first.handle()).unwrap(); + let reader = queue(&manager, second_db, &["a"], TransactionMode::ReadOnly); + let last = queue(&manager, first_db, &["a"], TransactionMode::ReadWrite); + assert!(!reader.handle().is_ready()); + assert!(manager.start_queued_transaction(reader.handle()).is_err()); + assert!(!last.handle().is_ready()); + manager.put(first_tx, "a", None, vec![1]).unwrap(); + manager.commit_transaction(first_tx).unwrap(); + drop(first); + let reader_tx = manager.start_queued_transaction(reader.handle()).unwrap(); + assert_eq!( + manager.get(reader_tx, "a", &Key::from(1)).unwrap(), + RequestOutcome::Value(Some(vec![1].into())) + ); + assert!(!last.handle().is_ready()); + manager.abort_transaction(reader_tx).unwrap(); + drop(reader); + let last_tx = manager.start_queued_transaction(last.handle()).unwrap(); + assert_eq!( + manager.put(last_tx, "a", None, vec![2]).unwrap(), + Key::from(2) + ); + manager.commit_transaction(last_tx).unwrap(); + drop(last); +} + +#[test] +fn disjoint_writers_and_readers_preserve_other_commits_in_memory_and_on_disk() { + let dir = TestDir::new(); + let mut manager = IndexedDbManager::new(&dir.path).unwrap(); + let database = open_database(&mut manager); + let first = queue(&manager, database, &["a"], TransactionMode::ReadWrite); + let second = queue(&manager, database, &["b"], TransactionMode::ReadWrite); + let reader = queue(&manager, database, &["c"], TransactionMode::ReadOnly); + let first_tx = manager.start_queued_transaction(first.handle()).unwrap(); + let second_tx = manager.start_queued_transaction(second.handle()).unwrap(); + let reader_tx = manager.start_queued_transaction(reader.handle()).unwrap(); + manager.put(first_tx, "a", None, vec![1]).unwrap(); + manager.put(second_tx, "b", None, vec![2]).unwrap(); + manager.commit_transaction(first_tx).unwrap(); + manager.commit_transaction(second_tx).unwrap(); + // A readonly transaction must neither revert the writes nor fail quota. + manager + .commit_transaction_with_quota( + reader_tx, + IndexedDbQuotaCheck { + quota: 0, + non_indexed_db_usage: 0, + }, + ) + .unwrap(); + drop((first, second, reader)); + for reload in [false, true] { + if reload { + manager = IndexedDbManager::new(&dir.path).unwrap(); + } + let db = open_database(&mut manager); + let tx = manager + .begin_transaction(db, &["a".into(), "b".into()], TransactionMode::ReadWrite) + .unwrap(); + for (store, value) in [("a", 1), ("b", 2)] { + assert_eq!( + manager.get(tx, store, &Key::from(1)).unwrap(), + RequestOutcome::Value(Some(vec![value].into())) + ); + assert_eq!(manager.put(tx, store, None, vec![3]).unwrap(), Key::from(2)); + } + manager.abort_transaction(tx).unwrap(); + manager.close_database(db).unwrap(); + } +} + +#[test] +fn quota_checks_include_new_commits_to_other_stores() { + for at_commit in [false, true] { + let mut manager = IndexedDbManager::new_in_memory(); + let database = open_database(&mut manager); + let initial_usage = manager.origin_usage_bytes("origin").unwrap(); + let first = manager + .begin_transaction(database, &["a".into()], TransactionMode::ReadWrite) + .unwrap(); + let second = manager + .begin_transaction(database, &["b".into()], TransactionMode::ReadWrite) + .unwrap(); + let quota = IndexedDbQuotaCheck { + quota: initial_usage + 180, + non_indexed_db_usage: 0, + }; + if at_commit { + manager + .put_with_quota(second, "b", None, vec![2; 100], quota) + .unwrap(); + } + manager.put(first, "a", None, vec![1; 100]).unwrap(); + manager.commit_transaction_with_quota(first, quota).unwrap(); + let published_usage = manager.origin_usage_bytes("origin").unwrap(); + let result = if at_commit { + manager + .commit_transaction_with_quota(second, quota) + .map(|_| Key::from(0)) + } else { + manager.put_with_quota(second, "b", None, vec![2; 100], quota) + }; + assert!(matches!(result, Err(IndexedDbError::QuotaExceeded { .. }))); + if !at_commit { + assert_eq!( + manager.next_generated_key(second, "b").unwrap(), + Key::from(1) + ); + manager.commit_transaction(second).unwrap(); + } + assert_eq!( + manager.origin_usage_bytes("origin").unwrap(), + published_usage + ); + let read = manager + .begin_transaction( + database, + &["a".into(), "b".into()], + TransactionMode::ReadOnly, + ) + .unwrap(); + assert_eq!( + manager.get(read, "a", &Key::from(1)).unwrap(), + RequestOutcome::Value(Some(vec![1; 100].into())) + ); + assert_eq!( + manager.get(read, "b", &Key::from(1)).unwrap(), + RequestOutcome::Value(None) + ); + manager.commit_transaction(read).unwrap(); + } +} + +#[test] +fn force_close_aborts_before_releasing_active_and_pending_admissions() { + let mut manager = IndexedDbManager::new_in_memory(); + let first_db = open_database(&mut manager); + let second_db = open_database(&mut manager); + let first = queue(&manager, first_db, &["a"], TransactionMode::ReadWrite); + let first_tx = manager.start_queued_transaction(first.handle()).unwrap(); + manager.put(first_tx, "a", None, vec![1]).unwrap(); + let pending = queue(&manager, first_db, &["a"], TransactionMode::ReadWrite); + let reader = queue(&manager, second_db, &["a"], TransactionMode::ReadOnly); + assert!(!reader.handle().is_ready()); + manager.force_close_database(first_db).unwrap(); + assert!(manager.commit_transaction(first_tx).is_err()); + assert!(!first.handle().is_ready()); + assert!(!pending.handle().is_ready()); + assert!(reader.handle().is_ready()); + let reader_tx = manager.start_queued_transaction(reader.handle()).unwrap(); + assert_eq!( + manager.get(reader_tx, "a", &Key::from(1)).unwrap(), + RequestOutcome::Value(None) + ); + manager.commit_transaction(reader_tx).unwrap(); + drop((first, pending, reader)); +} + +#[test] +fn clearing_storage_retires_admissions_before_reopening_the_same_database() { + for prefix in [false, true] { + let mut manager = IndexedDbManager::new_in_memory(); + let db = open_database(&mut manager); + let first = queue(&manager, db, &["a"], TransactionMode::ReadWrite); + let tx = manager.start_queued_transaction(first.handle()).unwrap(); + manager.put(tx, "a", None, vec![1]).unwrap(); + let pending = queue(&manager, db, &["a"], TransactionMode::ReadOnly); + if prefix { + manager.clear_origins_with_prefix("orig").unwrap(); + } else { + manager.clear_origin("origin").unwrap(); + } + assert!(!first.handle().is_ready()); + assert!(!pending.handle().is_ready()); + let db = open_database(&mut manager); + let read = queue(&manager, db, &["a"], TransactionMode::ReadOnly); + let tx = manager.start_queued_transaction(read.handle()).unwrap(); + assert_eq!( + manager.get(tx, "a", &Key::from(1)).unwrap(), + RequestOutcome::Value(None) + ); + manager.commit_transaction(tx).unwrap(); + drop((first, pending, read)); + } +} diff --git a/moli-indexeddb/src/transaction_queue.rs b/moli-indexeddb/src/transaction_queue.rs new file mode 100644 index 000000000..9b87aae04 --- /dev/null +++ b/moli-indexeddb/src/transaction_queue.rs @@ -0,0 +1,317 @@ +//! Admission of regular transactions across connections and event loops. +//! +//! A transaction waits for every older, unfinished transaction whose scope +//! overlaps and whose mode conflicts, including transactions still waiting to +//! start. The accepting realm owns a lease until backend commit or rollback. + +use parking_lot::Mutex; +use std::{ + collections::{BTreeMap, BTreeSet, VecDeque}, + sync::{Arc, Weak}, +}; + +use crate::{ConnectionRequestWake, DatabaseHandle, TransactionMode}; + +#[derive(Default)] +pub(crate) struct TransactionRequestQueues { + state: Mutex, +} + +#[derive(Default)] +struct QueueState { + next_id: u64, + queues: BTreeMap<(String, String), VecDeque>, +} + +struct TransactionScope { + stores: BTreeSet, + mode: TransactionMode, +} + +impl TransactionScope { + fn conflicts_with(&self, other: &Self) -> bool { + (self.mode != TransactionMode::ReadOnly || other.mode != TransactionMode::ReadOnly) + && !self.stores.is_disjoint(&other.stores) + } +} + +struct QueuedRequest { + id: u64, + database: DatabaseHandle, + scope: Arc, + ready: bool, + wake: ConnectionRequestWake, +} + +#[derive(Clone)] +pub struct TransactionRequestHandle { + queues: Weak, + key: (String, String), + id: u64, + database: DatabaseHandle, + scope: Arc, +} + +/// Drop only after committing or aborting the backend transaction. The lease +/// never retains the storage partition or any JS object. +pub struct TransactionRequestLease(TransactionRequestHandle); + +impl TransactionRequestLease { + pub fn handle(&self) -> &TransactionRequestHandle { + &self.0 + } +} + +impl Drop for TransactionRequestLease { + fn drop(&mut self) { + self.0.finish(); + } +} + +impl TransactionRequestQueues { + pub(crate) fn enqueue( + self: &Arc, + key: (String, String), + database: DatabaseHandle, + stores: BTreeSet, + mode: TransactionMode, + wake: ConnectionRequestWake, + ) -> TransactionRequestLease { + let scope = Arc::new(TransactionScope { stores, mode }); + let mut state = self.state.lock(); + state.next_id = state + .next_id + .checked_add(1) + .expect("IDB transaction id overflow"); + let id = state.next_id; + let queue = state.queues.entry(key.clone()).or_default(); + let ready = !queue.iter().any(|older| scope.conflicts_with(&older.scope)); + queue.push_back(QueuedRequest { + id, + database, + scope: scope.clone(), + ready, + wake, + }); + // Initial readiness is returned to the caller. A wake here could run + // before the accepting realm has attached its transaction wrapper. + TransactionRequestLease(TransactionRequestHandle { + queues: Arc::downgrade(self), + key, + id, + database, + scope, + }) + } + + /// Called after rolling back the connection's backend transactions. Wakes + /// may inspect this coordinator, but must not reenter the storage manager. + pub(crate) fn cancel_connection(&self, database: DatabaseHandle) { + let wakes = { + let mut state = self.state.lock(); + let mut wakes = Vec::new(); + state.queues.retain(|_, queue| { + let before = queue.len(); + queue.retain(|entry| entry.database != database); + if before != queue.len() { + wake_newly_ready(queue, &mut wakes); + } + !queue.is_empty() + }); + wakes + }; + for wake in wakes { + wake(); + } + } +} + +fn wake_newly_ready(queue: &mut VecDeque, wakes: &mut Vec) { + for index in 0..queue.len() { + if !queue[index].ready + && !queue + .iter() + .take(index) + .any(|older| queue[index].scope.conflicts_with(&older.scope)) + { + queue[index].ready = true; + wakes.push(queue[index].wake.clone()); + } + } +} + +impl TransactionRequestHandle { + pub fn is_ready(&self) -> bool { + self.queues.upgrade().is_some_and(|queues| { + queues + .state + .lock() + .queues + .get(&self.key) + .is_some_and(|queue| queue.iter().any(|entry| entry.id == self.id && entry.ready)) + }) + } + + pub(crate) fn belongs_to(&self, queues: &Arc) -> bool { + self.queues.ptr_eq(&Arc::downgrade(queues)) + } + + pub(crate) fn database(&self) -> DatabaseHandle { + self.database + } + + pub(crate) fn store_names(&self) -> Vec { + self.scope.stores.iter().cloned().collect() + } + + pub(crate) fn mode(&self) -> TransactionMode { + self.scope.mode + } + + fn finish(&self) { + let Some(queues) = self.queues.upgrade() else { + return; + }; + let wakes = { + let mut state = queues.state.lock(); + let Some(queue) = state.queues.get_mut(&self.key) else { + return; + }; + let Some(index) = queue.iter().position(|entry| entry.id == self.id) else { + return; + }; + queue.remove(index); + let mut wakes = Vec::new(); + wake_newly_ready(queue, &mut wakes); + if queue.is_empty() { + state.queues.remove(&self.key); + } + wakes + }; + for wake in wakes { + wake(); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::sync::atomic::{AtomicUsize, Ordering}; + + fn request( + queues: &Arc, + connection: u64, + stores: &[&str], + mode: TransactionMode, + wake: ConnectionRequestWake, + ) -> TransactionRequestLease { + queues.enqueue( + ("origin".into(), "db".into()), + DatabaseHandle::from_raw(connection), + stores.iter().map(|store| (*store).into()).collect(), + mode, + wake, + ) + } + + #[test] + fn pending_writers_block_later_readers_across_connections_and_cancel_on_drop() { + let queues = Arc::new(TransactionRequestQueues::default()); + let wakes = Arc::new(AtomicUsize::new(0)); + let wake: ConnectionRequestWake = { + let queues = Arc::downgrade(&queues); + let wakes = wakes.clone(); + Arc::new(move || { + // Wakes must run without holding the coordinator lock. + assert!(queues.upgrade().unwrap().state.try_lock().is_some()); + wakes.fetch_add(1, Ordering::SeqCst); + }) + }; + let reader = request(&queues, 1, &["a"], TransactionMode::ReadOnly, wake.clone()); + let parallel_reader = request(&queues, 2, &["a"], TransactionMode::ReadOnly, wake.clone()); + let writer = request( + &queues, + 2, + &["a", "b"], + TransactionMode::ReadWrite, + wake.clone(), + ); + let last_reader = request(&queues, 3, &["b"], TransactionMode::ReadOnly, wake.clone()); + let independent = request(&queues, 1, &["c"], TransactionMode::ReadWrite, wake); + assert!(reader.0.is_ready()); + assert!(parallel_reader.0.is_ready()); + assert!(!writer.0.is_ready()); + assert!(!last_reader.0.is_ready()); + assert!(independent.0.is_ready()); + assert_eq!(wakes.load(Ordering::SeqCst), 0); + drop(reader); + assert!(!writer.0.is_ready()); + std::thread::spawn(move || drop(parallel_reader)) + .join() + .unwrap(); + assert!(writer.0.is_ready()); + assert!(!last_reader.0.is_ready()); + assert_eq!(wakes.load(Ordering::SeqCst), 1); + drop(writer); + assert!(last_reader.0.is_ready()); + assert_eq!(wakes.load(Ordering::SeqCst), 2); + drop((last_reader, independent)); + assert!(queues.state.lock().queues.is_empty()); + } + + #[test] + fn canceling_middle_writer_unblocks_readers_and_connection_cancellation_is_idempotent() { + let queues = Arc::new(TransactionRequestQueues::default()); + let wake: ConnectionRequestWake = Arc::new(|| {}); + let first = request(&queues, 1, &["a"], TransactionMode::ReadOnly, wake.clone()); + let writer = request( + &queues, + 2, + &["a", "b"], + TransactionMode::ReadWrite, + wake.clone(), + ); + let reader = request( + &queues, + 3, + &["a", "b"], + TransactionMode::ReadOnly, + wake.clone(), + ); + let independent_writer = + request(&queues, 2, &["c"], TransactionMode::ReadWrite, wake.clone()); + let independent_reader = request(&queues, 3, &["c"], TransactionMode::ReadOnly, wake); + assert!(!reader.0.is_ready()); + assert!(!independent_reader.0.is_ready()); + queues.cancel_connection(DatabaseHandle::from_raw(2)); + assert!(first.0.is_ready()); + assert!(reader.0.is_ready()); + assert!(independent_reader.0.is_ready()); + assert!(!writer.0.is_ready()); + assert!(!independent_writer.0.is_ready()); + drop((writer, independent_writer)); + assert!(reader.0.is_ready()); + drop((first, reader, independent_reader)); + assert!(queues.state.lock().queues.is_empty()); + } + + #[test] + fn database_names_and_storage_keys_are_independent_and_leases_do_not_retain_partition() { + let queues = Arc::new(TransactionRequestQueues::default()); + let leases = + [("origin", "db"), ("origin", "other"), ("bucket", "db")].map(|(origin, name)| { + queues.enqueue( + (origin.into(), name.into()), + DatabaseHandle::from_raw(1), + BTreeSet::from(["store".into()]), + TransactionMode::ReadWrite, + Arc::new(|| {}), + ) + }); + assert!(leases.iter().all(|lease| lease.0.is_ready())); + drop(queues); + assert!(leases.iter().all(|lease| !lease.0.is_ready())); + drop(leases); + } +} diff --git a/moli-indexeddb/src/usage.rs b/moli-indexeddb/src/usage.rs index 30736b73d..20be15bb8 100644 --- a/moli-indexeddb/src/usage.rs +++ b/moli-indexeddb/src/usage.rs @@ -17,13 +17,17 @@ pub(crate) fn origin_usage_bytes(state: &OriginState) -> u64 { } pub(crate) fn database_usage_bytes(name: &str, database: &DatabaseData) -> u64 { + database_usage_bytes_for_stores(name, database.stores.iter()) +} + +pub(crate) fn database_usage_bytes_for_stores<'a>( + name: &str, + stores: impl Iterator, +) -> u64 { string_usage_bytes(name) .saturating_add(U64_STORAGE_BYTES) .saturating_add(sum_usage( - database - .stores - .iter() - .map(|(name, store)| object_store_usage_bytes(name, store)), + stores.map(|(name, store)| object_store_usage_bytes(name, store)), )) } diff --git a/moli-renderer-v8/src/context_bootstrap.rs b/moli-renderer-v8/src/context_bootstrap.rs index 550bd4da5..56b50577a 100644 --- a/moli-renderer-v8/src/context_bootstrap.rs +++ b/moli-renderer-v8/src/context_bootstrap.rs @@ -281,9 +281,10 @@ pub(crate) use self::image_data::{ pub(crate) use self::indexed_db::{ ConnectionRequestHandle, IndexedDbTaskSourceEntry, discard_indexed_db_task_by_id, flush_blocked_indexed_db_requests, flush_indexed_db_connection_notification, - flush_indexed_db_task_by_id, flush_next_indexed_db_task, indexed_db_has_pending_tasks, - install_worker_indexed_db_runtime_state, retire_indexed_db_context, - set_indexed_db_manager_for_context, set_worker_indexed_db_task_wake_for_context, + flush_indexed_db_task_by_id, flush_indexed_db_transaction_starts, flush_next_indexed_db_task, + indexed_db_has_pending_tasks, install_worker_indexed_db_runtime_state, + retire_indexed_db_context, set_indexed_db_manager_for_context, + set_worker_indexed_db_task_wake_for_context, }; #[cfg(test)] pub(crate) use self::indexed_db::{ diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/core.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/core.rs index f8079d102..b2ce5162c 100644 --- a/moli-renderer-v8/src/context_bootstrap/indexed_db/core.rs +++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/core.rs @@ -6,11 +6,11 @@ use super::{ INDEXED_DB_KEY_RANGE_MARKER_SLOT, INDEXED_DB_REQUEST_ERROR_SLOT, INDEXED_DB_REQUEST_READY_STATE_SLOT, INDEXED_DB_REQUEST_RESULT_SLOT, INDEXED_DB_TRANSACTION_ABORTED_SLOT, INDEXED_DB_TRANSACTION_ACTIVE_SLOT, - INDEXED_DB_TRANSACTION_COMMITTING_SLOT, INDEXED_DB_TRANSACTION_DB_KEY_SLOT, - INDEXED_DB_TRANSACTION_FINISHED_SLOT, INDEXED_DB_TRANSACTION_HANDLE_SLOT, IdbKeyRangeQuery, - IndexEntry, IndexInfo, IndexedDbError, IndexedDbExecutionOwner, IndexedDbExternalObject, - IndexedDbManager, IndexedDbObjectStoreMetadata, IndexedDbRuntimeArray, IndexedDbStorageScope, - IndexedDbValue, IndexedDbWrapperKind, Key, KeyPath, ObjectStoreInfo, PreparedObjectStoreWrite, + INDEXED_DB_TRANSACTION_COMMITTING_SLOT, INDEXED_DB_TRANSACTION_FINISHED_SLOT, + INDEXED_DB_TRANSACTION_HANDLE_SLOT, IdbKeyRangeQuery, IndexEntry, IndexInfo, IndexedDbError, + IndexedDbExecutionOwner, IndexedDbExternalObject, IndexedDbManager, + IndexedDbObjectStoreMetadata, IndexedDbRuntimeArray, IndexedDbStorageScope, IndexedDbValue, + IndexedDbWrapperKind, Key, KeyPath, ObjectStoreInfo, PreparedObjectStoreWrite, TransactionHandle, TransactionMode, context_host_ptr_from_global_bridge, global_constructor_prototype, indexed_db_database_store_metadata, indexed_db_object_store_metadata, indexed_db_runtime_array, indexed_db_transaction_mode, diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/core/registry.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/core/registry.rs index 8cc73ffa0..d9333f4d3 100644 --- a/moli-renderer-v8/src/context_bootstrap/indexed_db/core/registry.rs +++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/core/registry.rs @@ -1,7 +1,7 @@ use super::*; mod connections; -mod readwrite; +mod transactions; pub(in crate::context_bootstrap::indexed_db) use self::connections::{ database_connection_for_handle, database_registry_key, @@ -9,7 +9,6 @@ pub(in crate::context_bootstrap::indexed_db) use self::connections::{ register_blocked_database_context, register_open_database_connection, unregister_blocked_database_context, unregister_open_database_connection, }; -pub(in crate::context_bootstrap::indexed_db) use self::readwrite::{ - has_unfinished_readwrite_transaction_for_db, readwrite_transaction_can_start, - register_readwrite_transaction, transaction_db_key, unregister_readwrite_transaction, +pub(in crate::context_bootstrap::indexed_db) use self::transactions::{ + register_regular_transaction, unregister_regular_transaction, }; diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/core/registry/readwrite.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/core/registry/readwrite.rs deleted file mode 100644 index b127434ba..000000000 --- a/moli-renderer-v8/src/context_bootstrap/indexed_db/core/registry/readwrite.rs +++ /dev/null @@ -1,96 +0,0 @@ -use super::*; - -pub(in crate::context_bootstrap::indexed_db) fn transaction_db_key( - scope: &mut v8::PinScope<'_, '_>, - transaction: v8::Local<'_, v8::Object>, -) -> Option { - object_string_property(scope, transaction, INDEXED_DB_TRANSACTION_DB_KEY_SLOT) -} - -pub(in crate::context_bootstrap::indexed_db) fn register_readwrite_transaction( - scope: &mut v8::PinScope<'_, '_>, - transaction: v8::Local<'_, v8::Object>, -) { - push_unique_object_to_indexed_db_runtime_array( - scope, - IndexedDbRuntimeArray::ReadwriteTransactions, - transaction, - ); -} - -pub(in crate::context_bootstrap::indexed_db) fn unregister_readwrite_transaction( - scope: &mut v8::PinScope<'_, '_>, - transaction: v8::Local<'_, v8::Object>, -) { - let Some(queue) = indexed_db_runtime_array(scope, IndexedDbRuntimeArray::ReadwriteTransactions) - else { - return; - }; - let next = v8::Array::new(scope, 0); - for index in 0..queue.length() { - let Some(value) = queue.get_index(scope, index) else { - continue; - }; - if value.strict_equals(transaction.into()) { - continue; - } - let _ = next.set_index(scope, next.length(), value); - } - replace_indexed_db_runtime_array(scope, IndexedDbRuntimeArray::ReadwriteTransactions, next); -} - -pub(in crate::context_bootstrap::indexed_db) fn readwrite_transaction_can_start( - scope: &mut v8::PinScope<'_, '_>, - transaction: v8::Local<'_, v8::Object>, -) -> bool { - let Some(db_key) = transaction_db_key(scope, transaction) else { - return true; - }; - let Some(queue) = indexed_db_runtime_array(scope, IndexedDbRuntimeArray::ReadwriteTransactions) - else { - return true; - }; - for index in 0..queue.length() { - let Some(value) = queue.get_index(scope, index) else { - continue; - }; - let Ok(candidate) = v8::Local::::try_from(value) else { - continue; - }; - if candidate.strict_equals(transaction.into()) { - return true; - } - if transaction_db_key(scope, candidate).as_deref() == Some(&db_key) - && !object_bool_property(scope, candidate, INDEXED_DB_TRANSACTION_FINISHED_SLOT) - .unwrap_or(false) - { - return false; - } - } - true -} - -pub(in crate::context_bootstrap::indexed_db) fn has_unfinished_readwrite_transaction_for_db( - scope: &mut v8::PinScope<'_, '_>, - db_key: &str, -) -> bool { - let Some(queue) = indexed_db_runtime_array(scope, IndexedDbRuntimeArray::ReadwriteTransactions) - else { - return false; - }; - for index in 0..queue.length() { - let Some(value) = queue.get_index(scope, index) else { - continue; - }; - let Ok(transaction) = v8::Local::::try_from(value) else { - continue; - }; - if transaction_db_key(scope, transaction).as_deref() == Some(db_key) - && !object_bool_property(scope, transaction, INDEXED_DB_TRANSACTION_FINISHED_SLOT) - .unwrap_or(false) - { - return true; - } - } - false -} diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/core/registry/transactions.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/core/registry/transactions.rs new file mode 100644 index 000000000..185e2fc96 --- /dev/null +++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/core/registry/transactions.rs @@ -0,0 +1,35 @@ +use super::*; + +pub(in crate::context_bootstrap::indexed_db) fn register_regular_transaction( + scope: &mut v8::PinScope<'_, '_>, + transaction: v8::Local<'_, v8::Object>, +) { + push_unique_object_to_indexed_db_runtime_array( + scope, + IndexedDbRuntimeArray::Transactions, + transaction, + ); +} + +pub(in crate::context_bootstrap::indexed_db) fn unregister_regular_transaction( + scope: &mut v8::PinScope<'_, '_>, + transaction: v8::Local<'_, v8::Object>, +) { + crate::context_bootstrap::indexed_db::finish_indexed_db_transaction_start_request( + scope, + transaction, + ); + let Some(queue) = indexed_db_runtime_array(scope, IndexedDbRuntimeArray::Transactions) else { + return; + }; + let next = v8::Array::new(scope, 0); + for index in 0..queue.length() { + let Some(value) = queue.get_index(scope, index) else { + continue; + }; + if !value.strict_equals(transaction.into()) { + let _ = next.set_index(scope, next.length(), value); + } + } + replace_indexed_db_runtime_array(scope, IndexedDbRuntimeArray::Transactions, next); +} diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/database.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/database.rs index d5a86140c..718602f94 100644 --- a/moli-renderer-v8/src/context_bootstrap/indexed_db/database.rs +++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/database.rs @@ -1,27 +1,25 @@ use super::{ - INDEXED_DB_DATABASE_CLOSED_SLOT, INDEXED_DB_DATABASE_KEY_SLOT, - INDEXED_DB_DATABASE_UPGRADE_TRANSACTION_SLOT, INDEXED_DB_TRANSACTION_ABORTED_SLOT, - INDEXED_DB_TRANSACTION_ACTIVE_SLOT, INDEXED_DB_TRANSACTION_COMMITTING_SLOT, - INDEXED_DB_TRANSACTION_FINISHED_SLOT, IndexedDbExecutionOwner, IndexedDbStorageScope, - IndexedDbWrapperKind, KeyPath, ObjectStoreInfo, ObjectStoreOptions, TransactionMode, - abort_queued_transaction_requests, compare_idb_keys, context_host_ptr_from_global_bridge, - create_object_store_object, create_open_request_object, create_transaction_object, - current_storage_scope, database_handle_from_value, dom_exception_value, - enqueue_blocked_delete_task, enqueue_blocked_open_task, + INDEXED_DB_DATABASE_CLOSED_SLOT, INDEXED_DB_DATABASE_UPGRADE_TRANSACTION_SLOT, + INDEXED_DB_TRANSACTION_ABORTED_SLOT, INDEXED_DB_TRANSACTION_ACTIVE_SLOT, + INDEXED_DB_TRANSACTION_COMMITTING_SLOT, INDEXED_DB_TRANSACTION_FINISHED_SLOT, + IndexedDbExecutionOwner, IndexedDbStorageScope, IndexedDbWrapperKind, KeyPath, ObjectStoreInfo, + ObjectStoreOptions, TransactionMode, abort_queued_transaction_requests, compare_idb_keys, + context_host_ptr_from_global_bridge, create_object_store_object, create_open_request_object, + create_transaction_object, current_storage_scope, database_handle_from_value, + dom_exception_value, enqueue_blocked_delete_task, enqueue_blocked_open_task, enqueue_drain_blocked_open_requests_task, enqueue_indexed_db_task, - enqueue_next_readwrite_transaction_start, enqueue_transaction_abort_task, - enqueue_transaction_commit_task, has_unfinished_readwrite_transaction_for_db, - indexed_db_databases_settle_task_payload, indexed_db_factory_storage_scope, - indexed_db_runtime_factory, indexed_db_typed_execution_owner, indexed_db_typed_wrapper_is, - object_bool_property, object_property_as_object, object_store_info_from_database_metadata, - object_string_property, origin_allows_indexed_db, parse_optional_idb_key_path_member, - register_indexed_db_databases_settle_task, register_readwrite_transaction, - remove_database_store_metadata, request_error_object, require_idb_key, - set_database_store_metadata, set_indexed_db_slot_value, + enqueue_ready_transaction_starts, enqueue_transaction_abort_task, + enqueue_transaction_commit_task, indexed_db_databases_settle_task_payload, + indexed_db_factory_storage_scope, indexed_db_runtime_factory, indexed_db_typed_execution_owner, + indexed_db_typed_wrapper_is, object_bool_property, object_property_as_object, + object_store_info_from_database_metadata, origin_allows_indexed_db, + parse_optional_idb_key_path_member, register_indexed_db_databases_settle_task, + register_regular_transaction, remove_database_store_metadata, request_error_object, + require_idb_key, set_database_store_metadata, set_indexed_db_slot_value, storage_scope_for_window_execution_context, store_request_error, - sync_transaction_object_store_names_from_database, throw_type_error, transaction_db_key, + sync_transaction_object_store_names_from_database, throw_type_error, transaction_handle_from_value, unregister_open_database_connection, - unregister_readwrite_transaction, v8_string, v8str, validate_storage_bucket_scope, + unregister_regular_transaction, v8_string, v8str, validate_storage_bucket_scope, with_indexed_db_manager, }; diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/database/transactions/database.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/database/transactions/database.rs index a473383fe..6e6196fe4 100644 --- a/moli-renderer-v8/src/context_bootstrap/indexed_db/database/transactions/database.rs +++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/database/transactions/database.rs @@ -1,5 +1,9 @@ use super::*; -use crate::context_bootstrap::indexed_db::schedule_indexed_db_transaction_deactivation_after_microtask_checkpoint; +use crate::context_bootstrap::indexed_db::{ + indexed_db_transaction_start_wake, + schedule_indexed_db_transaction_deactivation_after_microtask_checkpoint, + set_indexed_db_transaction_start_request, +}; use crate::webidl; use moli_indexeddb::{DatabaseHandle, IndexedDbError}; @@ -63,7 +67,7 @@ pub(in crate::context_bootstrap::indexed_db) fn idb_database_transaction_callbac let mut store_names = parsed.store_names.0; store_names.sort_unstable(); store_names.dedup(); - // Validate before queuing a readwrite transaction as well: its backend + // Validate before queuing a transaction as well: its backend // handle may only be allocated after an earlier transaction finishes. if store_names .iter() @@ -123,33 +127,28 @@ fn create_regular_transaction<'s>( // Objects and task queues belong to the connection's realm even when the // operation was borrowed from another Window. Conversion and exceptions // remain in the callback's realm. - if mode == TransactionMode::ReadWrite { - let db_key = object_string_property(scope, database, INDEXED_DB_DATABASE_KEY_SLOT) - .unwrap_or_default(); - let transaction_handle = if has_unfinished_readwrite_transaction_for_db(scope, &db_key) { - None - } else { - Some(with_indexed_db_manager(scope, |manager| { - manager.begin_transaction(handle, store_names, mode) - })?) - }; - let Some(transaction) = - create_transaction_object(scope, database, transaction_handle, mode, store_names) - else { - return Ok(None); - }; - register_readwrite_transaction(scope, transaction); - schedule_indexed_db_transaction_deactivation_after_microtask_checkpoint(scope, transaction); - return Ok(Some(transaction)); - } - let transaction_handle = with_indexed_db_manager(scope, |manager| { - manager.begin_transaction(handle, store_names, mode) + let owner = indexed_db_typed_execution_owner(scope, database).expect("IDB database owner"); + let wake = indexed_db_transaction_start_wake(scope, owner); + let request = with_indexed_db_manager(scope, |manager| { + manager.queue_transaction_start(handle, store_names, mode, wake) })?; + let transaction_handle = if request.handle().is_ready() { + Some(with_indexed_db_manager(scope, |manager| { + manager.start_queued_transaction(request.handle()) + })?) + } else { + None + }; let Some(transaction) = - create_transaction_object(scope, database, Some(transaction_handle), mode, store_names) + create_transaction_object(scope, database, transaction_handle, mode, store_names) else { + if let Some(handle) = transaction_handle { + let _ = with_indexed_db_manager(scope, |manager| manager.abort_transaction(handle)); + } return Ok(None); }; + set_indexed_db_transaction_start_request(scope, transaction, request); + register_regular_transaction(scope, transaction); schedule_indexed_db_transaction_deactivation_after_microtask_checkpoint(scope, transaction); Ok(Some(transaction)) } diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/database/transactions/lifecycle.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/database/transactions/lifecycle.rs index fc0e5a76a..246bbe31c 100644 --- a/moli-renderer-v8/src/context_bootstrap/indexed_db/database/transactions/lifecycle.rs +++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/database/transactions/lifecycle.rs @@ -28,11 +28,8 @@ pub(in crate::context_bootstrap::indexed_db) fn finish_transaction_abort<'s>( v8::Boolean::new(scope, true).into(), ); abort_queued_transaction_requests(scope, transaction); - let db_key = transaction_db_key(scope, transaction); - unregister_readwrite_transaction(scope, transaction); - if let Some(db_key) = db_key { - enqueue_next_readwrite_transaction_start(scope, &db_key); - } + unregister_regular_transaction(scope, transaction); + enqueue_ready_transaction_starts(scope); enqueue_transaction_abort_task(scope, transaction); } diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/mod.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/mod.rs index 1123f5c77..081b8b0ae 100644 --- a/moli-renderer-v8/src/context_bootstrap/indexed_db/mod.rs +++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/mod.rs @@ -63,6 +63,10 @@ pub(crate) fn flush_blocked_indexed_db_requests(scope: &mut v8::PinScope<'_, '_> flush_drain_blocked_open_requests_task(scope); } +pub(crate) fn flush_indexed_db_transaction_starts(scope: &mut v8::PinScope<'_, '_>) { + enqueue_ready_transaction_starts(scope); +} + pub(crate) fn flush_indexed_db_connection_notification( scope: &mut v8::PinScope<'_, '_>, handle: moli_indexeddb::DatabaseHandle, diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/runtime.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/runtime.rs index c3d1067c2..07b64c129 100644 --- a/moli-renderer-v8/src/context_bootstrap/indexed_db/runtime.rs +++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/runtime.rs @@ -10,8 +10,7 @@ const INDEXED_DB_FACTORY_INITIALIZED_FIELD: &str = "moli.IndexedDb.runtime.facto const INDEXED_DB_TASK_QUEUE_FIELD: &str = "moli.IndexedDb.runtime.taskQueue"; const INDEXED_DB_OPEN_DATABASES_FIELD: &str = "moli.IndexedDb.runtime.openDatabases"; const INDEXED_DB_BLOCKED_OPEN_QUEUE_FIELD: &str = "moli.IndexedDb.runtime.blockedOpenQueue"; -const INDEXED_DB_READWRITE_TRANSACTION_QUEUE_FIELD: &str = - "moli.IndexedDb.runtime.readwriteTransactionQueue"; +const INDEXED_DB_TRANSACTION_QUEUE_FIELD: &str = "moli.IndexedDb.runtime.transactionQueue"; #[derive(Default, WebApiObject)] #[webapi(interface = web_api_interfaces::IDBFactory, require_prototype)] @@ -33,6 +32,7 @@ pub(crate) enum IndexedDbTaskSourceEntry { RuntimeQueue(IndexedDbTaskId), DrainBlockedOpenRequests, VersionChange(DatabaseHandle), + TransactionsReady, } #[derive(Clone, Copy)] @@ -40,7 +40,7 @@ pub(in crate::context_bootstrap::indexed_db) enum IndexedDbRuntimeArray { TaskQueue, OpenDatabases, BlockedOpenQueue, - ReadwriteTransactions, + Transactions, } impl IndexedDbRuntimeArray { @@ -49,7 +49,7 @@ impl IndexedDbRuntimeArray { Self::TaskQueue => INDEXED_DB_TASK_QUEUE_FIELD, Self::OpenDatabases => INDEXED_DB_OPEN_DATABASES_FIELD, Self::BlockedOpenQueue => INDEXED_DB_BLOCKED_OPEN_QUEUE_FIELD, - Self::ReadwriteTransactions => INDEXED_DB_READWRITE_TRANSACTION_QUEUE_FIELD, + Self::Transactions => INDEXED_DB_TRANSACTION_QUEUE_FIELD, } } } @@ -159,6 +159,16 @@ pub(super) fn indexed_db_connection_notification_wake( ) } +pub(super) fn indexed_db_transaction_start_wake( + scope: &mut v8::PinScope<'_, '_>, + owner: IndexedDbExecutionOwner, +) -> state::ConnectionRequestWake { + // Connection retirement aborts native transactions and cancels their + // admissions. This callback only wakes the owner; it must not reenter the + // storage manager when cancellation is running under the manager lock. + indexed_db_task_wake(scope, owner, IndexedDbTaskSourceEntry::TransactionsReady) +} + pub(super) fn indexed_db_has_external_task_source(scope: &mut v8::PinScope<'_, '_>) -> bool { context_host_ptr_from_global_bridge(scope).is_some() || scope @@ -323,7 +333,7 @@ fn ensure_runtime_state_fields<'s>( ensure_runtime_array_field(scope, state, IndexedDbRuntimeArray::TaskQueue)?; ensure_runtime_array_field(scope, state, IndexedDbRuntimeArray::OpenDatabases)?; ensure_runtime_array_field(scope, state, IndexedDbRuntimeArray::BlockedOpenQueue)?; - ensure_runtime_array_field(scope, state, IndexedDbRuntimeArray::ReadwriteTransactions)?; + ensure_runtime_array_field(scope, state, IndexedDbRuntimeArray::Transactions)?; Some(()) } diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks.rs index 4ef5cbf67..f42307bbc 100644 --- a/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks.rs +++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks.rs @@ -14,31 +14,30 @@ use super::{ IndexedDbRuntimeArray, IndexedDbStorageScope, IndexedDbTransactionOperation, OpenOptions, PreparedObjectStoreWrite, TransactionHandle, TransactionMode, apply_index_collection_direction, apply_object_store_collection_direction, close_indexed_db_database_connection, - create_database_object, create_transaction_object, database_handle_from_value, - database_registry_key, define_non_enumerable_value_property, deserialize_js_value, - dispatch_idb_named_event, dispatch_version_change_event, dom_exception_value, - dom_string_list_values, enforce_object_store_unique_constraints, + create_database_object, create_transaction_object, database_registry_key, + define_non_enumerable_value_property, deserialize_js_value, dispatch_idb_named_event, + dispatch_version_change_event, dom_exception_value, enforce_object_store_unique_constraints, enqueue_version_change_to_open_connections, flush_databases_settle_task, has_open_database_connections_for_key, index_cursor_snapshot, indexed_db_blocked_task_payload, indexed_db_index_info, indexed_db_open_task_payload, indexed_db_request_dispatch_task_request, indexed_db_request_transaction_object, indexed_db_runtime_array, - indexed_db_runtime_array_contains_object, indexed_db_transaction_task_transaction, - indexed_db_typed_execution_owner, indexed_db_typed_owner_scope, - indexed_db_typed_task_execution_context, indexed_db_typed_task_execution_owner, - indexed_db_typed_task_id, indexed_db_typed_task_kind, indexed_db_typed_task_owner_scope, - indexed_db_typed_task_storage_scope, inject_key_path_into_value, key_in_range, key_to_js_value, + indexed_db_runtime_array_contains_object, indexed_db_transaction_start_request, + indexed_db_transaction_task_transaction, indexed_db_typed_execution_owner, + indexed_db_typed_owner_scope, indexed_db_typed_task_execution_context, + indexed_db_typed_task_execution_owner, indexed_db_typed_task_id, indexed_db_typed_task_kind, + indexed_db_typed_task_owner_scope, indexed_db_typed_task_storage_scope, + inject_key_path_into_value, key_in_range, key_to_js_value, materialize_cursor_result_in_request_realm, object_bool_property, object_hidden_value, - object_number_property, object_property_as_object, object_store_cursor_snapshot, - object_string_property, pop_first_indexed_db_task, push_indexed_db_operation_waiting_for_start, + object_number_property, object_store_cursor_snapshot, object_string_property, + pop_first_indexed_db_task, push_indexed_db_operation_waiting_for_start, push_object_to_indexed_db_runtime_array, push_unique_object_to_indexed_db_runtime_array, - readwrite_transaction_can_start, refresh_cursor_surface, refresh_database_surface, - register_blocked_database_context, replace_indexed_db_runtime_array, request_error_object, - scan_index_entries, scan_object_store_entries, serialize_js_value, set_indexed_db_slot_value, + refresh_cursor_surface, refresh_database_surface, register_blocked_database_context, + replace_indexed_db_runtime_array, request_error_object, scan_index_entries, + scan_object_store_entries, serialize_js_value, set_indexed_db_slot_value, signal_worker_indexed_db_task_wake, storage_bucket_quota_check_for_object_store, storage_bucket_quota_check_for_transaction, take_indexed_db_operations_waiting_for_start, - take_indexed_db_task_by_id, transaction_db_key, transaction_handle_from_value, - unregister_blocked_database_context, unregister_readwrite_transaction, v8str, - validate_storage_bucket_scope, with_indexed_db_manager, + take_indexed_db_task_by_id, transaction_handle_from_value, unregister_blocked_database_context, + unregister_regular_transaction, v8str, validate_storage_bucket_scope, with_indexed_db_manager, }; use super::{ IndexedDbTaskSourceEntry, context_host_ptr_from_global_bridge, diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/dispatch/router.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/dispatch/router.rs index e0b55c458..c39fb9a17 100644 --- a/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/dispatch/router.rs +++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/dispatch/router.rs @@ -23,6 +23,7 @@ pub(crate) fn flush_next_indexed_db_task(scope: &mut v8::PinScope<'_, '_>) -> bo scope, handle, ); } + IndexedDbTaskSourceEntry::TransactionsReady => enqueue_ready_transaction_starts(scope), } return true; } diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/dispatch/transaction/commit.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/dispatch/transaction/commit.rs index 90cd495b1..419139068 100644 --- a/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/dispatch/transaction/commit.rs +++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/dispatch/transaction/commit.rs @@ -126,9 +126,6 @@ fn finish_transaction<'s>( INDEXED_DB_TRANSACTION_FINISHED_SLOT, v8::Boolean::new(scope, true).into(), ); - let db_key = transaction_db_key(scope, transaction); - unregister_readwrite_transaction(scope, transaction); - if let Some(db_key) = db_key { - enqueue_next_readwrite_transaction_start(scope, &db_key); - } + unregister_regular_transaction(scope, transaction); + enqueue_ready_transaction_starts(scope); } diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/operations/flush/start.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/operations/flush/start.rs index 63835fd75..b415ac000 100644 --- a/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/operations/flush/start.rs +++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/operations/flush/start.rs @@ -21,27 +21,21 @@ pub(in crate::context_bootstrap::indexed_db) fn flush_transaction_start_task<'s> .unwrap_or(false) || object_bool_property(scope, transaction, INDEXED_DB_TRANSACTION_FINISHED_SLOT) .unwrap_or(false) - || !readwrite_transaction_can_start(scope, transaction) { return; } - let Some(database) = object_property_as_object(scope, transaction, "db") else { + let Some(request) = indexed_db_transaction_start_request(scope, transaction) else { return; }; - let Some(handle) = database_handle_from_value(scope, database.into()) else { + if !request.is_ready() { return; - }; - let store_names = object_property_as_object(scope, transaction, "objectStoreNames") - .map(|names| dom_string_list_values(scope, names)) - .unwrap_or_default(); - match with_indexed_db_manager(scope, |manager| { - manager.begin_transaction(handle, &store_names, TransactionMode::ReadWrite) - }) { + } + match with_indexed_db_manager(scope, |manager| manager.start_queued_transaction(&request)) { Ok(transaction_handle) => { - success::start_readwrite_transaction(scope, transaction, transaction_handle); + success::start_regular_transaction(scope, transaction, transaction_handle); } Err(error) => { - fail::fail_readwrite_transaction_start(scope, transaction, &error); + fail::fail_regular_transaction_start(scope, transaction, &error); } } } diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/operations/flush/start/fail.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/operations/flush/start/fail.rs index 4308ae580..696f4ebfd 100644 --- a/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/operations/flush/start/fail.rs +++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/operations/flush/start/fail.rs @@ -1,30 +1,11 @@ use super::*; use moli_indexeddb::IndexedDbError; -pub(super) fn fail_readwrite_transaction_start<'s>( +pub(super) fn fail_regular_transaction_start<'s>( scope: &mut v8::PinScope<'s, '_>, transaction: v8::Local<'s, v8::Object>, error: &IndexedDbError, ) { let error = request_error_object(scope, error); - let _ = transaction.set(scope, v8str(scope, "error").into(), error); - set_indexed_db_slot_value( - scope, - transaction, - INDEXED_DB_TRANSACTION_ACTIVE_SLOT, - v8::Boolean::new(scope, false).into(), - ); - set_indexed_db_slot_value( - scope, - transaction, - INDEXED_DB_TRANSACTION_FINISHED_SLOT, - v8::Boolean::new(scope, true).into(), - ); - unregister_readwrite_transaction(scope, transaction); - if let Some(database) = - crate::context_bootstrap::indexed_db::indexed_db_transaction_database(scope, transaction) - { - crate::context_bootstrap::indexed_db::finish_indexed_db_database_close(scope, database); - } - let _ = dispatch_idb_named_event(scope, transaction, "error", |_, _| {}); + crate::context_bootstrap::indexed_db::finish_transaction_abort(scope, transaction, error); } diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/operations/flush/start/success.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/operations/flush/start/success.rs index 6156460e9..c058bf6ec 100644 --- a/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/operations/flush/start/success.rs +++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/operations/flush/start/success.rs @@ -1,6 +1,6 @@ use super::*; -pub(super) fn start_readwrite_transaction<'s>( +pub(super) fn start_regular_transaction<'s>( scope: &mut v8::PinScope<'s, '_>, transaction: v8::Local<'s, v8::Object>, transaction_handle: TransactionHandle, diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/queue/transaction.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/queue/transaction.rs index 4b11f621f..3e0d1b898 100644 --- a/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/queue/transaction.rs +++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/queue/transaction.rs @@ -7,7 +7,7 @@ mod successor; pub(in crate::context_bootstrap::indexed_db) use self::abort::enqueue_transaction_abort_task; pub(in crate::context_bootstrap::indexed_db) use self::commit::enqueue_transaction_commit_task; -pub(in crate::context_bootstrap::indexed_db) use self::successor::enqueue_next_readwrite_transaction_start; +pub(in crate::context_bootstrap::indexed_db) use self::successor::enqueue_ready_transaction_starts; pub(in crate::context_bootstrap::indexed_db) fn enqueue_transaction_operation_error<'s>( scope: &mut v8::PinScope<'s, '_>, diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/queue/transaction/successor.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/queue/transaction/successor.rs index 8556bd33a..91e17ddb3 100644 --- a/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/queue/transaction/successor.rs +++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/tasks/queue/transaction/successor.rs @@ -1,12 +1,10 @@ use super::start::enqueue_transaction_start_task; use super::*; -pub(in crate::context_bootstrap::indexed_db) fn enqueue_next_readwrite_transaction_start( +pub(in crate::context_bootstrap::indexed_db) fn enqueue_ready_transaction_starts( scope: &mut v8::PinScope<'_, '_>, - db_key: &str, ) { - let Some(queue) = indexed_db_runtime_array(scope, IndexedDbRuntimeArray::ReadwriteTransactions) - else { + let Some(queue) = indexed_db_runtime_array(scope, IndexedDbRuntimeArray::Transactions) else { return; }; for index in 0..queue.length() { @@ -16,9 +14,6 @@ pub(in crate::context_bootstrap::indexed_db) fn enqueue_next_readwrite_transacti let Ok(transaction) = v8::Local::::try_from(value) else { continue; }; - if transaction_db_key(scope, transaction).as_deref() != Some(db_key) { - continue; - } if object_bool_property(scope, transaction, INDEXED_DB_TRANSACTION_FINISHED_SLOT) .unwrap_or(false) || object_bool_property(scope, transaction, INDEXED_DB_TRANSACTION_STARTED_SLOT) @@ -26,7 +21,10 @@ pub(in crate::context_bootstrap::indexed_db) fn enqueue_next_readwrite_transacti { continue; } - enqueue_transaction_start_task(scope, transaction); - break; + if indexed_db_transaction_start_request(scope, transaction) + .is_some_and(|request| request.is_ready()) + { + enqueue_transaction_start_task(scope, transaction); + } } } diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/typed_state.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/typed_state.rs index 0d2c09432..72d86bf5c 100644 --- a/moli-renderer-v8/src/context_bootstrap/indexed_db/typed_state.rs +++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/typed_state.rs @@ -398,6 +398,7 @@ struct IndexedDbTransactionLifecycleState { database: Option>, upgrade_open_request: Option>, handle: Option, + start_request: Option, mode: TransactionMode, active: bool, committing: bool, @@ -425,6 +426,7 @@ impl IndexedDbTransactionLifecycleState { database: Some(database), upgrade_open_request: None, handle, + start_request: None, mode, active: true, committing: false, @@ -683,6 +685,9 @@ impl IndexedDbRuntimeStateTable { for request in self.requests.values_mut() { drop(request.connection_request.take()); } + for transaction in self.transactions.values_mut() { + drop(transaction.start_request.take()); + } } } @@ -880,6 +885,55 @@ pub(super) fn indexed_db_transaction_mode( table.borrow().transactions.get(&id).map(|state| state.mode) } +pub(super) fn set_indexed_db_transaction_start_request( + scope: &mut v8::PinScope<'_, '_>, + transaction: v8::Local<'_, v8::Object>, + request: moli_indexeddb::TransactionRequestLease, +) { + let id = indexed_db_typed_state_id(scope, transaction).expect("transaction id"); + let table = indexed_db_runtime_state_table_for_object(scope, transaction); + let mut table = table.borrow_mut(); + let state = table.transactions.get_mut(&id).expect("transaction state"); + assert!(state.start_request.is_none(), "transaction scheduled twice"); + state.start_request = Some(request); +} + +pub(super) fn indexed_db_transaction_start_request( + scope: &mut v8::PinScope<'_, '_>, + transaction: v8::Local<'_, v8::Object>, +) -> Option { + let id = indexed_db_typed_state_id(scope, transaction)?; + let table = indexed_db_runtime_state_table_for_object(scope, transaction); + Some( + table + .borrow() + .transactions + .get(&id)? + .start_request + .as_ref()? + .handle() + .clone(), + ) +} + +pub(super) fn finish_indexed_db_transaction_start_request( + scope: &mut v8::PinScope<'_, '_>, + transaction: v8::Local<'_, v8::Object>, +) { + let Some(id) = indexed_db_typed_state_id(scope, transaction) else { + return; + }; + let table = indexed_db_runtime_state_table_for_object(scope, transaction); + let request = table + .borrow_mut() + .transactions + .get_mut(&id) + .and_then(|state| state.start_request.take()); + // Releasing admission wakes other event loops after native commit/rollback + // and after releasing the local state borrow. + drop(request); +} + pub(in crate::context_bootstrap::indexed_db) fn schedule_indexed_db_transaction_deactivation_after_microtask_checkpoint< 's, >( diff --git a/moli-renderer-v8/src/script_vm/indexed_db_task_body.rs b/moli-renderer-v8/src/script_vm/indexed_db_task_body.rs index 5d9e1ac53..76aaaddaf 100644 --- a/moli-renderer-v8/src/script_vm/indexed_db_task_body.rs +++ b/moli-renderer-v8/src/script_vm/indexed_db_task_body.rs @@ -84,6 +84,10 @@ impl ScriptVm { scope, handle, ) } + RendererPageIndexedDbTaskKind::TransactionsReady => { + crate::context_bootstrap::flush_indexed_db_transaction_starts(scope); + true + } }; execution_context .dispatch_scope() @@ -118,7 +122,8 @@ impl ScriptVm { unsafe { &*host_ptr }.finish_indexed_db_blocked_drain(execution_context); true } - RendererPageIndexedDbTaskKind::VersionChange(_) => false, + RendererPageIndexedDbTaskKind::VersionChange(_) + | RendererPageIndexedDbTaskKind::TransactionsReady => false, }) })?; Ok(if removed {