X Tutup
The Wayback Machine - https://web.archive.org/web/20250112154202/https://github.com/python/cpython/issues/126684
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

Add budget for asyncio Task #126684

Closed
hidva opened this issue Nov 11, 2024 · 5 comments
Closed

Add budget for asyncio Task #126684

hidva opened this issue Nov 11, 2024 · 5 comments
Labels
topic-asyncio type-feature A feature request or enhancement

Comments

@hidva
Copy link

hidva commented Nov 11, 2024

Feature or enhancement

Proposal:

Should we add a mechanism similar to Per-task operation budget:

Even though Tokio is not able to preempt, there is still an opportunity to nudge a task to yield back to the scheduler. As of 0.2.14, each Tokio task has an operation budget. This budget is reset when the scheduler switches to the task. Each Tokio resource (socket, timer, channel, ...) is aware of this budget. As long as the task has budget remaining, the resource operates as it did previously. Each asynchronous operation (actions that users must .await on) decrements the task's budget. Once the task is out of budget, all Tokio resources will perpetually return "not ready" until the task yields back to the scheduler. At that point, the budget is reset, and future .awaits on Tokio resources will again function normally.

The possible modifications are as follows:

diff --git a/Lib/asyncio/futures.py b/Lib/asyncio/futures.py
index 5f6fa234872..6356d9c2f6d 100644
--- a/Lib/asyncio/futures.py
+++ b/Lib/asyncio/futures.py
@@ -285,6 +285,8 @@ def __await__(self):
         if not self.done():
             self._asyncio_future_blocking = True
             yield self  # This tells Task to wait for completion.
+        if current_task_has_no_budget:
+            yield self  # We don't have a budget! Let others run.
         if not self.done():
             raise RuntimeError("await wasn't used with future")
         return self.result()  # May raise too.

Has this already been discussed elsewhere?

This is a minor feature, which does not need previous discussion elsewhere

Links to previous discussion of this feature:

No response

@hidva hidva added the type-feature A feature request or enhancement label Nov 11, 2024
@github-project-automation github-project-automation bot moved this to Todo in asyncio Nov 11, 2024
@asvetlov
Copy link
Contributor

Could you please provide more comprehensive example?
In particular, I'm curious where asyncio should reset and decrease the budget.

A working (but maybe incomplete) example would be awesome!

@hidva
Copy link
Author

hidva commented Nov 11, 2024

Could you please provide more comprehensive example?

Our online LLM service currently has two Coroutine Tasks that communicate through an asyncio.Queue. Here is a code example:

q = asyncio.Queue()

async def task1():
  while True:
    req = await get_req_from_connection()
    do_something(req)
    await q.put(req)

async def task2():
  req = await q.get()
  do_something2(req)

Since there are always requests in the connection during load testing, task1 keeps executing, which blocks task2 from running. Our current workaround is using q = asyncio.Queue(maxsize=128). However, we still want to know if it's possible to implement a mechanism similar to the Tokio Task budget.

where asyncio should reset budget

When a task switches from non-runnable to the runnable state.

where asyncio should decrease budget

Currently, I only have a preliminary idea that hasn't been fully validated, as demonstrated in the following code:

diff --git a/Lib/asyncio/futures.py b/Lib/asyncio/futures.py
index 5f6fa234872..e4839e51a7d 100644
--- a/Lib/asyncio/futures.py
+++ b/Lib/asyncio/futures.py
@@ -285,6 +285,10 @@ def __await__(self):
         if not self.done():
             self._asyncio_future_blocking = True
             yield self  # This tells Task to wait for completion.
+        # Returning True from acquire means that the current task has a budget available
+        # and we have deducted from it.
+        if not current_task_budget.acquire():
+            yield self
         if not self.done():
             raise RuntimeError("await wasn't used with future")
         return self.result()  # May raise too.

@asvetlov
Copy link
Contributor

I see your request.

Unfortunately, asyncio works in a different way.
It has no callback or something that is called on every await thing.
Say, if q.put(req) has enough capacity in the queue it returns without creating a future and awaiting for it.
Thus, I don't see a single place where we can put the budget decreasing.

Modifying all async functions in asyncio and third-party libraries to decrease a budget doesn't sound like a right way to do things, sorry.
Also, budget management can have a visible performance degradation.

P.S.
Your example could be modified by adding explicit task switch to avoid starvation problem:

async def task1():
  while True:
    req = await get_req_from_connection()
    do_something(req)
    await asyncio.sleep(0)  # enforce task switch
    await q.put(req)

@hidva
Copy link
Author

hidva commented Nov 11, 2024

Thanks

@hidva hidva closed this as completed Nov 11, 2024
@github-project-automation github-project-automation bot moved this from Todo to Done in asyncio Nov 11, 2024
@tomasr8 tomasr8 closed this as not planned Won't fix, can't repro, duplicate, stale Nov 11, 2024
@sanjeevagasta

This comment was marked as spam.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
topic-asyncio type-feature A feature request or enhancement
Projects
Status: Done
Development

No branches or pull requests

5 participants
X Tutup