Search code examples
firebasegoogle-cloud-firestorepython-asyncio

Is it possible to call asyncio.run() multiple times with firebase?


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("\n\nSuccess1\n\n")
asyncio.run(_upload_async())
print("\n\nSuccess2\n\n")

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?


Solution

  • If you are happy with this design, just change the control flow lines at the end to use the same loop. Istead of:

    asyncio.run(_upload_async())
    print("\n\nSuccess1\n\n")
    asyncio.run(_upload_async())
    print("\n\nSuccess2\n\n")
    

    Do

    loop = asyncio.new_event_loop()  # Maybe change this line to the very
                                    # beggining, after the imports.
    loop.run_until_complete(_upload_async())
    print("\n\nSuccess1\n\n")
    loop.run_until_complete(_upload_async())
    print("\n\nSuccess2\n\n")
    

    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 the app and db_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.