rexie
rexie copied to clipboard
TransactionInactiveError
I have code that tries to read/modify/write entries in a table. It basically looks like this:
async fn modify(ids: &[Uuid]) {
let transaction = database.transaction(&["foo"], TransactionMode::ReadWrite)?;
let table = transaction.store("foo")?;
for id in ids {
let obj = table.get(&id.to_string()).await?;
// modify object here
table.put(&obj, None).await?;
}
transaction.done().await?;
}
This code results in the error failed to execute indexed db request: TransactionInactiveError: Failed to execute 'put' on 'IDBObjectStore': The transaction is not active.
My research (source) indicates that the transaction is inactive right when control is given back to the JS event loop and active again only in the callback of the IndexedDB operation (the get
in this case).
This is inherently incompatible with the way async/await works in Rust, since you don't get control to exactly when the Future is polled (only that it won't happen before the operation finishes).
I don't quite get how others get this to work, since this doesn't seem to be such an uncommon operation to do, and I can't find any hints of problems reported by others over this?
This was tested on Chromium 108.
I've now moved to using the idb-sys
crate that doesn't use async, and there it works fine. The code is just way more convoluted, unfortunately.
I've now moved to using the
idb-sys
crate that doesn't use async, and there it works fine. The code is just way more convoluted, unfortunately.
Thanks for creating this issue. Can you also try with idb
crate which uses async/await
syntax?
I've added tests in idb
crate to test this and it seems to work: https://github.com/devashishdxt/idb/blob/750d6d3a3e4c2e0a0e0c821c853e5e8b1db8d0c0/idb/tests/transaction.rs#L167
I suspect that if there's no Future to process other than the idb one, it's always going to bounce to that one when the transaction is active again, making this impossible to test in isolation. You'd have to schedule something else at the same time that's not the same IndexedDB transaction.
I've looked through the dexie code to see how they're doing it, and it appears that they've implemented their own transactions internally that are just a queue that gets filled up and then flushed to the database all at once once it's done.
Can you provide an example of what you're trying to do in // modify object here
?
I'm recording the last access date, so I'm updating the timestamp there.
It is not possible for me to debug this without a reproducible example.
I'll try to come up with something.
I ran int the same issue and was able to reproduce the problem in your idb
test suite by adding some sleeps:
panicked at 'id should be ok: failed to add a value: TransactionInactiveError: Failed to execute 'add' on 'IDBObjectStore': The transaction has finished.', idb/tests/transaction.rs:199:9
If anyone else in this thread found a solution since then please lmk, I'm contemplating building my own transaction system on top :scream:
@elsirion Thanks for sharing an example. IndexedDB transaction work a little different as compared to transactions in other databases. Any transaction with no outstanding request gets committed when the control of event loop is given back to the browser. In your example, sleep
returns the control of event loop to the browser and as there aren't any more pending requests on transaction, it is auto-committed.
If you want long-lived transaction across the event-loop control boundaries, you'll have to add some logic to do that yourself.
Making a network call is another example of surrendering the control of event loop to the browser. So, the transaction may get auto-committed if you make a network call.
Here are some notes on IndexedDB's transaction lifecycle: https://www.w3.org/TR/IndexedDB/#transaction-lifecycle
Here's a potential approach that may be used to handle this: https://jlongster.com/future-sql-web (refer to Long-lived IndexedDB transactions
section in this article).
Yeah, it's not a bug in this crate, just insane semantics of IndexedDB transactions :cry:
My best bet is probably to build an ACID-compliant transaction abstraction on top and then only use the IndexedDB transactions at commit time so that there are no interruptions in execution.
@elsirion that's what Dexie is doing.
This ticket is about Rust Futures being inherently incompatible with indexeddb API. It's not possible to correctly convert from the transactions in indexeddb to Futures.
This ticket is about Rust Futures being inherently incompatible with indexeddb API.
I agree, given the lack of guarantees about rust future execution and the IndexedDB behavior I don't really see how these can be made to work together reliably. It might work by accident most of the time or for limited use cases where rexie futures always return Ready
on first poll and never yield to the executor, but I'd be very uncomfortable building a bigger application on top.
That's why I think the two problems are related. Having a proper transaction implementation on top of idb-sys
in Rexie would solve the problem and is likely the only way to do so. Would such a patch be welcome @devashishdxt?
@elsirion Sure. You can try adding a transaction implementation (like other JS libraries do) on top. I think this'll greatly increase usability of indexedDB transaction in wasm.