Skip to content

Parallel fixture setup/teardown violates contextvar stack discipline #137

Open
@oremanj

Description

@oremanj

Fixtures are mostly encapsulated each in their own task, so the order in which they run doesn't matter much as long as we respect the dependency graph. But there's an important exception to this "mostly": fixtures and the test all share a contextvars context, so if two different fixtures each modify a contextvar in a way that expects stack discipline to be respected, surprising things can happen.

I ran into this with a test that did:

@pytest.fixture
async def trio_asyncio_loop():
    async with trio_asyncio.open_loop() as loop:
        yield loop

@pytest.fixture
async def some_service():
    async with trio_asyncio.open_loop() as loop:
        task = loop.create_task(<something>)
        yield
        task.cancel()
        await trio_asyncio.run_aio_future(task)
        await trio_asyncio.aio_as_trio(some_aio_cleanup)()

async def test_something(trio_asyncio_loop, some_service):
    ...

(A more correct solution would be to make some_service request the trio_asyncio_loop fixture instead of opening another loop of its own, but imagine that the fixtures came from different places and the author of test_something wasn't thinking about the fact that some_service used a trio-asyncio loop.)

If we ran fixture setup and teardown in the same order that a synchronous pytest would, there's no problem here; the some_service trio-asyncio loop is properly nested within the trio_asyncio_loop one, and teardown works fine. But since setups and teardowns can execute in parallel, and Trio task scheduling order is nondeterministic, it's possible to get the order:

  • setup trio_asyncio_loop
  • setup some_service
  • run test
  • teardown trio_asyncio_loop
  • teardown some_service

This causes the trio-asyncio loop contextvar to be set to None (the value that it had upon entry to trio_asyncio_loop) while some_service is still relying on having a loop available. Various exceptions result as asyncio.get_event_loop() starts returning None.

I think the right fix to this is to stop running fixtures in parallel; we can run them in the same order that pytest would if they were synchronous instead. It doesn't seem like the parallelism is buying us much, and it can create confusing outcomes in situations like this.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions