-
Notifications
You must be signed in to change notification settings - Fork 559
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
useObject inside FlatList throws Exception in HostFunction: Cannot create asynchronous query while in a write transaction #4375
Comments
@mfbx9da4 Thanks for reporting. We will try to reproduce this and get back to you soon. |
@mfbx9da4 Just wanted to update, it seems the async nature of how the writes are happening are causing a bit of a race condition where the write transaction is started in the event loop, then the main thread renders |
@mfbx9da4 Been talking to other members of the team about this. We currently have an open feature request for async transactions in realm (see #1099). Currently, we cannot guarantee that |
Do you mean the main UI thread of the native platform or main JS thread? Assuming main JS thread, I still don't see how the write transaction isn't closed before anything else on the main JS thread is executed since JS is single threaded and AFIK, a write transaction callback is synchronous. Do you mean, under the hood it goes something like this
In which case why is the callback enqueued onto the event loop? Why not do it synchronously? |
Having read #1099, I can see |
As a short term workaround could you expose a promise |
I've thought of another workaround which would fix my exact issue but wouldn't attack the root cause. In the example code, the |
@mfbx9da4 we did some more digging. When the second write transaction starts, any event listeners that haven't finished yet are given time to complete their tasks. In this case, the event listener in We are going to continue to think about a good way to solve this. As a workaround, I would recommend trying to minimise the amount of write transactions that are occurring within asynchronous functions (if there is only one transaction occurring, this seems to work just fine). We might also add a check in the Thanks for taking the time to report and comment on this issue. We will report back when we have made some progress. |
Did you ever get to the bottom of why this only happens in conjunction with FlatList? Or have you been able to reproduce without FlatList? |
@mfbx9da4 It's not related to anything specific with FlatList. You will get the same crash if you replace
It's more to do with re-rendering in conjunction with an async function. |
Ah okay I wasn't able to reproduce with a simple .map(). Good to know, more serious than I thought. |
One other workaround would be not to use |
Yeah fundamentally what is happening is something like:
Whereas with e.g. So it's a general bug, but exacerbated by the way Realm React works (adding/removing listeners frequently). The plan to fix is it to queue up any "add listener" calls that occur while a transaction is in progress, then drain the queue when that transaction ends. Thanks for the great bug reports @mfbx9da4, they're really appreciated! |
My pleasure! Is there an event one could subscribe to when the active transaction finishes? |
I have tried to recreate the issue once again without FlatList but to no avail. @takameyer I tried your code sample but never get the error. Try uncommenting this line and commenting out the FlatList just beneath, you'll find the error no longer appears. @tomduncalf I think a slightly more accurate sequence of events would be:
The sequence of events isn't too important as either way it's clear there is a race condition where listeners can be added inside a transaction. And the same listener error can be observed with What I don’t understand is how any other JS code can run in between the transaction starting and ending. The only way I see this being possible is if realm.write does look something like this but I don't think it does 🤔. realm.write = (callback) => {
beginTransaction()
setTimeout(() => {
callback()
endTransaction()
})
} If that's the case, why not make |
Also I put together a quick solution which does properly wait until the active transaction is finished: function useObject<T>(type: string, primaryKey: string): (T & Realm.Object) | undefined {
const [object, setObject] = useState<(T & Realm.Object) | undefined>(
realm.objectForPrimaryKey(type, primaryKey)
)
useEffect(() => {
let isMounted = true
const listenerCallback: Realm.ObjectChangeCallback = (_, changes) => {
if (changes.changedProperties.length > 0) {
setObject(realm.objectForPrimaryKey(type, primaryKey))
} else if (changes.deleted) {
setObject(undefined)
}
}
if (object !== undefined) {
waitForNoActiveTransaction()
.then(() => {
if (isMounted) object.addListener(listenerCallback)
})
.catch(error => console.error('Failed to add listener', error))
}
return () => {
object?.removeListener(listenerCallback)
isMounted = false
}
}, [object, type, primaryKey])
return object
}
function waitForNoActiveTransaction() {
return pollFor(() => !realm.isInTransaction, { attempts: 100, interval: 10 })
} It would, of course, be much better if there was an event I could subscribe to when the transaction finishes. |
Is there any solution for this? |
@Acetyld you should be able to drop in replace That said, it's more of a workaround until there is an official fix from the core team. |
Make use of setImmediate to ensure event listeners are added after write transactions have completed. Update useQueryHook tests to give the event loop time to finish and add a listener. Fix a test in useObject render to allow event listners to fire after writes.
Make use of setImmediate to ensure event listeners are added after write transactions have completed. Update useQueryHook tests to give the event loop time to finish and add a listener. Fix a test in useObject render to allow event listners to fire after writes.
@mfbx9da4 A fix is now in review. Sorry for the delay, we were looking over various ways to solve this and finally landed on the correct solution. |
Awesome @takameyer, reading that description I was finally able to reproduce in a less convoluted way const id = 'foo'
const fooCol = realm.objects<Foo>('foo')
fooCol.addListener(() => {
// This will be triggered inside a write transaction
fooCol[0]?.addListener(() => {
console.log('hey ho')
})
})
realm.write(() => {
realm.create<Foo>('foo', { id }, Realm.UpdateMode.Modified)
})
await sleep(0)
realm.write(() => {
realm.create<Foo>('foo', { id }, Realm.UpdateMode.Modified)
})
So the real issue here is that event listeners callbacks are invoked inside a write transaction. If those event listener callbacks in turn also create listeners we get the error "Cannot create asynchronous query while in a write transaction". By enqueuing the in-turn-listener-creators onto the event loop with Isn't the real fix though, to just flush the listener callbacks before starting the write transaction? Or am I missing something? Assuming the Side note, it's a shame realm fires listeners even though the row hasn't changed. |
We looked into this, but the listeners could potentially start another write transaction, and we could be caught in an endless loop of write transactions.
I don't think it's a good idea to do this all the time, since this is classically synchronous. If you are writing code outside of the hook, then it is possible to implement |
This nested addListener issue is so edge case you can't reasonably expect users to expect the issue to occur. Let alone expecting them to workout that I would also argue that fundamentally addListener only has asynchronous use cases. I agree it makes sense to skip |
Fix for the #4375 Add failing test for listener issue Make use of setImmediate to ensure event listeners are added after write transactions have completed. Update useQueryHook tests to give the event loop time to finish and add a listener. Fix a test in useObject render to allow event listners to fire after writes. Only use `setImmediate` if realm is in a write transaction. Update comments to be more clear.
@mfbx9da4 I see your point, but it would be a breaking change. There are non-react users of this code that will possibly run into issues if we change this at the core level. In any case, this is fixed for |
How frequently does the bug occur?
All the time
Description
Calling
useObject
inside a flat list item component can throw the Exception "Cannot create asynchronous query while in a write transaction" if arealm.write()
occurs around the same time.My guess cause is that the FlatList is doing some multithreading work to do the layouting of the item. As you will see in my example, the offending writes do not call addListener inside the transaction.
Stacktrace & log output
Can you reproduce the bug?
Yes, always
Reproduction Steps
Version
"realm": "^10.20.0-beta.1",
What SDK flavour are you using?
Local Database only
Are you using encryption?
No, not using encryption
Platform OS and version(s)
ios
Build environment
No response
Cocoapods version
The text was updated successfully, but these errors were encountered: