CBL C SDK CBLQuery_AddChangeListener deadlocks when called in transaction after CBLDatabase_BeginTransaction

I’ve run into a bug with an extension on my Kotlin Multiplatform Couchbase Lite library where I add a query change listener within a transaction. This code works on Android and JVM using the Java SDK and iOS and macOS using the Objective-C SDK, but not on Linux and Windows using the native C SDK. The C SDK deadlocks and hangs when calling CBLQuery_AddChangeListener after calling CBLDatabase_BeginTransaction. This is with Couchbase Lite version 3.0.12.

This would be better off as a bug report on the C repo, but I’d chalk it up to “thing number 627 that you are not allowed to do while holding a transaction open.” Basically, you should only be doing read and write operations inside of a transaction and we are thinking about how to best express this restriction in C. I would expect the same result in other languages but perhaps those SDKs are doing something asynchronously, which is a lot more of a pain in C / C++. Right now the leading idea is for us to immediately error out if we can detect this situation, so just a forewarning I wouldn’t expect this behavior to change.

Thanks. I created an issue in the C SDK repo.

It does look like this works in the other SDKs because it posts the live query to start on the executor or queue the callbacks are registered on.

My use case is for performing a query within a transaction and observing that same query for future changes. Pseudocode example:

transaction {
    count = performCountQuery()
    query = limitOffsetPageQuery(count)
    query.addChangeListener() <-- not allowed
    results = query.execute()
return results

I can workaround by saving the query reference and adding the listener outside the transaction. I think ideally the listener begins listening before the first execute()'s results in order to ensure the listener will receive all updates since that execution. There’s technically a window another thread could modify the database once the transaction is closed before the listener is registered.

The other thing I don’t like about my current implementation is that it gets the results twice, first from the execute() and again from the live query listener, which it ignores the first call to. So I’m looking to rework it to use the live query’s results for the initial results. When the live query listener gets called after a database change, I have to invalidate the entire query because I need to perform a fresh count query to get a potentially new offset anyway. This is complicated by the results coming in the live query’s callback though.

We really should have a list of things you can do while holding a transaction open. It is a shorter list than the inverse…

1 Like

Such a list would be great! Additionally, I would also appreciate erroring out as described by @borrrden!