LibWeb: Implement IndexedDB transaction dependency ordering
This fixes the regression in idbindex_reverse_cursor.any.html, which was actually exposing the underlying issue of ignoring conflicting read/write transactions. Now, if a read/write transaction is in the queue, no transactions can coincide with its requests' execution.
This commit is contained in:
parent
3c24a394c6
commit
ea96072fee
7 changed files with 129 additions and 5 deletions
|
|
@ -4,6 +4,7 @@
|
|||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <AK/AnyOf.h>
|
||||
#include <LibWeb/Bindings/IDBDatabasePrototype.h>
|
||||
#include <LibWeb/Bindings/Intrinsics.h>
|
||||
#include <LibWeb/Crypto/Crypto.h>
|
||||
|
|
@ -247,6 +248,8 @@ WebIDL::ExceptionOr<GC::Ref<IDBTransaction>> IDBDatabase::transaction(Variant<St
|
|||
// 8. Set transaction’s cleanup event loop to the current event loop.
|
||||
transaction->set_cleanup_event_loop(HTML::main_thread_event_loop());
|
||||
|
||||
block_on_conflicting_transactions(transaction);
|
||||
|
||||
// 9. Return an IDBTransaction object representing transaction.
|
||||
return transaction;
|
||||
}
|
||||
|
|
@ -299,4 +302,45 @@ void IDBDatabase::check_pending_transaction_waits()
|
|||
}
|
||||
}
|
||||
|
||||
// https://w3c.github.io/IndexedDB/#transaction-scheduling
|
||||
void IDBDatabase::block_on_conflicting_transactions(GC::Ref<IDBTransaction> transaction)
|
||||
{
|
||||
// The following constraints define when a transaction can be started:
|
||||
|
||||
// - A read-only transactions tx can start when there are no read/write transactions which:
|
||||
// - A read/write transaction tx can start when there are no transactions which:
|
||||
|
||||
Vector<GC::Ref<IDBTransaction>> blocking;
|
||||
for (auto const& other : m_transactions) {
|
||||
// - Were created before tx; and
|
||||
if (other.ptr() == transaction.ptr())
|
||||
break;
|
||||
|
||||
// NB: According to the above conditions, we only block on transactions if one is read/write.
|
||||
if (transaction->is_readonly() && other->is_readonly())
|
||||
continue;
|
||||
|
||||
// - have overlapping scopes with tx; and
|
||||
bool have_overlapping_scopes = any_of(transaction->scope(), [&](auto const& store) {
|
||||
return other->scope().contains_slow(store);
|
||||
});
|
||||
if (!have_overlapping_scopes)
|
||||
continue;
|
||||
|
||||
// - are not finished.
|
||||
if (other->is_finished())
|
||||
continue;
|
||||
|
||||
blocking.append(other);
|
||||
}
|
||||
|
||||
if (blocking.is_empty())
|
||||
return;
|
||||
|
||||
transaction->request_list().block_execution();
|
||||
wait_for_transactions_to_finish(blocking, GC::create_function(realm().heap(), [transaction] {
|
||||
transaction->request_list().unblock_execution();
|
||||
}));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -82,6 +82,7 @@ public:
|
|||
|
||||
void wait_for_transactions_to_finish(ReadonlySpan<GC::Ref<IDBTransaction>>, GC::Ref<GC::Function<void()>> on_complete);
|
||||
void check_pending_transaction_waits();
|
||||
void block_on_conflicting_transactions(GC::Ref<IDBTransaction>);
|
||||
|
||||
protected:
|
||||
explicit IDBDatabase(JS::Realm&, Database&);
|
||||
|
|
|
|||
|
|
@ -18,6 +18,9 @@ void RequestList::enqueue(GC::Ref<IDBRequest> request, GC::Ref<GC::Function<void
|
|||
|
||||
void RequestList::maybe_process_next_request()
|
||||
{
|
||||
if (m_blocked)
|
||||
return;
|
||||
|
||||
while (!m_entries.is_empty() && !m_entries.first().request)
|
||||
m_entries.remove(0);
|
||||
|
||||
|
|
@ -76,6 +79,12 @@ void RequestList::check_all_processed()
|
|||
callback->function()();
|
||||
}
|
||||
|
||||
void RequestList::unblock_execution()
|
||||
{
|
||||
m_blocked = false;
|
||||
maybe_process_next_request();
|
||||
}
|
||||
|
||||
GC::Ref<IDBRequest> RequestList::RequestIterator::operator*() const
|
||||
{
|
||||
return *m_entries[m_index].request;
|
||||
|
|
|
|||
|
|
@ -27,8 +27,12 @@ public:
|
|||
|
||||
bool is_empty() const;
|
||||
|
||||
void block_execution() { m_blocked = true; }
|
||||
void unblock_execution();
|
||||
|
||||
void set_on_all_processed(GC::Ref<GC::Function<void()>> callback);
|
||||
void check_all_processed();
|
||||
void maybe_process_next_request();
|
||||
|
||||
private:
|
||||
struct Entry {
|
||||
|
|
@ -36,10 +40,9 @@ private:
|
|||
GC::Root<GC::Function<void()>> steps;
|
||||
};
|
||||
|
||||
void maybe_process_next_request();
|
||||
|
||||
Vector<Entry> m_entries;
|
||||
GC::Root<GC::Function<void()>> m_on_all_processed;
|
||||
bool m_blocked { false };
|
||||
|
||||
public:
|
||||
class RequestIterator {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
txn1 complete
|
||||
rw+rw: val=2
|
||||
txn2 complete
|
||||
rw+ro: val=3
|
||||
txn3 complete
|
||||
ro+rw: val=4
|
||||
txn4 complete
|
||||
|
|
@ -2,7 +2,6 @@ Harness status: OK
|
|||
|
||||
Found 2 tests
|
||||
|
||||
1 Pass
|
||||
1 Fail
|
||||
2 Pass
|
||||
Pass Reverse cursor sees update from separate transactions.
|
||||
Fail Reverse cursor sees in-transaction update.
|
||||
Pass Reverse cursor sees in-transaction update.
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
<!DOCTYPE html>
|
||||
<script src="include.js"></script>
|
||||
<script>
|
||||
asyncTest(done => {
|
||||
setTimeout(() => {
|
||||
spoofCurrentURL("https://example.com/txn-scheduling.html");
|
||||
|
||||
const dbName = "test-txn-scheduling";
|
||||
const delReq = indexedDB.deleteDatabase(dbName);
|
||||
delReq.onsuccess = function() {
|
||||
const openReq = indexedDB.open(dbName, 1);
|
||||
openReq.onupgradeneeded = function(e) {
|
||||
e.target.result.createObjectStore('store', {keyPath: 'key'});
|
||||
};
|
||||
openReq.onsuccess = function(e) {
|
||||
const db = e.target.result;
|
||||
let log = [];
|
||||
const print = (msg) => { log.push(msg); };
|
||||
|
||||
// Case 1: readwrite then readwrite (overlapping scope)
|
||||
// txn2 must not start until txn1 finishes.
|
||||
const txn1 = db.transaction('store', 'readwrite');
|
||||
txn1.objectStore('store').put({key: 'a', val: 1});
|
||||
txn1.objectStore('store').put({key: 'a', val: 2});
|
||||
|
||||
const txn2 = db.transaction('store', 'readwrite');
|
||||
// This get runs AFTER txn1 commits, so it must see val:2.
|
||||
const getReq = txn2.objectStore('store').get('a');
|
||||
getReq.onsuccess = () => print("rw+rw: val=" + getReq.result.val);
|
||||
txn2.objectStore('store').put({key: 'a', val: 3});
|
||||
|
||||
// Case 2: readwrite then readonly (overlapping scope)
|
||||
// txn3 must not start until txn2 finishes.
|
||||
const txn3 = db.transaction('store', 'readonly');
|
||||
const getReq2 = txn3.objectStore('store').get('a');
|
||||
getReq2.onsuccess = () => print("rw+ro: val=" + getReq2.result.val);
|
||||
|
||||
// Case 3: readonly then readwrite (overlapping scope)
|
||||
// txn4 must not start until txn3 finishes (though txn3 is readonly,
|
||||
// a subsequent readwrite cannot overlap with it).
|
||||
const txn4 = db.transaction('store', 'readwrite');
|
||||
txn4.objectStore('store').put({key: 'a', val: 4});
|
||||
const getReq3 = txn4.objectStore('store').get('a');
|
||||
getReq3.onsuccess = () => print("ro+rw: val=" + getReq3.result.val);
|
||||
|
||||
txn1.oncomplete = () => print("txn1 complete");
|
||||
txn2.oncomplete = () => print("txn2 complete");
|
||||
txn3.oncomplete = () => print("txn3 complete");
|
||||
txn4.oncomplete = () => {
|
||||
print("txn4 complete");
|
||||
for (const msg of log)
|
||||
println(msg);
|
||||
db.close();
|
||||
indexedDB.deleteDatabase(dbName);
|
||||
done();
|
||||
};
|
||||
};
|
||||
};
|
||||
}, 0);
|
||||
});
|
||||
</script>
|
||||
Loading…
Reference in a new issue