Skip to content
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

"Event Loop is Closed" error while Testing #988

Closed
jjackoway opened this issue Oct 19, 2017 · 22 comments
Closed

"Event Loop is Closed" error while Testing #988

jjackoway opened this issue Oct 19, 2017 · 22 comments

Comments

@jjackoway
Copy link

jjackoway commented Oct 19, 2017

I've had multiple issues with the event loop in my tests. I suspect they're all related, but am not totally sure.

The first is that if I try to make multiple requests to my app using app.test_client in a single test case, I get errors on awaits within my app that the event loop is closed:

tests/integration/test_v1.py exception calling callback for <Future at 0x10e027588 state=finished returned UpdateResult>
Traceback (most recent call last):
  File "/usr/local/Cellar/python3/3.6.2/Frameworks/Python.framework/Versions/3.6/lib/python3.6/concurrent/futures/_base.py", line 297, in _invoke_callbacks
    callback(self)
  File "/usr/local/Cellar/python3/3.6.2/Frameworks/Python.framework/Versions/3.6/lib/python3.6/asyncio/futures.py", line 419, in _call_set_state
    dest_loop.call_soon_threadsafe(_set_state, destination, source)
  File "uvloop/loop.pyx", line 1023, in uvloop.loop.Loop.call_soon_threadsafe (uvloop/loop.c:23219)
  File "uvloop/loop.pyx", line 500, in uvloop.loop.Loop._call_soon (uvloop/loop.c:13241)
  File "uvloop/loop.pyx", line 504, in uvloop.loop.Loop._call_soon_handle (uvloop/loop.c:13300)
  File "uvloop/loop.pyx", line 532, in uvloop.loop.Loop._check_closed (uvloop/loop.c:13860)
RuntimeError: Event loop is closed

I also get this error shortly afterwards:
RuntimeError: Task <Task pending coro=<Sanic.handle_request() running at /Users/.../server.py:36> cb=[run_until_complete.<locals>.<lambda>()]> got Future <Future pending cb=[_chain_future.<locals>._call_check_cancel() at /usr/local/Cellar/python3/3.6.2/Frameworks/Python.framework/Versions/3.6/lib/python3.6/asyncio/futures.py:408]> attached to a different loop

I was able to work around this using pytest-sanic, setting up some fixtures, and restructuring my tests to all be async functions.

However, this process is kind of painful and not how I'd like to test my app, and it would be great if we could configure the test_client not to shut down its event loop after a single request.

Even with that workaround, however, I then encountered the same issue when I added a startup listener to my app:

    @app.listener('before_server_start')
    async def setup_mongo(app, loop):
        await Db.initialize()

    #for reference
    async def initialize():
        db = await Db.get_db()
        await db.....create_index(...)

I get the same set of errors.

@yunstanford
Copy link
Member

@jjackoway app.test_client starts and stops the app for each single request, it's not very good practice... IMO, creating some fixtures shared by different unit tests is good for maintenance and also very easy to add custom settings.

Also, app.test_client requires a outdated version of aiohttp.

@jjackoway
Copy link
Author

@yunstanford fair enough, although I still encounter the same error using fixtures + pytest-sanic once I add a startup listener to my app as shown.

@yunstanford
Copy link
Member

yunstanford commented Oct 30, 2017

@jjackoway weird, can you show me some pieces of your code and the error msg ? and which version of Sanic are you using ?

I have some unit tests around Sanic + Mongo, works fine.

@jjackoway
Copy link
Author

jjackoway commented Oct 30, 2017

@yunstanford I'm using sanic 0.6.0, pytest-sanic 0.1.4

conftest:

@pytest.fixture
def app():
    return create_app()


@pytest.fixture
def test_app(loop, app, test_client):
    return loop.run_until_complete(test_client(app))


@pytest.fixture
def ut():
    return TestCase('__init__')

Test Cases (simplified to demonstrate the issue):

async def test_alive(test_app, ut):

    response = await test_app.get('/alive.txt')

    text = await response.text()

    ut.assertEqual('OK', text)


async def test_alive_again(test_app, ut):
    # exact same code as other one, which works if i don't include this

Errors:

Traceback (most recent call last):
  File "/usr/local/Cellar/python3/3.6.2/Frameworks/Python.framework/Versions/3.6/lib/python3.6/concurrent/futures/_base.py", line 297, in _invoke_callbacks
    callback(self)
  File "/usr/local/Cellar/python3/3.6.2/Frameworks/Python.framework/Versions/3.6/lib/python3.6/asyncio/futures.py", line 419, in _call_set_state
    dest_loop.call_soon_threadsafe(_set_state, destination, source)
  File "/usr/local/Cellar/python3/3.6.2/Frameworks/Python.framework/Versions/3.6/lib/python3.6/asyncio/base_events.py", line 620, in call_soon_threadsafe
    self._check_closed()
  File "/usr/local/Cellar/python3/3.6.2/Frameworks/Python.framework/Versions/3.6/lib/python3.6/asyncio/base_events.py", line 357, in _check_closed
    raise RuntimeError('Event loop is closed')
RuntimeError: Event loop is closed

And

RuntimeError: Task <Task pending coro=<test_client.<locals>.create_client() running at /...../.tox/py3/lib/python3.6/site-packages/pytest_sanic/plugin.py:129> cb=[_run_until_complete_cb() at /usr/local/Cellar/python3/3.6.2/Frameworks/Python.framework/Versions/3.6/lib/python3.6/asyncio/base_events.py:176]> got Future <Future pending cb=[_chain_future.<locals>._call_check_cancel() at /usr/local/Cellar/python3/3.6.2/Frameworks/Python.framework/Versions/3.6/lib/python3.6/asyncio/futures.py:408]> attached to a different loop

Which happens on the await create_index line initiated by the following block:

    async def setup_mongo(app, loop):
        await Db.initialize()

    #for reference
    async def initialize():
        db = await Db.get_db()
        await db.....create_index(...)```

@yunstanford
Copy link
Member

Sorry, your code is not organized very well, i can't get much info... which mongodb driver are you using ? How did you initialize your db driver ? you may wanna pass loop explicitly.

BTW, i used motor + sanic, which works fine.

And why do you wanna fixture ut ? simply assert text == 'OK' should be fine. maybe i didn't get you.. but it doesn't matter though.

@jjackoway
Copy link
Author

I'm using motor, and I'm not explicitly doing anything with the loop anywhere inside of any of my code. I don't really want to start doing that and complicate my server code.

I pass ut as a fixture because I use some of the more complex assertions in other tests, and it's convenient to have it be a fixture.

@yunstanford
Copy link
Member

@jjackoway Ideally, you don't have to pass loop anywhere.. In addition, pytest-sanic have done that for you under the hood for extra safe.

I'm pretty sure, Sanic + pytest-sanic + motor works fine with startup/shutdown listeners, i have some unit tests around that. If you can show your code(setup code) in better way, i could help you catch your issue.

@bratushka
Copy link

Hello @yunstanford and @jjackoway . Got the same issue. Created a minimal app to reproduce it, please find it here. The problem is described in the README.

Any idea on how to fix that?

@yunstanford
Copy link
Member

@bratushka the sanic test_client will be tear down for each single request, probably something wrong there. did you try pytest-sanic if you're using pytest ?

@bratushka
Copy link

@yunstanford I don't use pytest. Actually I've already found another way to achieve my objectives, this is just a POC of the problem, so smarter people could find the bug inside the Sanic library

@jibinmathew69
Copy link

Facing exact same issue, how do I run multiple requests within test case using pytest-sanic.

@jibinmathew69
Copy link

Could someone provide an end to end example for implementing multiple requests, or create proper tutorials?

@harshanarayana
Copy link
Contributor

@jibinmathew691993 Can you help me with a bit more details of what your use case is?
If you are looking for parallel test execution, there is an example if the repo under examples directory named pytest_xdist.py. There are other examples available under examples directory as well.

A bit more detail on what's the expectation for example/tutorial would help.

@bratushka
Copy link

@jibinmathew691993 if you are using aiopg like I was and you try to reuse the connection to the database like I was doing:

connection = None
async def get_connection():
    """Get existing connection if not closed or create one"""
    global connection

    if connection is None or connection.closed:
        connection = await aiopg.connect(
            database='user',
            user='user',
            password='password',
            host='db',
        )

    return connection

I've got a workaround for you.

The if statement should look like this:

    if connection is None or connection.closed or connection._loop.is_closed:

Reason: SanicTestClient._sanic_endpoint_test calls self.app.stop() which leads to get_event_loop().stop() where the event loop is closed, while your connection is still using it. Just check if the loop is still running.

P.S.: I don't think get_event_loop().stop() is a good way to kill the app because the loop can be used by other libraries. Will try to fix that when (and if) I have time.

@jibinmathew69
Copy link

jibinmathew69 commented Dec 10, 2018

I am running an asynio.ensure_future(task) in the api call for an asynchronous API call when I disable that the async task the test work properly. However, when the async task is enabled, the Event Loop is Closed error happens.

platform linux -- Python 3.6.3, pytest-4.0.1, py-1.7.0, pluggy-0.8.0
rootdir: /home/jibin/PycharmProjects/imgurupload, inifile:
plugins: sanic-0.1.15
collected 2 items                                                                                                                                                                                  

tests/test_app.py ..                                                                                                                                                                         [100%]

===================================================================================== 2 passed in 0.14 seconds =====================================================================================
exception calling callback for <Future at 0x7fa4cc971a90 state=finished returned list>
Traceback (most recent call last):
  File "/usr/lib/python3.6/concurrent/futures/_base.py", line 324, in _invoke_callbacks
    callback(self)
  File "/usr/lib/python3.6/asyncio/futures.py", line 419, in _call_set_state
    dest_loop.call_soon_threadsafe(_set_state, destination, source)
  File "/usr/lib/python3.6/asyncio/base_events.py", line 620, in call_soon_threadsafe
    self._check_closed()
  File "/usr/lib/python3.6/asyncio/base_events.py", line 357, in _check_closed
    raise RuntimeError('Event loop is closed')
RuntimeError: Event loop is closed
exception calling callback for <Future at 0x7fa4cc971390 state=finished returned list>
Traceback (most recent call last):
  File "/usr/lib/python3.6/concurrent/futures/_base.py", line 324, in _invoke_callbacks
    callback(self)
  File "/usr/lib/python3.6/asyncio/futures.py", line 419, in _call_set_state
    dest_loop.call_soon_threadsafe(_set_state, destination, source)
  File "/usr/lib/python3.6/asyncio/base_events.py", line 620, in call_soon_threadsafe
    self._check_closed()
  File "/usr/lib/python3.6/asyncio/base_events.py", line 357, in _check_closed
    raise RuntimeError('Event loop is closed')
RuntimeError: Event loop is closed
exception calling callback for <Future at 0x7fa4cc971588 state=finished returned list>
Traceback (most recent call last):
  File "/usr/lib/python3.6/concurrent/futures/_base.py", line 324, in _invoke_callbacks
    callback(self)
  File "/usr/lib/python3.6/asyncio/futures.py", line 419, in _call_set_state
    dest_loop.call_soon_threadsafe(_set_state, destination, source)
  File "/usr/lib/python3.6/asyncio/base_events.py", line 620, in call_soon_threadsafe
    self._check_closed()
  File "/usr/lib/python3.6/asyncio/base_events.py", line 357, in _check_closed
    raise RuntimeError('Event loop is closed')
RuntimeError: Event loop is closed
exception calling callback for <Future at 0x7fa4cc9711d0 state=finished returned list>
Traceback (most recent call last):
  File "/usr/lib/python3.6/concurrent/futures/_base.py", line 324, in _invoke_callbacks
    callback(self)
  File "/usr/lib/python3.6/asyncio/futures.py", line 419, in _call_set_state
    dest_loop.call_soon_threadsafe(_set_state, destination, source)
  File "/usr/lib/python3.6/asyncio/base_events.py", line 620, in call_soon_threadsafe
    self._check_closed()
  File "/usr/lib/python3.6/asyncio/base_events.py", line 357, in _check_closed
    raise RuntimeError('Event loop is closed')
RuntimeError: Event loop is closed
Task was destroyed but it is pending!
task: <Task pending coro=<UrlCrawler.get_image() running at /home/jibin/PycharmProjects/imgurupload/controllers/UrlCrawler.py:89> wait_for=<Future pending cb=[<TaskWakeupMethWrapper object at 0x7fa4ccab8438>()]> cb=[_wait.<locals>._on_completion() at /usr/lib/python3.6/asyncio/tasks.py:380]>
Task was destroyed but it is pending!
task: <Task pending coro=<UrlCrawler.get_image() running at /home/jibin/PycharmProjects/imgurupload/controllers/UrlCrawler.py:89> wait_for=<Future pending cb=[<TaskWakeupMethWrapper object at 0x7fa4ccab8558>()]> cb=[_wait.<locals>._on_completion() at /usr/lib/python3.6/asyncio/tasks.py:380]>
Task was destroyed but it is pending!
task: <Task pending coro=<UrlCrawler.get_image() running at /home/jibin/PycharmProjects/imgurupload/controllers/UrlCrawler.py:89> wait_for=<Future pending cb=[<TaskWakeupMethWrapper object at 0x7fa4ccab8678>()]> cb=[_wait.<locals>._on_completion() at /usr/lib/python3.6/asyncio/tasks.py:380]>
Task was destroyed but it is pending!
task: <Task pending coro=<UrlCrawler.get_image() running at /home/jibin/PycharmProjects/imgurupload/controllers/UrlCrawler.py:89> wait_for=<Future pending cb=[<TaskWakeupMethWrapper object at 0x7fa4ccab8798>()]> cb=[_wait.<locals>._on_completion() at /usr/lib/python3.6/asyncio/tasks.py:380]>
Task was destroyed but it is pending!
task: <Task pending coro=<UrlCrawler.get_image() running at /home/jibin/PycharmProjects/imgurupload/controllers/UrlCrawler.py:89> wait_for=<Future pending cb=[<TaskWakeupMethWrapper object at 0x7fa4ccab88b8>()]> cb=[_wait.<locals>._on_completion() at /usr/lib/python3.6/asyncio/tasks.py:380]>
Unclosed client session
client_session: <aiohttp.client.ClientSession object at 0x7fa4d03dc6a0>
Task was destroyed but it is pending!
task: <Task pending coro=<UrlCrawler.fetch_urls() running at /home/jibin/PycharmProjects/imgurupload/controllers/UrlCrawler.py:68> wait_for=<Future pending cb=[<TaskWakeupMethWrapper object at 0x7fa4ccab81c8>()]>>
Exception ignored in: <coroutine object UrlCrawler.fetch_urls at 0x7fa4d03ff990>
Traceback (most recent call last):
  File "/home/jibin/PycharmProjects/imgurupload/controllers/UrlCrawler.py", line 71, in fetch_urls
    kwargs["finished"][id] = datetime.datetime.utcnow().isoformat()
  File "/usr/local/lib/python3.6/dist-packages/aiohttp/client.py", line 828, in __aexit__
    await self.close()
  File "/usr/local/lib/python3.6/dist-packages/aiohttp/client.py", line 779, in close
    self._connector.close()
  File "/usr/local/lib/python3.6/dist-packages/aiohttp/connector.py", line 657, in close
    ev.cancel()
  File "/usr/local/lib/python3.6/dist-packages/aiohttp/locks.py", line 38, in cancel
    waiter.cancel()
  File "/usr/lib/python3.6/asyncio/base_events.py", line 574, in call_soon
    self._check_closed()
  File "/usr/lib/python3.6/asyncio/base_events.py", line 357, in _check_closed
    raise RuntimeError('Event loop is closed')
RuntimeError: Event loop is closed
Task was destroyed but it is pending!
task: <Task pending coro=<TCPConnector._resolve_host() running at /usr/local/lib/python3.6/dist-packages/aiohttp/connector.py:730> wait_for=<Future pending cb=[_chain_future.<locals>._call_check_cancel() at /usr/lib/python3.6/asyncio/futures.py:408, <TaskWakeupMethWrapper object at 0x7fa4ccab8a68>()]> cb=[shield.<locals>._done_callback() at /usr/lib/python3.6/asyncio/tasks.py:679]>
Task was destroyed but it is pending!
task: <Task pending coro=<TCPConnector._resolve_host() running at /usr/local/lib/python3.6/dist-packages/aiohttp/connector.py:730> wait_for=<Future pending cb=[_chain_future.<locals>._call_check_cancel() at /usr/lib/python3.6/asyncio/futures.py:408, <TaskWakeupMethWrapper object at 0x7fa4ccab8f18>()]> cb=[shield.<locals>._done_callback() at /usr/lib/python3.6/asyncio/tasks.py:679]>
Task was destroyed but it is pending!
task: <Task pending coro=<TCPConnector._resolve_host() running at /usr/local/lib/python3.6/dist-packages/aiohttp/connector.py:716> wait_for=<Task pending coro=<Event.wait() running at /usr/lib/python3.6/asyncio/locks.py:271> wait_for=<Future cancelled> cb=[<TaskWakeupMethWrapper object at 0x7fa4ccab8f78>()]> cb=[shield.<locals>._done_callback() at /usr/lib/python3.6/asyncio/tasks.py:679]>
Task was destroyed but it is pending!
task: <Task pending coro=<TCPConnector._resolve_host() running at /usr/local/lib/python3.6/dist-packages/aiohttp/connector.py:730> wait_for=<Future pending cb=[_chain_future.<locals>._call_check_cancel() at /usr/lib/python3.6/asyncio/futures.py:408, <TaskWakeupMethWrapper object at 0x7fa4cc98e138>()]> cb=[shield.<locals>._done_callback() at /usr/lib/python3.6/asyncio/tasks.py:679]>
Task was destroyed but it is pending!
task: <Task pending coro=<Event.wait() running at /usr/lib/python3.6/asyncio/locks.py:271> wait_for=<Future cancelled> cb=[<TaskWakeupMethWrapper object at 0x7fa4ccab8f78>()]>
Task was destroyed but it is pending!
task: <Task pending coro=<TCPConnector._resolve_host() running at /usr/local/lib/python3.6/dist-packages/aiohttp/connector.py:730> wait_for=<Future pending cb=[_chain_future.<locals>._call_check_cancel() at /usr/lib/python3.6/asyncio/futures.py:408, <TaskWakeupMethWrapper object at 0x7fa4cc98e2b8>()]> cb=[shield.<locals>._done_callback() at /usr/lib/python3.6/asyncio/tasks.py:679]>

@bratushka
Copy link

@jibinmathew691993 please show your tests/test_app.py

@jibinmathew69
Copy link

jibinmathew69 commented Dec 10, 2018

@bratushka @harshanarayana
contents of test_app.py

import json
from main import make_app

@pytest.yield_fixture
def app():
    app = make_app()
    yield app

@pytest.fixture
def test_cli(loop, app, test_client):

    return loop.run_until_complete(test_client(app))



async def test_upload_api(test_cli):
    data = {
	"urls" : [
				"https:"
		]
    }
    response = await test_cli.post('/v1/images/upload',data=json.dumps(data))
    data = await response.json()
    response2 = await test_cli.get("/v1/images/upload/%s"%data["jobId"])
    assert response2.status == 200

async def test_status(test_cli):

    response = await test_cli.get('/v1/images')

    assert response.status == 200

@bratushka
Copy link

bratushka commented Dec 10, 2018

@jibinmathew691993 and the code of the /v1/images/upload endpoint, please. Or better just post the project on github or anywhere so we don't ask for tiny pieces of code

@jibinmathew69
Copy link

jibinmathew69 commented Dec 11, 2018

this is the function call in /v1/images/upload end point

    @app.route('/v1/images/upload',methods=["POST"])
    async def upload(request):
        from controllers.UrlCrawler import UrlCrawler
        from controllers.Imgur import Imgur

        return await UrlCrawler().fetcher(request,Imgur(),
                                    completed=completed,
                                    pending=pending,
                                    failed=failed,
                                    created=created,
                                    finished=finished
                                    )
    async def fetcher(self,request,imgur,**kwargs):
        if 'urls' not in request.json:
            raise ServerError("Insufficient parameters",status_code=400)

        urls = request.json["urls"]

        from uuid import uuid4
        import datetime


        os.makedirs(os.path.join(self.upload_folder,id))
        asyncio.ensure_future(self.fetch_urls(urls,id,imgur,**kwargs))
        return await self.create_response(id)


    async def create_response(self,id):
        return response.json({
            "jobId" : id
        },status=200)

@harshanarayana
Copy link
Contributor

@jibinmathew691993 I am looking into this to see what I can find out. I will update this thread shortly with findings.

@ahopkins
Copy link
Member

I am closing this as it does not seem like a Sanic issue and should either be addressed on pytest-sanic or the community forums. If I am wrong and there is something related to Sanic itself, please open a new issue.

@KallieLev
Copy link

KallieLev commented Jan 31, 2023

For anyone interested, this solved the issue in testing Faust agents for me.

adding the following fixture -

@pytest.mark.asyncio()
@pytest.fixture()
def event_loop():
    """necessary for testing Faust agents"""
    yield app.loop

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

8 participants