Skip to content

Fix for AsyncDatabase and parallel transactions #888 ?#889

Merged
mariusconjeaud merged 8 commits intoneo4j-contrib:rc/6.0.0from
DenesPal:fix/888-async-database-transactions
Sep 25, 2025
Merged

Fix for AsyncDatabase and parallel transactions #888 ?#889
mariusconjeaud merged 8 commits intoneo4j-contrib:rc/6.0.0from
DenesPal:fix/888-async-database-transactions

Conversation

@DenesPal
Copy link
Copy Markdown
Contributor

@DenesPal DenesPal commented Aug 26, 2025

async_.core.AsyncDatabase was incorrectly subclassed from threading.local, that did not make it's properties context-local, leaking state (like session and transaction states) across async contexts.

I am assuming that sync_.core.Database implementation is correct.
Apart from the two class-level global mappings (_NODE_CLASS_REGISTRY and _DB_SPECIFIC_CLASS_REGISTRY), all instance properties of Database are threading-local.
So I've converted the same instance properties of AsyncDatabase to ContextVar @property-s (because ContextVars work with .get & .set).

@DenesPal DenesPal force-pushed the fix/888-async-database-transactions branch from d2f39ab to f83d85b Compare August 27, 2025 14:27
Following the pattern of neomodel.sync_.core.Database where all instance properties are threading-local (except global mappings _NODE_CLASS_REGISTRY and _DB_SPECIFIC_CLASS_REGISTRY), I've converted the same properties of AsyncDatabase to ContextVars (to be context-local instance properties).
@DenesPal DenesPal force-pushed the fix/888-async-database-transactions branch from f83d85b to 11ab178 Compare August 27, 2025 14:38
@DenesPal
Copy link
Copy Markdown
Contributor Author

I'm wondering if all properties of AsyncDatabase should be context-aware?
pid would be the same for all contexts as the process does not change. (Same applies for thread-local Database.pid)
I'm wondering if it is practical changing the database driver/URL in an async context, expecting that separate contexts carry on with different connection. If not then url, driver, _database_name, _database_version, _database_edition could also be normal instance properties.

@robsdedude
Copy link
Copy Markdown
Contributor

robsdedude commented Sep 11, 2025

Disclaimer: I'm not a maintainer of neomodel, but I've kindly been asked for input. So here goes:

I'm wondering if all properties of AsyncDatabase should be context-aware?

From a general maintenance perspective I suggest to keep the async and sync implementations as close as possible from a mental perspective. So at least for this PR, I suggest to turn everything that's thread local in sync into context local in async. The PR could (optional) even go a step further and also use ContextVars for the sync implementation because according to the Python API docs each thread gets its own context. So that should be equivalent. Actually, if I remember correctly, the sync code is auto-generated/transpiled from the async code (see /bin/make-unasync). Feels like the CI should've complained about this, but I guess there's no job setup for such check 🙃

Another thing to keep in mind is that async Python has some gotchas/rough edges. What do you think this snippet does?

import asyncio
from contextvars import ContextVar


var = ContextVar('var', default='default')


def print_var():
    print(f'var: {var.get()}')


async def task():
    print("task:")
    var.set('task')
    print_var()


async def main():
    print("main:")
    print_var()
    await asyncio.wait_for(task(), timeout=1)
    print("main:")
    print_var()


if __name__ == '__main__':
    asyncio.run(main())

Does tasks var.set affect what main sees?

This is a trick question... Until Python 3.11 it doesn't, because asyncio.wait_for in those older versions spawns an internal task, while in newer Python implementations it doesn't. In threading terms this would be equivalent to some innocent looking standard lib calls spawning a thread under the hood. I frankly don't know if there are other asyncio functions that do this. That's detrimental. Imagine some code like this:

async def save_node():
        node = TestNode(name="Cool node")
        await node.save()

async def main():
    async with adb.transaction:
        # don't want to wait for ever to create the node
        asyncio.wait_for(save_node, timeout=1)

That reads alright and does what it looks like on the tin for newer Python version, but on older Python verions save_node will not be running inside main's transaction.

I don't think this is a blocker for this PR, but it needs consideration and probably some warning in the docs.

@mariusconjeaud mariusconjeaud linked an issue Sep 24, 2025 that may be closed by this pull request
@mariusconjeaud
Copy link
Copy Markdown
Collaborator

Actually, if I remember correctly, the sync code is auto-generated/transpiled from the async code (see /bin/make-unasync)

Indeed it is auto-transpiled, with ways to have custom code for either async or sync ; both of which mechanisms I took from the Neo4j python driver you know so well !

Based on your comments (thanks a ton), then it sounds like moving everything in AsyncDatabase to ContextVar and letting it get transpiled works. I will update the PR with the transpiling, then test it, and set it for merge on the next "major" release - in case there are breaking changes we did not catch. Is that OK @DenesPal ?

Feels like the CI should've complained about this, but I guess there's no job setup for such check 🙃

Well, what can I say ? Checking that the transpile happened is a good idea ! I have been relying too much on my pre-commit actions here.

@mariusconjeaud mariusconjeaud changed the base branch from master to rc/6.0.0 September 24, 2025 14:43
@mariusconjeaud
Copy link
Copy Markdown
Collaborator

So... Looking good for me now ; the new tests pass ; no existing tests were broken, except those 3 that look totally unrelated, and work perfectly fine in my local environment ! Gah !

(Note I finally managed to fix the Aura tests though)

@mariusconjeaud
Copy link
Copy Markdown
Collaborator

OK tests are fixed, it was an issue with a previous PR that had duplicated code which I had missed

@sonarqubecloud
Copy link
Copy Markdown

@mariusconjeaud mariusconjeaud merged commit 1c566db into neo4j-contrib:rc/6.0.0 Sep 25, 2025
27 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Concurrency issues with multiple async contexts

3 participants