I encounter a RuntimeError: Event loop is closed
error when trying to run asyncio.run()
multiple times with Firebase. Is this expected? Here is a simplified example of the issue:
import streamlit as st
import firebase_admin
from firebase_admin import credentials, firestore_async
import json
import asyncio
key_dict = json.loads(st.secrets["textkey"])
cred = credentials.Certificate(key_dict)
app = firebase_admin.initialize_app(cred)
db_async = firestore_async.client(app)
async def _upload_async():
dicts_to_upload = {
"doc_1": {"key_1": "val_1"},
"doc_2": {"key_2": "val_2"},
}
tasks = []
for key in dicts_to_upload.keys():
result_dict = db_async.collection("test-collection").document(key)
task = result_dict.set(dicts_to_upload[key])
tasks.append(task)
await asyncio.gather(*tasks)
asyncio.run(_upload_async())
print("nnSuccess1nn")
asyncio.run(_upload_async())
print("nnSuccess2nn")
When running this, the output is:
Success1
Traceback (most recent call last):
File "/Users/colinrichter/Documents/GitHub/YCN-Calculator/testing.py", line 100, in <module>
asyncio.run(_upload_async())
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/asyncio/runners.py", line 190, in run
return runner.run(main)
^^^^^^^^^^^^^^^^
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/asyncio/runners.py", line 118, in run
return self._loop.run_until_complete(task)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/asyncio/base_events.py", line 653, in run_until_complete
return future.result()
^^^^^^^^^^^^^^^
File "/Users/colinrichter/Documents/GitHub/YCN-Calculator/testing.py", line 95, in _upload_async
await asyncio.gather(*tasks)
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/google/cloud/firestore_v1/async_document.py", line 131, in set
write_results = await batch.commit(**kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/google/cloud/firestore_v1/async_batch.py", line 60, in commit
commit_response = await self._client._firestore_api.commit(
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/google/cloud/firestore_v1/services/firestore/async_client.py", line 1055, in commit
response = await rpc(
^^^^^^^^^^
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/google/api_core/retry/retry_unary_async.py", line 230, in retry_wrapped_func
return await retry_target(
^^^^^^^^^^^^^^^^^^^
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/google/api_core/retry/retry_unary_async.py", line 160, in retry_target
_retry_error_helper(
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/google/api_core/retry/retry_base.py", line 212, in _retry_error_helper
raise final_exc from source_exc
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/google/api_core/retry/retry_unary_async.py", line 155, in retry_target
return await target()
^^^^^^^^
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/google/api_core/timeout.py", line 120, in func_with_timeout
return func(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/google/api_core/grpc_helpers_async.py", line 166, in error_remapped_callable
call = callable_(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/grpc/aio/_channel.py", line 150, in __call__
call = UnaryUnaryCall(
^^^^^^^^^^^^^^^
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/grpc/aio/_call.py", line 565, in __init__
self._invocation_task = loop.create_task(self._invoke())
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/asyncio/base_events.py", line 434, in create_task
self._check_closed()
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/asyncio/base_events.py", line 519, in _check_closed
raise RuntimeError('Event loop is closed')
RuntimeError: Event loop is closed
sys:1: RuntimeWarning: coroutine 'UnaryUnaryCall._invoke' was never awaited
RuntimeWarning: Enable tracemalloc to get the object allocation traceback
I also confirmed in Firebase that it is uploading the test-collection
correctly the first time. However, when running it the second time I encounter the error. If I replace the firebase functions with a regular async function that does not use Firebase (like just adding something to doc_1
and doc_2
) then it works as expected without this error.
I am doing this because I have a website on Streamlit where users fill out a form which updates some dictionaries, and then I upload those dictionaries to Firebase. I am trying to do this asynchronously to increase performance, but I can only get it to work the first time a user fills out the form. All subsequent times, I run into a RuntimeError: Event loop is closed
error.
AmI doing something incorrectly, or is this a bug/feature of Firebase?
2
Answers
If you are happy with this design, just change the control flow lines at the end to use the same loop. Istead of:
Do
What is taking place there is that the firebase async client is binding itself to the running loop the first time it is actually used, and, when you call
asyncio.run
, a new loop is created. It probably would work if you’d recreate theapp
anddb_async
instances between calls.The "right thing to do" however, would be to refactor this code and put all of these instances and calls out of "top level" code and into an asynchronous main function, though – the only line of code outside any function should be one calling the loop to run this controlling function.
It is possible to run
asyncio.run
twice, but it isn’t recommended. Here is an example that runs it twice:This prints out:
But
asyncio.run
is meant to run the async part of your program from start to finish. It isn’t necessarily meant to be called multiple times. From the documentation:From your error, it indeed seems like Firebase is doing something with the event loop. I haven’t ever used Firebase, so I don’t know. I would look into what the functions:
are doing. Are they startup up an event loop or doing any management of one? I would also investigate the lines:
and verify that they are creating proper tasks.
In terms of what to do differently, you should (generally speaking) have a single
asyncio
event loop that stays running that handles everything you need it to. Callingasyncio.run
multiple times should only really be needed when you have multipleasyncio
event loops, each running on different threads, but that seems like it would be rare indeed.I don’t know what the rest of your program looks like, but a nice way to offload work onto an
asyncio
event loop is to send it messages via anasyncio.Queue
. Here’s some relevant StackOverflow answers covering this: