pytest-dev / pytest-xdist

pytest plugin for distributed testing and loop-on-failures testing modes.
https://pytest-xdist.readthedocs.io
MIT License
1.49k stars 232 forks source link

pytest hangs indefinitely after completing tests in parallel #60

Open justinlovinger opened 8 years ago

justinlovinger commented 8 years ago

I run into this behavior when running web tests with Google App Engine. However, since this issue only occurs when using xdist, I do not believe it is caused by App Engine itself.

Run the following two tests in parallel. Tests will complete, but pytest will not finish:

def test_REMOVE():
    port = 9999
    admin_port = 9998
    cmd = '"{python}" "{gaepath}/dev_appserver.py" "{apppath}"' \
          ' -A test-{port} --port={port} --admin_port={adminport}' \
          ' --datastore_path=C:/tmp/test_datastore_{port}' \
          ' --clear_datastore=yes'.format(python=sys.executable,
                                          gaepath=GAE_PATH,
                                          apppath=HELLO_PATH,
                                          port=port, adminport=admin_port)
    app_engine = subprocess.Popen(cmd)

    time.sleep(8)

    app_engine.terminate()

def test_REMOVE_2():
    port = 10001
    admin_port = 10000
    cmd = '"{python}" "{gaepath}/dev_appserver.py" "{apppath}"' \
          ' -A test-{port} --port={port} --admin_port={adminport}' \
          ' --datastore_path=C:/tmp/test_datastore_{port}' \
          ' --clear_datastore=yes'.format(python=sys.executable,
                                          gaepath=GAE_PATH,
                                          apppath=HELLO_PATH,
                                          port=port, adminport=admin_port)
    app_engine = subprocess.Popen(cmd)

    time.sleep(8)

    app_engine.terminate()

I ran the tests with the following line:

py.test -k REMOVE -n 2

The result is:

============================= test session starts =============================
platform win32 -- Python 2.7.11, pytest-2.9.1, py-1.4.31, pluggy-0.3.1
rootdir: ..., inifile:
plugins: cov-2.2.1, xdist-1.14
gw0 [2] / gw1 [2]
scheduling tests via LoadScheduling
..

Killing the process results in the following traceback:

Traceback (most recent call last):
  File "c:\python27\lib\runpy.py", line 162, in _run_module_as_main
    "__main__", fname, loader, pkg_name)
  File "c:\python27\lib\runpy.py", line 72, in _run_code
    exec code in run_globals
  File "C:\Python27\Scripts\py.test.exe\__main__.py", line 9, in <module>
  File "c:\python27\lib\site-packages\_pytest\config.py", line 49, in main
    return config.hook.pytest_cmdline_main(config=config)
  File "c:\python27\lib\site-packages\_pytest\vendored_packages\pluggy.py", line 724, in __call__
    return self._hookexec(self, self._nonwrappers + self._wrappers, kwargs)
  File "c:\python27\lib\site-packages\_pytest\vendored_packages\pluggy.py", line 338, in _hookexec
    return self._inner_hookexec(hook, methods, kwargs)
  File "c:\python27\lib\site-packages\_pytest\vendored_packages\pluggy.py", line 333, in <lambda>
    _MultiCall(methods, kwargs, hook.spec_opts).execute()
  File "c:\python27\lib\site-packages\_pytest\vendored_packages\pluggy.py", line 596, in execute
    res = hook_impl.function(*args)
  File "c:\python27\lib\site-packages\_pytest\main.py", line 119, in pytest_cmdline_main
    return wrap_session(config, _main)
  File "c:\python27\lib\site-packages\_pytest\main.py", line 114, in wrap_session
    exitstatus=session.exitstatus)
  File "c:\python27\lib\site-packages\_pytest\vendored_packages\pluggy.py", line 724, in __call__
    return self._hookexec(self, self._nonwrappers + self._wrappers, kwargs)
  File "c:\python27\lib\site-packages\_pytest\vendored_packages\pluggy.py", line 338, in _hookexec
    return self._inner_hookexec(hook, methods, kwargs)
  File "c:\python27\lib\site-packages\_pytest\vendored_packages\pluggy.py", line 333, in <lambda>
    _MultiCall(methods, kwargs, hook.spec_opts).execute()
  File "c:\python27\lib\site-packages\_pytest\vendored_packages\pluggy.py", line 595, in execute
    return _wrapped_call(hook_impl.function(*args), self.execute)
  File "c:\python27\lib\site-packages\_pytest\vendored_packages\pluggy.py", line 249, in _wrapped_call
    wrap_controller.send(call_outcome)
  File "c:\python27\lib\site-packages\_pytest\terminal.py", line 363, in pytest_sessionfinish
    outcome.get_result()
  File "c:\python27\lib\site-packages\_pytest\vendored_packages\pluggy.py", line 279, in get_result
    _reraise(*ex)  # noqa
  File "c:\python27\lib\site-packages\_pytest\vendored_packages\pluggy.py", line 264, in __init__
    self.result = func()
  File "c:\python27\lib\site-packages\_pytest\vendored_packages\pluggy.py", line 596, in execute
    res = hook_impl.function(*args)
  File "c:\python27\lib\site-packages\xdist\dsession.py", line 517, in pytest_sessionfinish
    nm.teardown_nodes()
  File "c:\python27\lib\site-packages\xdist\slavemanage.py", line 62, in teardown_nodes
    self.group.terminate(self.EXIT_TIMEOUT)
  File "c:\python27\lib\site-packages\execnet\multi.py", line 208, in terminate
    for gw in self._gateways_to_join])
  File "c:\python27\lib\site-packages\execnet\multi.py", line 297, in safe_terminate
    workerpool.waitall()
  File "c:\python27\lib\site-packages\execnet\gateway_base.py", line 325, in waitall
    return my_waitall_event.wait(timeout=timeout)
  File "c:\python27\lib\threading.py", line 614, in wait
    self.__cond.wait(timeout)
  File "c:\python27\lib\threading.py", line 340, in wait
    waiter.acquire()
KeyboardInterrupt

Finally, I am on windows.

Note that this test is run with the hello world application given by Google. Any other app engine application I've tested causes the same hang.

Also, I have tried a Popen that simply runs python, and this does not result in the same hang.

RonnyPfannschmidt commented 8 years ago

does it happen on linux as well?

nicoddemus commented 8 years ago

It seems there's a thread running in background preventing the nodes created by xdist to terminate properly...

Also I notice you are trying to forcefully kill the AppEngine process you started... isn't there a cleaner way to do the same, using an API perhaps?

nicoddemus commented 8 years ago

Also, I would suggest taking a look at pytest-beds: Fixtures for testing Google Appengine (GAE) apps.

justinlovinger commented 8 years ago

According to the App Engine docs, "To stop the local server: with Mac OS X or Unix, press Control-C or with Windows, press Control-Break in your command prompt window." (https://cloud.google.com/appengine/docs/python/tools/using-local-server).

Modifying the above code to send the Control-Break signal:

def test_REMOVE():
    port = 9999
    admin_port = 9998
    cmd = '"{python}" "{gaepath}/dev_appserver.py" "{apppath}"' \
          ' -A test-{port} --port={port} --admin_port={adminport}' \
          ' --datastore_path=C:/tmp/test_datastore_{port}' \
          ' --clear_datastore=yes'.format(python=sys.executable,
                                          gaepath=GAE_PATH,
                                          apppath=HELLO_PATH,
                                          port=port, adminport=admin_port)
    app_engine = subprocess.Popen(cmd)

    time.sleep(8)

    app_engine.send_signal(signal.CTRL_BREAK_EVENT) # New Code

def test_REMOVE_2():
    port = 10001
    admin_port = 10000
    cmd = '"{python}" "{gaepath}/dev_appserver.py" "{apppath}"' \
          ' -A test-{port} --port={port} --admin_port={adminport}' \
          ' --datastore_path=C:/tmp/test_datastore_{port}' \
          ' --clear_datastore=yes'.format(python=sys.executable,
                                          gaepath=GAE_PATH,
                                          apppath=HELLO_PATH,
                                          port=port, adminport=admin_port)
    app_engine = subprocess.Popen(cmd)

    time.sleep(8)

    app_engine.send_signal(signal.CTRL_BREAK_EVENT) # New Code

Now actually terminates pytest:

============================= test session starts =============================
platform win32 -- Python 2.7.11, pytest-2.9.1, py-1.4.31, pluggy-0.3.1
rootdir: ..., inifile:
plugins: cov-2.2.1, xdist-1.14
collected 2 items

test_xdist.py .^C

This occurs whether xdist is used or not.

P.S. It looks like pytest-beds only has fixtures for the testbed, which is not used for end to end tests that require the actual server. Also, I make my own fixtures for the actual tests. The above code is just for demonstration and xdist testing.

P.P.S. I will take a look at Linux when I get a change to setup App Engine on my Linux machine. I suspect the --boxed option may fix this, but is not available on windows.

RonnyPfannschmidt commented 8 years ago

the --boxed option is going to go away

nicoddemus commented 8 years ago

@PaintingInAir Sorry, it wasn't clear to me: did send the signal using send_signal fix the issue?

justinlovinger commented 8 years ago

@nicoddemus send_signal results in a new issue. Specifically, it kills the parent process as well as the child process. Some investigation indicates that this is a bug with Python itself.

There appears to be a workaround for the send_signal bug, which involves creating a stand alone process (not a child) and using inter-process communication to send the kill command. However, with this bug, it's impossible to tell if sending the proper Control-Break command to the App Engine process will fix the issue with xdist.

Regardless, it appears the underlying issue is a loose thread that causes xdist to hang. I guess the first question is whether or not xdist should be able to handle this situation. Note that pytest can finish under these circumstances, when xdist is not used. If this is a bug worth fixing, it is probably worth isolating the underlying cause (such as a loose thread) first.

ghost commented 7 years ago

This is happening on AppVeyor.

INTERNALERROR> Traceback (most recent call last):
INTERNALERROR>   File "<string>", line 984, in _send
INTERNALERROR>   File "<string>", line 430, in to_io
INTERNALERROR>   File "<string>", line 396, in write
INTERNALERROR> OSError: [Errno 22] Invalid argument
INTERNALERROR> 
INTERNALERROR> During handling of the above exception, another exception occurred:
INTERNALERROR> 
INTERNALERROR> Traceback (most recent call last):
INTERNALERROR>   File "c:\python36-x64\lib\site-packages\_pytest\main.py", line 98, in wrap_session
INTERNALERROR>     session.exitstatus = doit(config, session) or 0
INTERNALERROR>   File "c:\python36-x64\lib\site-packages\_pytest\main.py", line 133, in _main
INTERNALERROR>     config.hook.pytest_runtestloop(session=session)
INTERNALERROR>   File "c:\python36-x64\lib\site-packages\_pytest\vendored_packages\pluggy.py", line 745, in __call__
INTERNALERROR>     return self._hookexec(self, self._nonwrappers + self._wrappers, kwargs)
INTERNALERROR>   File "c:\python36-x64\lib\site-packages\_pytest\vendored_packages\pluggy.py", line 339, in _hookexec
INTERNALERROR>     return self._inner_hookexec(hook, methods, kwargs)
INTERNALERROR>   File "c:\python36-x64\lib\site-packages\_pytest\vendored_packages\pluggy.py", line 334, in <lambda>
INTERNALERROR>     _MultiCall(methods, kwargs, hook.spec_opts).execute()
INTERNALERROR>   File "c:\python36-x64\lib\site-packages\_pytest\vendored_packages\pluggy.py", line 614, in execute
INTERNALERROR>     res = hook_impl.function(*args)
INTERNALERROR>   File "<remote exec>", line 61, in pytest_runtestloop
INTERNALERROR>   File "<remote exec>", line 77, in run_tests
INTERNALERROR>   File "c:\python36-x64\lib\site-packages\_pytest\vendored_packages\pluggy.py", line 745, in __call__
INTERNALERROR>     return self._hookexec(self, self._nonwrappers + self._wrappers, kwargs)
INTERNALERROR>   File "c:\python36-x64\lib\site-packages\_pytest\vendored_packages\pluggy.py", line 339, in _hookexec
INTERNALERROR>     return self._inner_hookexec(hook, methods, kwargs)
INTERNALERROR>   File "c:\python36-x64\lib\site-packages\_pytest\vendored_packages\pluggy.py", line 334, in <lambda>
INTERNALERROR>     _MultiCall(methods, kwargs, hook.spec_opts).execute()
INTERNALERROR>   File "c:\python36-x64\lib\site-packages\_pytest\vendored_packages\pluggy.py", line 613, in execute
INTERNALERROR>     return _wrapped_call(hook_impl.function(*args), self.execute)
INTERNALERROR>   File "c:\python36-x64\lib\site-packages\_pytest\vendored_packages\pluggy.py", line 254, in _wrapped_call
INTERNALERROR>     return call_outcome.get_result()
INTERNALERROR>   File "c:\python36-x64\lib\site-packages\_pytest\vendored_packages\pluggy.py", line 279, in get_result
INTERNALERROR>     raise ex[1].with_traceback(ex[2])
INTERNALERROR>   File "c:\python36-x64\lib\site-packages\_pytest\vendored_packages\pluggy.py", line 265, in __init__
INTERNALERROR>     self.result = func()
INTERNALERROR>   File "c:\python36-x64\lib\site-packages\_pytest\vendored_packages\pluggy.py", line 613, in execute
INTERNALERROR>     return _wrapped_call(hook_impl.function(*args), self.execute)
INTERNALERROR>   File "c:\python36-x64\lib\site-packages\_pytest\vendored_packages\pluggy.py", line 254, in _wrapped_call
INTERNALERROR>     return call_outcome.get_result()
INTERNALERROR>   File "c:\python36-x64\lib\site-packages\_pytest\vendored_packages\pluggy.py", line 279, in get_result
INTERNALERROR>     raise ex[1].with_traceback(ex[2])
INTERNALERROR>   File "c:\python36-x64\lib\site-packages\_pytest\vendored_packages\pluggy.py", line 265, in __init__
INTERNALERROR>     self.result = func()
INTERNALERROR>   File "c:\python36-x64\lib\site-packages\_pytest\vendored_packages\pluggy.py", line 614, in execute
INTERNALERROR>     res = hook_impl.function(*args)
INTERNALERROR>   File "c:\python36-x64\lib\site-packages\_pytest\runner.py", line 66, in pytest_runtest_protocol
INTERNALERROR>     runtestprotocol(item, nextitem=nextitem)
INTERNALERROR>   File "c:\python36-x64\lib\site-packages\_pytest\runner.py", line 79, in runtestprotocol
INTERNALERROR>     reports.append(call_and_report(item, "call", log))
INTERNALERROR>   File "c:\python36-x64\lib\site-packages\_pytest\runner.py", line 137, in call_and_report
INTERNALERROR>     hook.pytest_runtest_logreport(report=report)
INTERNALERROR>   File "c:\python36-x64\lib\site-packages\_pytest\vendored_packages\pluggy.py", line 745, in __call__
INTERNALERROR>     return self._hookexec(self, self._nonwrappers + self._wrappers, kwargs)
INTERNALERROR>   File "c:\python36-x64\lib\site-packages\_pytest\vendored_packages\pluggy.py", line 339, in _hookexec
INTERNALERROR>     return self._inner_hookexec(hook, methods, kwargs)
INTERNALERROR>   File "c:\python36-x64\lib\site-packages\_pytest\vendored_packages\pluggy.py", line 334, in <lambda>
INTERNALERROR>     _MultiCall(methods, kwargs, hook.spec_opts).execute()
INTERNALERROR>   File "c:\python36-x64\lib\site-packages\_pytest\vendored_packages\pluggy.py", line 614, in execute
INTERNALERROR>     res = hook_impl.function(*args)
INTERNALERROR>   File "<remote exec>", line 92, in pytest_runtest_logreport
INTERNALERROR>   File "<remote exec>", line 25, in sendevent
INTERNALERROR>   File "<string>", line 717, in send
INTERNALERROR>   File "<string>", line 990, in _send
INTERNALERROR> OSError: cannot send (already closed?)
floer32 commented 6 years ago

I've experienced this as well and had to switch to --capture=no as a default in my pytest.ini, then teammates can re-enable capture if they need to.

Also: I noticed the issue less when I was using pytest-sugar plugin, I wonder if somehow that affected the capturing in a way that was less prone to this issue.

Smosker commented 5 years ago

also having this problem, does anyone fixed it?

basavaraj1985 commented 5 years ago

I am seeing the same issue on linux. Is there any fix/workaround? I am using pytest version 3.1.0.

mangeshrane commented 5 years ago

Hi, I'm also facing same issue but it occures only when tests are in classes. is there any workaround ?

nicoddemus commented 5 years ago

Hi,

Unfortunately it is hard to nail down the problem; usually it is not a bug in pytest-xdist per-se, but some bad interaction of the system under testing. Is anybody able to provide a minimal reproducible example?

rohansb commented 5 years ago

Seeing same issue while running in a Docker container; image based off of python:3.7.2-alpine3.9

here is gist with minimal reproducible example: https://gist.github.com/rohansb/3699278d4f7388c3881ced81a0fa66c9 docker build . from fooproject root

nicoddemus commented 5 years ago

@rohansb thanks, but strangely I don't even see pytest-xdist being installed in your Docker image... are you sure that reproduces the problem or is related to xdist?

rohansb commented 5 years ago

@nicoddemus that's right, does that indicate its not related to xdist? Here is sample output i generated using same code from above gist docker-build-2019-04-30 15-15-37

.. this is where it hangs indefinitely, off-course!

rohansb commented 5 years ago

FYI.. issue persists even after installing pytest-xdist docker-build-2019-04-30 17-30-24

floer32 commented 5 years ago

Just thinking --

It may be infeasible to solve for all envs all the time, it's actually a somewhat hard problem

Something that could be tried: if one cannot put capture=no overall, could tell it to capture output and logs to file and never stdout/stderr. If needed, you could use pytest hookspec to cat those tempfiles at end of session as needed

stevecj commented 5 years ago

I'm having this same problem in a different context. I'm running pytest-django tests locally with xdist, and it hangs in the same way. Based on the conversation here, I tried running them with the --capture=no option, but still had the problem.

Any suggestions on how to debug & work around this in my case?

nicoddemus commented 4 years ago

I would try to attach a remote debugger and see where the processes are getting stuck (PyCharm has a remote debugger for example).

NoReflexness commented 4 years ago

Had the same issue running pytest with xdist in a docker. Docker never terminted after test completion. Pytest on pid 1 was unkillable and no stop signal could kill it.

--boxed fixed the issues but broke scopes for fixtures resulting in a lot of setup and tear down.

--dist loadscope fixed the issue but had (acceptable in my case) impact on how the cli received log output. I pass it to reportportal anyways. ¯_(ツ)_/¯

svetlyak40wt commented 4 years ago

I've dumped stack traces (with Pyrasyte) from the hanged master process. It has many threads like this:

Thread 0x7f3c7a336700
  File "/home/art/dwh/.env3/lib/python3.7/site-packages/execnet/gateway_base.py", line 285, in _perform_spawn
    reply.run()
  File "/home/art/dwh/.env3/lib/python3.7/site-packages/execnet/gateway_base.py", line 220, in run
    self._result = func(*args, **kwargs)
  File "/home/art/dwh/.env3/lib/python3.7/site-packages/execnet/gateway_base.py", line 967, in _thread_receiver
    msg = Message.from_io(io)
  File "/home/art/dwh/.env3/lib/python3.7/site-packages/execnet/gateway_base.py", line 432, in from_io
    header = io.read(9)  # type 1, channel 4, payload 4
  File "/home/art/dwh/.env3/lib/python3.7/site-packages/execnet/gateway_base.py", line 400, in read
    data = self._read(numbytes - len(buf))

Most probably, these threads communicate with spawned subprocesses.

And there is one thread, which hangs on the Queue:

Thread 0x7f3c87c67700
  File "/home/art/.pycharm_helpers/pycharm/_jb_pytest_runner.py", line 42, in <module>
    pytest.main(args, plugins_to_load + [Plugin])
  File "/home/art/dwh/.env3/lib/python3.7/site-packages/_pytest/config/__init__.py", line 67, in main
    return config.hook.pytest_cmdline_main(config=config)
  File "/home/art/dwh/.env3/lib/python3.7/site-packages/pluggy/hooks.py", line 286, in __call__
    return self._hookexec(self, self.get_hookimpls(), kwargs)
  File "/home/art/dwh/.env3/lib/python3.7/site-packages/pluggy/manager.py", line 93, in _hookexec
    return self._inner_hookexec(hook, methods, kwargs)
  File "/home/art/dwh/.env3/lib/python3.7/site-packages/pluggy/manager.py", line 87, in <lambda>
    firstresult=hook.spec.opts.get("firstresult") if hook.spec else False,
  File "/home/art/dwh/.env3/lib/python3.7/site-packages/pluggy/callers.py", line 187, in _multicall
    res = hook_impl.function(*args)
  File "/home/art/dwh/.env3/lib/python3.7/site-packages/_pytest/main.py", line 208, in pytest_cmdline_main
    return wrap_session(config, _main)
  File "/home/art/dwh/.env3/lib/python3.7/site-packages/_pytest/main.py", line 178, in wrap_session
    session.exitstatus = doit(config, session) or 0
  File "/home/art/dwh/.env3/lib/python3.7/site-packages/_pytest/main.py", line 215, in _main
    config.hook.pytest_runtestloop(session=session)
  File "/home/art/dwh/.env3/lib/python3.7/site-packages/pluggy/hooks.py", line 286, in __call__
    return self._hookexec(self, self.get_hookimpls(), kwargs)
  File "/home/art/dwh/.env3/lib/python3.7/site-packages/pluggy/manager.py", line 93, in _hookexec
    return self._inner_hookexec(hook, methods, kwargs)
  File "/home/art/dwh/.env3/lib/python3.7/site-packages/pluggy/manager.py", line 87, in <lambda>
    firstresult=hook.spec.opts.get("firstresult") if hook.spec else False,
  File "/home/art/dwh/.env3/lib/python3.7/site-packages/pluggy/callers.py", line 187, in _multicall
    res = hook_impl.function(*args)
  File "/home/art/dwh/.env3/lib/python3.7/site-packages/xdist/dsession.py", line 115, in pytest_runtestloop
    self.loop_once()
  File "/home/art/dwh/.env3/lib/python3.7/site-packages/xdist/dsession.py", line 129, in loop_once
    eventcall = self.queue.get(timeout=2.0)
  File "/usr/lib/python3.7/queue.py", line 179, in get
    self.not_empty.wait(remaining)
  File "/usr/lib/python3.7/threading.py", line 300, in wait
    gotit = waiter.acquire(True, timeout)
RahulPalve commented 3 years ago

I am having same issue, tests works fine when tried to debug using debugger in vscode. It seems there is some problem with threads. OS: Ubuntu 20.04 Python 3.6.6

bokunodev commented 3 years ago

hi, i experienced the same problem with my fastapi app. in my case, the cause is pymysqlpool package. to solve this problem, i created mock class to replace the pymysqlpool.Pool.

gpouilloux commented 2 years ago

If it can help someone, I had the same symptom with pytest hanging

The root cause was that postgresql requires to close all sessions, otherwise there are still open locks

see: https://docs.sqlalchemy.org/en/14/faq/metadata_schema.html#my-program-is-hanging-when-i-say-table-drop-metadata-drop-all

flipbit03 commented 2 years ago

:fire: Solved here (SQLAlchemy Session and Sessionmaker + Pytest Fixture + Coverage interaction) :fire:

Here's the backstory:

The host system is a FastAPI application and we started to have the same issue with our test suite when using pytest + coverage + pytest-cov. Our lockups were tied to tests which directly needed a SQLAlchemy Session object (from a test fixture), for things like direct manipulation of database objects, instead of going thru the FastAPI calls.

We have a generalized way of grabbing a Session object, from a global Sessionmaker which is also tied to a global Engine

session_factory: Callable[[], Session] = sessionmaker(
    autocommit=False, autoflush=True, bind=dors_engine
)

def get_session() -> Iterator[Session]:
    db_session: Session = session_factory()

    try:
        yield db_session
    finally:
        db_session.close()

And this is the test fixture we had:

@pytest.fixture
def db_session(app_config) -> Session:
    yield from get_session()

The db_session.close() call above does its job fine during normal (API) workloads, but for some reason, the test fixture was hanging after the third or so time it tried to acquire a database session during those db-oriented tests (and only if we were using coverage or pytest-cov.

We changed the fixture to include a new-ish SQLAlchemy 1.4 utility function (that says in its docstring that "might be useful for test scenarios" - so I guess there's something to that) that closes all outstanding sessions it has a weakref to.

@pytest.fixture
def db_session(app_config) -> Session:
    yield from get_session()

    ##########################
    # This needs to be here because tests hang when run with "coverage" or "pytest --cov"
    # The problem does not arise when using a simple `pytest` invocation
    # Something in the way coverage's hooks work might be allowing database sessions
    # to become dangling/lost in context (and never freed)
    ##########################
    from sqlalchemy.orm import close_all_sessions
    close_all_sessions()

And just like that, our test suite was back up and running, both locally and on GitHub Actions (No need to downgrade or play around with pytest / coverage versions or anything like that).

Hope this helps someone else!

olliemath commented 2 years ago

Just spent an unpleasant couple of weeks trying to find the reason for this in our codebase. Thanks @flipbit03 and @gpouilloux for the hint.

SQLA sessions are stored as weak refs (see code here) and we use pypy which is a bit lazy about garbage-collection. I assume (but may be wrong) that pytest coverage can also interfering with the gc mechanism for these sessions too.

What that means practically is that if you accidentally check out a session in a fixture and forget to remove it and there is no gc cycle you can get a locked table when you try to drop it. Either a gc.collect, or on newer SQLA versions a close_all_sessions seem to be sufficient.