mirror of https://github.com/nocturn9x/giambio.git
100 lines
4.6 KiB
Python
100 lines
4.6 KiB
Python
from collections import deque
|
|
import types
|
|
from ._layers import Task
|
|
from heapq import heappush
|
|
from ._traps import sleep
|
|
import time
|
|
|
|
|
|
class TaskManager:
|
|
"""The only way to execute asynchronous code in giambio is trough a ``TaskManager`` (or one of its children classes) object used within
|
|
an ``async with`` context manager. The ``TaskManager`` is an ideal "playground" where all asynchronous code runs and where giambio's
|
|
event loop can control their execution flow.
|
|
|
|
The key feature of this mechanism is that all tasks are always joined automatically: This opens a new world of features,
|
|
allowing exceptions to propagate just as expected (exceptions are **never** discarded in giambio, unlike in some other libraries) and some other lower-level advantages.
|
|
Moreover, giambio always saves the return values so that you don't lose any important information when executing a coroutine.
|
|
|
|
There are a few concepts to explain here, though:
|
|
|
|
- The term "task" refers to a coroutine executed trough the ``TaskManager``'s methods ``spawn()`` and ``schedule()``, as well as
|
|
one executed with ``await coro()``
|
|
- If an exception occurs, all other tasks are cancelled (read more below) and the exception is later propagated in the parent task
|
|
- The concept of cancellation is a bit tricky, because there is no real way to stop a coroutine from executing without actually raising
|
|
an exception inside i. So when giambio needs to cancel a task, it just throws ``giambio.exceptions.CancelledError`` inside it and hopes for the best.
|
|
This exception inherits from ``BaseException``, which by convention means that it should not be catched. Doing so in giambio will likely break your code
|
|
and make it explode spectacularly. If you really want to catch it to perform cleanup, be sure to re-raise it when done (or to raise another unhandled exception if you want)
|
|
so that the internal loop can proceed with execution.
|
|
In general, when writing an asynchronous function, you should always consider that it might be cancelled at any time and handle that case accordingly.
|
|
"""
|
|
|
|
|
|
|
|
def __init__(self, loop):
|
|
self.values = {} # Results from each task
|
|
self.loop = loop # The event loop that spawned the TaskManager
|
|
|
|
async def _cancel_and_raise(self, exc):
|
|
"""Cancels all the tasks inside the TaskManager object and raises the exception
|
|
of the task that triggered this mechanism"""
|
|
|
|
try:
|
|
await self.loop.running.cancel()
|
|
except Exception as error:
|
|
self.loop.running.exc = error
|
|
for task in self.loop.to_run + deque(self.loop.paused):
|
|
if isinstance(task, tuple): # Sleeping task
|
|
_, _, task = task
|
|
try:
|
|
await task.cancel()
|
|
except Exception as error:
|
|
task.exc = error
|
|
raise exc
|
|
|
|
async def __aenter__(self):
|
|
return self
|
|
|
|
async def __aexit__(self, type, value, traceback):
|
|
while True:
|
|
if not any([self.loop.to_run, self.loop.paused]):
|
|
break
|
|
tasks = self.loop.to_run + deque(self.loop.paused)
|
|
task = tasks.popleft()
|
|
if isinstance(task, tuple): # Sleeping task
|
|
_, _, task = task
|
|
self.values[task] = await task.join()
|
|
if task.exc:
|
|
print(task)
|
|
await self._cancel_and_raise(task.exc)
|
|
|
|
def spawn(self, coroutine: types.coroutine):
|
|
"""Schedules a task for execution, appending it to the call stack
|
|
|
|
:param coroutine: The coroutine to spawn, please note that if you want to execute foo, you need to pass foo() as this
|
|
returns a coroutine instead of a function object
|
|
:type coroutine: types.coroutine
|
|
:returns: A ``Task`` object
|
|
:rtype: class: Task
|
|
"""
|
|
|
|
task = Task(coroutine)
|
|
self.loop.to_run.append(task)
|
|
return task
|
|
|
|
def schedule(self, coroutine: types.coroutine, n: int):
|
|
"""Schedules a task for execution after when seconds
|
|
|
|
:param coroutine: The coroutine to spawn, please note that if you want to execute foo, you need to pass foo() as this
|
|
returns a coroutine instead of a function object
|
|
:type coroutine: types.coroutine
|
|
:param n: The delay in seconds after which the task should start running
|
|
:type n: int
|
|
:returns: A ``Task`` object
|
|
:rtype: class: Task
|
|
"""
|
|
|
|
self.loop.sequence += 1
|
|
task = Task(coroutine)
|
|
heappush(self.loop.paused, (self.loop.clock() + n, self.loop.sequence, task))
|
|
return task
|