Skip to content

Properly await coroutines registered with app.on_shutdown #4641

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

Open
wants to merge 58 commits into
base: main
Choose a base branch
from

Conversation

rodja
Copy link
Member

@rodja rodja commented Apr 21, 2025

This PR started with the intent to fix #4592 which reported unawaited coroutines when running pytests. It turns out, the warning lead down a rabbit hole of problems. Here is a list of the major findings and fixes:

  • the coroutines registered with app.on_shutdown have not been awaited properly
  • background_tasks where not canceled on shutdown
  • outbox.loop did not exit when receiving a cancel command
  • handle cancellation in internal house-keeping loops (pruning, binding, ...)
  • before Python 3.12 asyncio.wait_for misses cancel commands in certain conditions and must be fixed with wait-for2
  • ensure that all storage backups are written before exiting
  • introduce @background_tasks.await_on_shutdown annotation to mark coroutines as "not to cancel on shutdown" (and thereby also fixes app reset fails due to improper I/O #4312)
  • clean up multiprocessing to not get "leaked semaphore" warnings (fix was described in Leaked Semaphore Objects #4131 (comment))

Also this PR makes some minor improvements:

  • disable pytest warning from upstream packages which we can do nothing about
  • fix UTC warnings
  • add names to background_tasks where ever possible
  • only stop screen tests if console output contains "ERROR" (same as Fix unexpected logs in pytest when using the user fixture #4608 did for the user tests)
  • first steps to add some background_tasks documentation

rodja added 30 commits April 18, 2025 08:22
to not mix up the loops
to make sure it's not lost when resetting globals between test runs
to ensure all tasks and coroutines are really done before entering the next test
to better identify pending tasks and where they come from
@rodja rodja marked this pull request as ready for review April 23, 2025 05:49
@rodja rodja requested a review from falkoschindler April 23, 2025 05:50
@rodja rodja added feature Type/scope: New feature or enhancement testing Type/scope: Pytests and test fixtures review Status: PR is open and needs review 🟠 major Priority: Important, but not urgent labels Apr 23, 2025
@rodja rodja added this to the 2.16 milestone Apr 23, 2025
@evnchn
Copy link
Collaborator

evnchn commented Apr 23, 2025

The complexity of this PR has grown as we speak (or code?). I noticed that it currently contains 51 commits and a lot of changes over quite a lot of files, which makes it challenging for anyone to review and assess the overall impact.

It may be helpful to squash commits. Separate some fixes into another PR seems like another idea, with the added benefit of being able to see which changes maintain the passing test case while which breaks our tests.

However, this isn't strictly necesary, since Rodja and Falko still make for a killer combo as they can talk offline. Mark ignore to this concern by reacting 👀. Though I will say, regardless, it's too hard for me, I'm out as always. 😅

Copy link
Contributor

@falkoschindler falkoschindler left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, we're almost there. But right now we think collecting callables in functions_awaited_on_shutdown causes a potential memory leak, e.g. if the user decorates a function which is defined within a page function. So we have to either prune efficiently, work with weak references or find a different way to "mark" functions as "await on shutdown".

Copy link
Contributor

@falkoschindler falkoschindler left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we really need _should_await_on_shutdown. See my corresponding comment below.

Apart from that I tried to use the new decorator with the following snippet:

async def test1():
    print('test1 start')
    await asyncio.sleep(3)
    print('test1 end')


@background_tasks.await_on_shutdown
async def test2():
    print('test2 start')
    await asyncio.sleep(3)
    print('test2 end')

ui.button('Test1', on_click=lambda: background_tasks.create(test1()))
ui.button('Test2', on_click=lambda: background_tasks.create(test2()))
ui.button('Shutdown', on_click=app.shutdown)

But when quickly clicking "Test1"-"Test2"-"Shutdown", both tasks seem to be cancelled before the second print statement is executed. Am I doing anything wrong?

while running_tasks or lazy_tasks_running:
tasks = set(running_tasks) | set(lazy_tasks_running.values())
for task in tasks:
if not task.done() and not task.cancelled() and not _should_await_on_shutdown(task):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if not task.done() and not task.cancelled() and not _should_await_on_shutdown(task):
if not task.done() and not task.cancelled() and task.get_coro() not in functions_awaited_on_shutdown:

Is this possible?

async def teardown() -> None:
"""Cancel all running tasks and coroutines on shutdown. (For internal use only.)"""
while running_tasks or lazy_tasks_running:
tasks = set(running_tasks) | set(lazy_tasks_running.values())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
tasks = set(running_tasks) | set(lazy_tasks_running.values())
tasks = running_tasks | set(lazy_tasks_running.values())

running_tasks is already a set.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature Type/scope: New feature or enhancement 🟠 major Priority: Important, but not urgent review Status: PR is open and needs review testing Type/scope: Pytests and test fixtures
Projects
None yet
4 participants