mirror of https://github.com/nocturn9x/giambio.git
Fixed lots of minor bugs (and created some new bugs, of course). Pool-level cancellation is broken, but everything else seems to work given the current tests
This commit is contained in:
parent
40bcebbf5a
commit
98c9440115
|
@ -18,8 +18,8 @@ limitations under the License.
|
|||
|
||||
|
||||
import types
|
||||
from .objects import Task
|
||||
from .core import AsyncScheduler
|
||||
from giambio.objects import Task
|
||||
from giambio.core import AsyncScheduler
|
||||
|
||||
|
||||
class TaskManager:
|
||||
|
@ -33,9 +33,13 @@ class TaskManager:
|
|||
"""
|
||||
|
||||
self.loop = loop
|
||||
self.tasks = [] # We store a reference to all tasks, even the asleep ones!
|
||||
self.tasks = [] # We store a reference to all tasks in this pool, even the paused ones!
|
||||
self.cancelled = False
|
||||
self.started = self.loop.clock()
|
||||
self.timeout = self.started + timeout
|
||||
if timeout:
|
||||
self.timeout = self.started + timeout
|
||||
else:
|
||||
self.timeout = None
|
||||
|
||||
def spawn(self, func: types.FunctionType, *args):
|
||||
"""
|
||||
|
@ -44,6 +48,7 @@ class TaskManager:
|
|||
|
||||
task = Task(func(*args), func.__name__ or str(func), self)
|
||||
task.joiners = [self.loop.current_task]
|
||||
task.next_deadline = self.timeout or 0.0
|
||||
self.loop.tasks.append(task)
|
||||
self.loop.debugger.on_task_spawn(task)
|
||||
self.tasks.append(task)
|
||||
|
@ -57,6 +62,7 @@ class TaskManager:
|
|||
assert n >= 0, "The time delay can't be negative"
|
||||
task = Task(func(*args), func.__name__ or str(func), self)
|
||||
task.joiners = [self.loop.current_task]
|
||||
task.next_deadline = self.timeout or 0.0
|
||||
task.sleep_start = self.loop.clock()
|
||||
self.loop.paused.put(task, n)
|
||||
self.loop.debugger.on_task_schedule(task, n)
|
||||
|
@ -68,7 +74,17 @@ class TaskManager:
|
|||
|
||||
async def __aexit__(self, exc_type: Exception, exc: Exception, tb):
|
||||
for task in self.tasks:
|
||||
# This forces Python to block at the
|
||||
# This forces Python to stop at the
|
||||
# end of the block and wait for all
|
||||
# children to exit
|
||||
await task.join()
|
||||
self.tasks.remove(task)
|
||||
|
||||
async def cancel(self):
|
||||
"""
|
||||
Cancels the whole block
|
||||
"""
|
||||
|
||||
# TODO: This breaks, somehow, investigation needed
|
||||
for task in self.tasks:
|
||||
await task.cancel()
|
||||
|
|
241
giambio/core.py
241
giambio/core.py
|
@ -21,19 +21,21 @@ import types
|
|||
import socket
|
||||
from time import sleep as wait
|
||||
from timeit import default_timer
|
||||
from .objects import Task, TimeQueue
|
||||
from .traps import want_read, want_write
|
||||
from .util.debug import BaseDebugger
|
||||
from giambio.objects import Task, TimeQueue, DeadlinesQueue
|
||||
from giambio.traps import want_read, want_write
|
||||
from giambio.util.debug import BaseDebugger
|
||||
from itertools import chain
|
||||
from selectors import DefaultSelector, EVENT_READ, EVENT_WRITE
|
||||
from .exceptions import (InternalError,
|
||||
CancelledError,
|
||||
ResourceBusy,
|
||||
TooSlowError
|
||||
)
|
||||
from giambio.exceptions import (InternalError,
|
||||
CancelledError,
|
||||
ResourceBusy,
|
||||
GiambioError,
|
||||
TooSlowError
|
||||
)
|
||||
|
||||
|
||||
IOInterrupt = (BlockingIOError, InterruptedError)
|
||||
IO_SKIP_LIMIT = 5 # TODO: Inspect this
|
||||
|
||||
|
||||
class AsyncScheduler:
|
||||
|
@ -75,6 +77,12 @@ class AsyncScheduler:
|
|||
self.has_ran = False
|
||||
# The current pool
|
||||
self.current_pool = None
|
||||
# How many times we skipped I/O checks to let a task run
|
||||
# We limit the number of times we skip such checks to avoid
|
||||
# I/O starvation in highly concurrent systems
|
||||
self.io_skip = 0
|
||||
# A heap queue of deadlines to be checked
|
||||
self.deadlines = DeadlinesQueue()
|
||||
|
||||
def done(self):
|
||||
"""
|
||||
|
@ -107,17 +115,16 @@ class AsyncScheduler:
|
|||
# sleeping tasks, no events to deliver,
|
||||
# no I/O to do and no running tasks, we
|
||||
# simply tear us down and return to self.start
|
||||
self.shutdown()
|
||||
self.close()
|
||||
break
|
||||
elif not self.tasks:
|
||||
# If there are no actively running tasks
|
||||
# we try to schedule the asleep ones
|
||||
# We start by checking for I/O
|
||||
self.check_io()
|
||||
if self.paused:
|
||||
# Next, if there are no actively running tasks
|
||||
# we try to schedule the asleep ones
|
||||
self.awake_sleeping()
|
||||
if self.selector.get_map():
|
||||
# The next step is checking for I/O
|
||||
self.check_io()
|
||||
# Try to awake event-waiting tasks
|
||||
# Then we try to awake event-waiting tasks
|
||||
if self.events:
|
||||
self.check_events()
|
||||
# Otherwise, while there are tasks ready to run, well, run them!
|
||||
|
@ -150,24 +157,47 @@ class AsyncScheduler:
|
|||
# Sneaky method call, thanks to David Beazley for this ;)
|
||||
getattr(self, method)(*args)
|
||||
except AttributeError: # If this happens, that's quite bad!
|
||||
# This exception block is meant to be triggered by other async
|
||||
# libraries, which most likely have different trap names and behaviors.
|
||||
# If you get this exception and you're 100% sure you're not mixing
|
||||
# async primitives from other libraries, then it's a bug!
|
||||
raise InternalError("Uh oh! Something very bad just happened, did"
|
||||
" you try to mix primitives from other async libraries?") from None
|
||||
except CancelledError:
|
||||
# Task was cancelled (pending cancellation)
|
||||
# When a task needs to be cancelled, giambio tries to do it gracefully
|
||||
# first: if the task is paused in either I/O or sleeping, that's perfect.
|
||||
# But we also need to cancel a task if it was not sleeping or waiting on
|
||||
# any I/O because it could never do so (therefore blocking everything
|
||||
# forever). So, when cancellation can't be done right away, we schedule
|
||||
# if for the next execution step of the task. Giambio will also make sure
|
||||
# to re-raise cancellations at every checkpoint until the task lets the
|
||||
# exception propagate into us, because we *really* want the task to be
|
||||
# cancelled, and since asking kindly didn't work we have to use some
|
||||
# force :)
|
||||
self.current_task.status = "cancelled"
|
||||
self.current_task.cancelled = True
|
||||
self.current_task.cancel_pending = False
|
||||
self.debugger.after_cancel(self.current_task)
|
||||
self.join(self.current_task)
|
||||
except StopIteration as ret:
|
||||
# Task finished executing
|
||||
# At the end of the day, coroutines are generator functions with
|
||||
# some tricky behaviors, and this is one of them. When a coroutine
|
||||
# hits a return statement (either explicit or implicit), it raises
|
||||
# a StopIteration exception, which has an attribute named value that
|
||||
# represents the return value of the coroutine, if any. Of course this
|
||||
# exception is not an error and we should happily keep going after it,
|
||||
# most of this code below is just useful for internal/debugging purposes
|
||||
self.current_task.status = "end"
|
||||
self.current_task.result = ret.value
|
||||
self.current_task.finished = True
|
||||
self.debugger.on_task_exit(self.current_task)
|
||||
self.join(self.current_task)
|
||||
except BaseException as err:
|
||||
# Task raised an exception
|
||||
# TODO: We might want to do a bit more complex traceback hacking to remove any extra
|
||||
# frames from the exception call stack, but for now removing at least the first one
|
||||
# seems a sensible approach (it's us catching it so we don't care about that)
|
||||
self.current_task.exc = err
|
||||
self.current_task.exc.__traceback__ = self.current_task.exc.__traceback__.tb_next
|
||||
self.current_task.status = "crashed"
|
||||
self.debugger.on_exception_raised(self.current_task, err)
|
||||
self.join(self.current_task) # This propagates the exception
|
||||
|
@ -192,17 +222,6 @@ class AsyncScheduler:
|
|||
self.tasks.append(self.current_task)
|
||||
self.to_send = self.current_task
|
||||
|
||||
def check_timeouts(self):
|
||||
"""
|
||||
Checks for expired timeouts and raises appropriate
|
||||
errors
|
||||
"""
|
||||
|
||||
if self.clock() >= self.current_pool.timeout:
|
||||
# A pool with a timeout has expired!
|
||||
self.cancel_all_from_current_pool()
|
||||
raise TooSlowError()
|
||||
|
||||
def check_events(self):
|
||||
"""
|
||||
Checks for ready or expired events and triggers them
|
||||
|
@ -216,20 +235,17 @@ class AsyncScheduler:
|
|||
|
||||
def awake_sleeping(self):
|
||||
"""
|
||||
Checks for and reschedules sleeping tasks
|
||||
Reschedules sleeping tasks if their deadline
|
||||
has elapsed
|
||||
"""
|
||||
|
||||
wait(max(0.0, self.paused[0][0] - self.clock()))
|
||||
# Sleep until the closest deadline in order not to waste CPU cycles
|
||||
while self.paused[0][0] < self.clock():
|
||||
while self.paused and self.paused[0][0] < self.clock():
|
||||
# Reschedules tasks when their deadline has elapsed
|
||||
task = self.paused.get()
|
||||
slept = self.clock() - task.sleep_start
|
||||
task.sleep_start = None
|
||||
task.sleep_start = 0.0
|
||||
self.tasks.append(task)
|
||||
self.debugger.after_sleep(task, slept)
|
||||
if not self.paused:
|
||||
break
|
||||
|
||||
def check_io(self):
|
||||
"""
|
||||
|
@ -238,20 +254,44 @@ class AsyncScheduler:
|
|||
|
||||
before_time = self.clock() # Used for the debugger
|
||||
if self.tasks or self.events:
|
||||
# If there are either tasks or events and no I/O, never wait
|
||||
timeout = 0.0
|
||||
self.io_skip += 1
|
||||
if self.io_skip == IO_SKIP_LIMIT:
|
||||
# We can't skip every time there's some task ready
|
||||
# or else we might starve I/O waiting tasks
|
||||
# when a lot of things are running at the same time
|
||||
self.io_skip = 0
|
||||
timeout = 86400
|
||||
else:
|
||||
# If there are either tasks or events and no I/O, don't wait
|
||||
# (unless we already skipped this check too many times)
|
||||
timeout = 0.0
|
||||
elif self.paused:
|
||||
# If there are asleep tasks, wait until the closest deadline
|
||||
timeout = max(0.0, self.paused[0][0] - self.clock())
|
||||
if not self.deadlines:
|
||||
timeout = min([max(0.0, self.paused[0][0] - self.clock())])
|
||||
else:
|
||||
deadline = self.deadlines.get()
|
||||
timeout = min([max(0.0, self.paused[0][0] - self.clock()), deadline])
|
||||
if timeout != deadline:
|
||||
# If a sleeping tasks has to run
|
||||
# before another deadline, we schedule the former
|
||||
# first and put back the latter on the queue
|
||||
self.deadlines.put(deadline)
|
||||
else:
|
||||
# If there is *only* I/O, we wait a fixed amount of time
|
||||
timeout = 1.0
|
||||
self.debugger.before_io(timeout)
|
||||
io_ready = self.selector.select(timeout)
|
||||
# Get sockets that are ready and schedule their tasks
|
||||
for key, _ in io_ready:
|
||||
self.tasks.append(key.data) # Resource ready? Schedule its task
|
||||
self.debugger.after_io(self.clock() - before_time)
|
||||
timeout = 86400 # Thanks trio :D
|
||||
if self.selector.get_map():
|
||||
self.debugger.before_io(timeout)
|
||||
io_ready = self.selector.select(timeout)
|
||||
# Get sockets that are ready and schedule their tasks
|
||||
for key, _ in io_ready:
|
||||
self.tasks.append(key.data) # Resource ready? Schedule its task
|
||||
self.debugger.after_io(self.clock() - before_time)
|
||||
else:
|
||||
# Since select() does not work with 0 fds registered
|
||||
# we need to call time.sleep() if we need to pause
|
||||
# and no I/O has been registered
|
||||
wait(timeout)
|
||||
|
||||
def start(self, func: types.FunctionType, *args):
|
||||
"""
|
||||
|
@ -289,35 +329,19 @@ class AsyncScheduler:
|
|||
|
||||
return set(waiter for waiter in (evt.waiters for evt in self.events))
|
||||
|
||||
def cancel_all_from_current_pool(self):
|
||||
def cancel_all_from_current_pool(self, pool=None):
|
||||
"""
|
||||
Cancels all tasks in the current pool,
|
||||
preparing for the exception throwing
|
||||
from self.join
|
||||
Cancels all tasks in the current pool (or the given one)
|
||||
"""
|
||||
|
||||
to_reschedule = []
|
||||
for to_cancel in chain(self.tasks, self.paused, self.get_event_tasks()):
|
||||
if to_cancel.pool is self.current_pool:
|
||||
try:
|
||||
self.cancel(to_cancel)
|
||||
except CancelledError:
|
||||
# Task was cancelled
|
||||
self.current_task.status = "cancelled"
|
||||
self.current_task.cancelled = True
|
||||
self.current_task.cancel_pending = False
|
||||
self.debugger.after_cancel(self.current_task)
|
||||
elif to_cancel.status == "sleep":
|
||||
deadline = to_cancel.next_deadline - self.clock()
|
||||
to_reschedule.append((to_cancel, deadline))
|
||||
else:
|
||||
to_reschedule.append((to_cancel, None))
|
||||
for task, deadline in to_reschedule:
|
||||
if deadline is not None:
|
||||
self.paused.put(task, deadline)
|
||||
# If there is other work to do (nested pools)
|
||||
# we tell so to our caller
|
||||
return bool(to_reschedule)
|
||||
pool = pool or self.current_pool
|
||||
if pool:
|
||||
for to_cancel in pool.tasks:
|
||||
self.cancel(to_cancel)
|
||||
pool.cancelled = True
|
||||
return all([t.cancelled or t.finished or t.exc for t in pool.tasks])
|
||||
else: # If we're at the main task, we're sure everything else exited
|
||||
return True
|
||||
|
||||
def cancel_all(self):
|
||||
"""
|
||||
|
@ -326,22 +350,24 @@ class AsyncScheduler:
|
|||
"""
|
||||
|
||||
for to_cancel in chain(self.tasks, self.paused, self.get_event_tasks()):
|
||||
try:
|
||||
self.cancel(to_cancel)
|
||||
except CancelledError:
|
||||
# Task was cancelled
|
||||
self.current_task.status = "cancelled"
|
||||
self.current_task.cancelled = True
|
||||
self.current_task.cancel_pending = False
|
||||
self.debugger.after_cancel(self.current_task)
|
||||
self.cancel(to_cancel)
|
||||
return all([t.cancelled or t.exc or t.finished for t in chain(self.tasks, self.paused, self.get_event_tasks())])
|
||||
|
||||
def close(self):
|
||||
def close(self, *, ensure_done: bool = True):
|
||||
"""
|
||||
Closes the event loop, terminating all tasks
|
||||
inside it and tearing down any extra machinery
|
||||
inside it and tearing down any extra machinery.
|
||||
If ensure_done equals False, the loop will cancel *ALL*
|
||||
running and scheduled tasks and then tear itself down.
|
||||
If ensure_done equals False, which is the default behavior,
|
||||
this method will raise a GiambioError if the loop hasn't
|
||||
finished running.
|
||||
"""
|
||||
|
||||
self.cancel_all()
|
||||
if ensure_done:
|
||||
self.cancel_all()
|
||||
elif not self.done():
|
||||
raise GiambioError("event loop not terminated, call this method with ensure_done=False to forcefully exit")
|
||||
self.shutdown()
|
||||
|
||||
def join(self, task: Task):
|
||||
|
@ -351,14 +377,23 @@ class AsyncScheduler:
|
|||
task.join() on the task object)
|
||||
"""
|
||||
|
||||
if self.current_pool is None:
|
||||
if not self.done():
|
||||
return
|
||||
else:
|
||||
self.reschedule_joiners(task)
|
||||
return
|
||||
task.joined = True
|
||||
if task.finished or task.cancelled:
|
||||
self.reschedule_joiners(task)
|
||||
if all([t.finished or t.cancelled for t in self.current_pool.tasks]):
|
||||
self.reschedule_joiners(task)
|
||||
elif task.exc:
|
||||
if not self.cancel_all_from_current_pool():
|
||||
if self.cancel_all_from_current_pool():
|
||||
# This will reschedule the parent
|
||||
# only if any enclosed pool has
|
||||
# already exited, which is what we want
|
||||
# only if all the tasks inside it
|
||||
# have finished executing, either
|
||||
# by cancellation, an exception
|
||||
# or just returned
|
||||
self.reschedule_joiners(task)
|
||||
|
||||
def sleep(self, seconds: int or float):
|
||||
|
@ -375,19 +410,30 @@ class AsyncScheduler:
|
|||
else:
|
||||
self.tasks.append(self.current_task)
|
||||
|
||||
def cancel(self, task: Task = None):
|
||||
def cancel(self, task: Task):
|
||||
"""
|
||||
Schedules the task to be cancelled later
|
||||
or does so straight away if it is safe to do so
|
||||
"""
|
||||
|
||||
task = task or self.current_task
|
||||
if not task.finished and not task.exc:
|
||||
if task.status in ("io", "sleep"):
|
||||
# We cancel right away
|
||||
if task.status in ("io", "sleep", "init"):
|
||||
# We cancel immediately only in a context where it's safer to do
|
||||
# so. The concept of "safer" is quite tricky, because even though the
|
||||
# task is technically not running, it might leave some unfinished state
|
||||
# or dangling resource open after being cancelled, so maybe we need
|
||||
# a different approach altogether
|
||||
try:
|
||||
self.do_cancel(task)
|
||||
else:
|
||||
task.cancel_pending = True # Cancellation is deferred
|
||||
except CancelledError:
|
||||
# Task was cancelled
|
||||
task.status = "cancelled"
|
||||
task.cancelled = True
|
||||
task.cancel_pending = False
|
||||
self.debugger.after_cancel(task)
|
||||
else:
|
||||
# If we can't cancel in a somewhat "graceful" way, we just
|
||||
# defer this operation for later (check run() for more info)
|
||||
task.cancel_pending = True # Cancellation is deferred
|
||||
|
||||
def event_set(self, event):
|
||||
"""
|
||||
|
@ -488,3 +534,10 @@ class AsyncScheduler:
|
|||
|
||||
await want_write(sock)
|
||||
return sock.connect(addr)
|
||||
|
||||
def __del__(self):
|
||||
"""
|
||||
Garbage collects itself
|
||||
"""
|
||||
|
||||
self.close()
|
||||
|
|
|
@ -16,9 +16,8 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
"""
|
||||
|
||||
|
||||
from typing import List
|
||||
import traceback
|
||||
from typing import List
|
||||
|
||||
|
||||
class GiambioError(Exception):
|
||||
|
|
|
@ -16,11 +16,11 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
"""
|
||||
|
||||
import types
|
||||
from .traps import join, cancel, event_set, event_wait
|
||||
from giambio.traps import join, cancel, event_set, event_wait
|
||||
from heapq import heappop, heappush
|
||||
from .exceptions import GiambioError
|
||||
from giambio.exceptions import GiambioError
|
||||
from dataclasses import dataclass, field
|
||||
import typing
|
||||
|
||||
|
||||
@dataclass
|
||||
|
@ -30,7 +30,7 @@ class Task:
|
|||
A simple wrapper around a coroutine object
|
||||
"""
|
||||
|
||||
coroutine: types.CoroutineType
|
||||
coroutine: typing.Coroutine
|
||||
name: str
|
||||
pool: "giambio.context.TaskManager"
|
||||
cancelled: bool = False
|
||||
|
@ -77,9 +77,6 @@ class Task:
|
|||
|
||||
await cancel(self)
|
||||
|
||||
def __del__(self):
|
||||
self.coroutine.close()
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.coroutine)
|
||||
|
||||
|
@ -129,6 +126,9 @@ class TimeQueue:
|
|||
|
||||
self.clock = clock
|
||||
self.sequence = 0
|
||||
# The sequence number handles the race condition
|
||||
# of two tasks with identical deadlines acting
|
||||
# as a tie breaker
|
||||
self.container = []
|
||||
|
||||
def __contains__(self, item):
|
||||
|
@ -152,7 +152,7 @@ class TimeQueue:
|
|||
def __repr__(self):
|
||||
return f"TimeQueue({self.container}, clock={self.clock})"
|
||||
|
||||
def put(self, item, amount):
|
||||
def put(self, item, amount: float):
|
||||
"""
|
||||
Pushes an item onto the queue with its unique
|
||||
time amount and ID
|
||||
|
@ -167,3 +167,49 @@ class TimeQueue:
|
|||
"""
|
||||
|
||||
return heappop(self.container)[2]
|
||||
|
||||
|
||||
class DeadlinesQueue(TimeQueue):
|
||||
"""
|
||||
An ordered queue for storing tasks deadlines
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""
|
||||
Object constructor
|
||||
"""
|
||||
|
||||
super().__init__(None)
|
||||
|
||||
def __contains__(self, item):
|
||||
return super().__contains__(item)
|
||||
|
||||
def __iter__(self):
|
||||
return super().__iter__()
|
||||
|
||||
def __next__(self):
|
||||
return super().__next__()
|
||||
|
||||
def __getitem__(self, item):
|
||||
return super().__getitem__(item)
|
||||
|
||||
def __bool__(self):
|
||||
return super().__bool__()
|
||||
|
||||
def __repr__(self):
|
||||
return f"DeadlinesQueue({self.container})"
|
||||
|
||||
def put(self, amount: float):
|
||||
"""
|
||||
Pushes a deadline (timeout) onto the queue
|
||||
"""
|
||||
|
||||
heappush(self.container, (amount, self.sequence))
|
||||
self.sequence += 1
|
||||
|
||||
def get(self):
|
||||
"""
|
||||
Gets the first task that is meant to run
|
||||
"""
|
||||
|
||||
return super().get()
|
||||
|
|
|
@ -18,11 +18,11 @@ limitations under the License.
|
|||
|
||||
import inspect
|
||||
import threading
|
||||
from .core import AsyncScheduler
|
||||
from .exceptions import GiambioError
|
||||
from .context import TaskManager
|
||||
from giambio.core import AsyncScheduler
|
||||
from giambio.exceptions import GiambioError
|
||||
from giambio.context import TaskManager
|
||||
from timeit import default_timer
|
||||
from .util.debug import BaseDebugger
|
||||
from giambio.util.debug import BaseDebugger
|
||||
from types import FunctionType
|
||||
|
||||
|
||||
|
@ -70,7 +70,7 @@ def run(func: FunctionType, *args, **kwargs):
|
|||
if inspect.iscoroutine(func):
|
||||
raise GiambioError("Looks like you tried to call giambio.run(your_func(arg1, arg2, ...)), that is wrong!"
|
||||
"\nWhat you wanna do, instead, is this: giambio.run(your_func, arg1, arg2, ...)")
|
||||
elif not isinstance(func, FunctionType):
|
||||
elif not inspect.iscoroutinefunction(func):
|
||||
raise GiambioError("giambio.run() requires an async function as parameter!")
|
||||
new_event_loop(kwargs.get("debugger", None), kwargs.get("clock", default_timer))
|
||||
get_event_loop().start(func, *args)
|
||||
|
|
|
@ -16,9 +16,9 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
"""
|
||||
|
||||
from .run import get_event_loop
|
||||
import socket
|
||||
from .exceptions import ResourceClosed
|
||||
from giambio.run import get_event_loop
|
||||
from giambio.exceptions import ResourceClosed
|
||||
|
||||
|
||||
class AsyncSocket:
|
||||
|
|
|
@ -39,7 +39,7 @@ def create_trap(method, *args):
|
|||
async def sleep(seconds: int):
|
||||
"""
|
||||
Pause the execution of an async function for a given amount of seconds,
|
||||
without blocking the entire event loop, which keeps watching for other events
|
||||
without blocking the entire event loop, which keeps watching for other events.
|
||||
|
||||
This function is also useful as a sort of checkpoint, because it returns
|
||||
control to the scheduler, which can then switch to another task. If your code
|
||||
|
@ -78,7 +78,7 @@ async def join(task):
|
|||
|
||||
async def cancel(task):
|
||||
"""
|
||||
Cancels the given task
|
||||
Cancels the given task.
|
||||
|
||||
The concept of cancellation is tricky, because there is no real way to 'stop'
|
||||
a task if not by raising an exception inside it and ignoring whatever it
|
||||
|
@ -128,7 +128,11 @@ async def event_set(event):
|
|||
async def event_wait(event):
|
||||
"""
|
||||
Notifies the event loop that the current task has to wait
|
||||
for the given event to trigger
|
||||
for the given event to trigger. This trap returns
|
||||
immediately if the event has already been set
|
||||
"""
|
||||
|
||||
if event.set:
|
||||
return
|
||||
await create_trap("event_wait", event)
|
||||
|
||||
|
|
|
@ -20,193 +20,192 @@ from giambio.objects import Task
|
|||
|
||||
|
||||
class BaseDebugger(ABC):
|
||||
"""
|
||||
The base for all debugger objects
|
||||
"""
|
||||
"""
|
||||
The base for all debugger objects
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def on_start(self):
|
||||
"""
|
||||
This method is called when the event
|
||||
loop starts executing
|
||||
"""
|
||||
@abstractmethod
|
||||
def on_start(self):
|
||||
"""
|
||||
This method is called when the event
|
||||
loop starts executing
|
||||
"""
|
||||
|
||||
raise NotImplementedError
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def on_exit(self):
|
||||
"""
|
||||
This method is called when the event
|
||||
loop exits entirely (all tasks completed)
|
||||
"""
|
||||
@abstractmethod
|
||||
def on_exit(self):
|
||||
"""
|
||||
This method is called when the event
|
||||
loop exits entirely (all tasks completed)
|
||||
"""
|
||||
|
||||
raise NotImplementedError
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def on_task_schedule(self, task: Task, delay: float):
|
||||
"""
|
||||
This method is called when a new task is
|
||||
scheduled (not spawned)
|
||||
@abstractmethod
|
||||
def on_task_schedule(self, task: Task, delay: float):
|
||||
"""
|
||||
This method is called when a new task is
|
||||
scheduled (not spawned)
|
||||
|
||||
:param task: The Task object representing a
|
||||
giambio Task and wrapping a coroutine
|
||||
:type task: :class: giambio.objects.Task
|
||||
:param delay: The delay, in seconds, after which
|
||||
the task will start executing
|
||||
:type delay: float
|
||||
"""
|
||||
:param task: The Task object representing a
|
||||
giambio Task and wrapping a coroutine
|
||||
:type task: :class: giambio.objects.Task
|
||||
:param delay: The delay, in seconds, after which
|
||||
the task will start executing
|
||||
:type delay: float
|
||||
"""
|
||||
|
||||
raise NotImplementedError
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def on_task_spawn(self, task: Task):
|
||||
"""
|
||||
This method is called when a new task is
|
||||
spawned
|
||||
@abstractmethod
|
||||
def on_task_spawn(self, task: Task):
|
||||
"""
|
||||
This method is called when a new task is
|
||||
spawned
|
||||
|
||||
:param task: The Task object representing a
|
||||
giambio Task and wrapping a coroutine
|
||||
:type task: :class: giambio.objects.Task
|
||||
"""
|
||||
:param task: The Task object representing a
|
||||
giambio Task and wrapping a coroutine
|
||||
:type task: :class: giambio.objects.Task
|
||||
"""
|
||||
|
||||
raise NotImplementedError
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def on_task_exit(self, task: Task):
|
||||
"""
|
||||
This method is called when a task exits
|
||||
@abstractmethod
|
||||
def on_task_exit(self, task: Task):
|
||||
"""
|
||||
This method is called when a task exits
|
||||
|
||||
:param task: The Task object representing a
|
||||
giambio Task and wrapping a coroutine
|
||||
:type task: :class: giambio.objects.Task
|
||||
"""
|
||||
:param task: The Task object representing a
|
||||
giambio Task and wrapping a coroutine
|
||||
:type task: :class: giambio.objects.Task
|
||||
"""
|
||||
|
||||
raise NotImplementedError
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def before_task_step(self, task: Task):
|
||||
"""
|
||||
This method is called right before
|
||||
calling a task's run() method
|
||||
@abstractmethod
|
||||
def before_task_step(self, task: Task):
|
||||
"""
|
||||
This method is called right before
|
||||
calling a task's run() method
|
||||
|
||||
:param task: The Task object representing a
|
||||
giambio Task and wrapping a coroutine
|
||||
:type task: :class: giambio.objects.Task
|
||||
"""
|
||||
:param task: The Task object representing a
|
||||
giambio Task and wrapping a coroutine
|
||||
:type task: :class: giambio.objects.Task
|
||||
"""
|
||||
|
||||
raise NotImplementedError
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def after_task_step(self, task: Task):
|
||||
"""
|
||||
This method is called right after
|
||||
calling a task's run() method
|
||||
@abstractmethod
|
||||
def after_task_step(self, task: Task):
|
||||
"""
|
||||
This method is called right after
|
||||
calling a task's run() method
|
||||
|
||||
:param task: The Task object representing a
|
||||
giambio Task and wrapping a coroutine
|
||||
:type task: :class: giambio.objects.Task
|
||||
"""
|
||||
:param task: The Task object representing a
|
||||
giambio Task and wrapping a coroutine
|
||||
:type task: :class: giambio.objects.Task
|
||||
"""
|
||||
|
||||
raise NotImplementedError
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def before_sleep(self, task: Task, seconds: float):
|
||||
"""
|
||||
This method is called before a task goes
|
||||
to sleep
|
||||
@abstractmethod
|
||||
def before_sleep(self, task: Task, seconds: float):
|
||||
"""
|
||||
This method is called before a task goes
|
||||
to sleep
|
||||
|
||||
:param task: The Task object representing a
|
||||
giambio Task and wrapping a coroutine
|
||||
:type task: :class: giambio.objects.Task
|
||||
:param seconds: The amount of seconds the
|
||||
task wants to sleep
|
||||
:type seconds: int
|
||||
"""
|
||||
:param task: The Task object representing a
|
||||
giambio Task and wrapping a coroutine
|
||||
:type task: :class: giambio.objects.Task
|
||||
:param seconds: The amount of seconds the
|
||||
task wants to sleep
|
||||
:type seconds: int
|
||||
"""
|
||||
|
||||
raise NotImplementedError
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def after_sleep(self, task: Task, seconds: float):
|
||||
"""
|
||||
This method is called after a tasks
|
||||
awakes from sleeping
|
||||
@abstractmethod
|
||||
def after_sleep(self, task: Task, seconds: float):
|
||||
"""
|
||||
This method is called after a tasks
|
||||
awakes from sleeping
|
||||
|
||||
:param task: The Task object representing a
|
||||
giambio Task and wrapping a coroutine
|
||||
:type task: :class: giambio.objects.Task
|
||||
:param seconds: The amount of seconds the
|
||||
task actually slept
|
||||
:type seconds: float
|
||||
"""
|
||||
:param task: The Task object representing a
|
||||
giambio Task and wrapping a coroutine
|
||||
:type task: :class: giambio.objects.Task
|
||||
:param seconds: The amount of seconds the
|
||||
task actually slept
|
||||
:type seconds: float
|
||||
"""
|
||||
|
||||
raise NotImplementedError
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def before_io(self, timeout: float):
|
||||
"""
|
||||
This method is called right before
|
||||
the event loop checks for I/O events
|
||||
@abstractmethod
|
||||
def before_io(self, timeout: float):
|
||||
"""
|
||||
This method is called right before
|
||||
the event loop checks for I/O events
|
||||
|
||||
:param timeout: The max. amount of seconds
|
||||
that the loop will hang when using the select()
|
||||
system call
|
||||
:type timeout: float
|
||||
"""
|
||||
:param timeout: The max. amount of seconds
|
||||
that the loop will hang when using the select()
|
||||
system call
|
||||
:type timeout: float
|
||||
"""
|
||||
|
||||
raise NotImplementedError
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def after_io(self, timeout: float):
|
||||
"""
|
||||
This method is called right after
|
||||
the event loop has checked for I/O events
|
||||
@abstractmethod
|
||||
def after_io(self, timeout: float):
|
||||
"""
|
||||
This method is called right after
|
||||
the event loop has checked for I/O events
|
||||
|
||||
:param timeout: The actual amount of seconds
|
||||
that the loop has hung when using the select()
|
||||
system call
|
||||
:type timeout: float
|
||||
"""
|
||||
:param timeout: The actual amount of seconds
|
||||
that the loop has hung when using the select()
|
||||
system call
|
||||
:type timeout: float
|
||||
"""
|
||||
|
||||
raise NotImplementedError
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def before_cancel(self, task: Task):
|
||||
"""
|
||||
This method is called right before a task
|
||||
gets cancelled
|
||||
@abstractmethod
|
||||
def before_cancel(self, task: Task):
|
||||
"""
|
||||
This method is called right before a task
|
||||
gets cancelled
|
||||
|
||||
:param task: The Task object representing a
|
||||
giambio Task and wrapping a coroutine
|
||||
:type task: :class: giambio.objects.Task
|
||||
"""
|
||||
:param task: The Task object representing a
|
||||
giambio Task and wrapping a coroutine
|
||||
:type task: :class: giambio.objects.Task
|
||||
"""
|
||||
|
||||
raise NotImplementedError
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def after_cancel(self, task: Task):
|
||||
"""
|
||||
This method is called right after a task
|
||||
gets cancelled
|
||||
@abstractmethod
|
||||
def after_cancel(self, task: Task) -> object:
|
||||
"""
|
||||
This method is called right after a task
|
||||
gets cancelled
|
||||
|
||||
:param task: The Task object representing a
|
||||
giambio Task and wrapping a coroutine
|
||||
:type task: :class: giambio.objects.Task
|
||||
"""
|
||||
:param task: The Task object representing a
|
||||
giambio Task and wrapping a coroutine
|
||||
:type task: :class: giambio.objects.Task
|
||||
"""
|
||||
|
||||
raise NotImplementedError
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def on_exception_raised(self, task: Task, exc: BaseException):
|
||||
"""
|
||||
This method is called right after a task
|
||||
has raised an exception
|
||||
@abstractmethod
|
||||
def on_exception_raised(self, task: Task, exc: BaseException):
|
||||
"""
|
||||
This method is called right after a task
|
||||
has raised an exception
|
||||
|
||||
:param task: The Task object representing a
|
||||
giambio Task and wrapping a coroutine
|
||||
:type task: :class: giambio.objects.Task
|
||||
:param exc: The exception that was raised
|
||||
:type exc: BaseException
|
||||
"""
|
||||
|
||||
raise NotImplementedError
|
||||
:param task: The Task object representing a
|
||||
giambio Task and wrapping a coroutine
|
||||
:type task: :class: giambio.objects.Task
|
||||
:param exc: The exception that was raised
|
||||
:type exc: BaseException
|
||||
"""
|
||||
|
||||
raise NotImplementedError
|
||||
|
|
|
@ -17,7 +17,7 @@ async def child1():
|
|||
async def main():
|
||||
start = giambio.clock()
|
||||
async with giambio.create_pool() as pool:
|
||||
pool.spawn(child)
|
||||
pool.spawn(child) # If you comment this line, the pool will exit immediately!
|
||||
task = pool.spawn(child1)
|
||||
await task.cancel()
|
||||
print("[main] Children spawned, awaiting completion")
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
import giambio
|
||||
from debugger import Debugger
|
||||
|
||||
|
||||
async def child():
|
||||
print("[child] Child spawned!! Sleeping for 2 seconds")
|
||||
await giambio.sleep(2)
|
||||
print("[child] Had a nice nap!")
|
||||
|
||||
|
||||
async def child1():
|
||||
print("[child 1] Child spawned!! Sleeping for 2 seconds")
|
||||
await giambio.sleep(2)
|
||||
print("[child 1] Had a nice nap!")
|
||||
|
||||
|
||||
async def child2():
|
||||
print("[child 2] Child spawned!! Sleeping for 2 seconds")
|
||||
await giambio.sleep(2)
|
||||
print("[child 2] Had a nice nap!")
|
||||
|
||||
|
||||
async def main():
|
||||
start = giambio.clock()
|
||||
async with giambio.create_pool() as pool:
|
||||
pool.spawn(child)
|
||||
pool.spawn(child1)
|
||||
# async with giambio.create_pool() as a_pool:
|
||||
# a_pool.spawn(child2)
|
||||
await pool.cancel() # This cancels the *whole* block
|
||||
print("[main] Children spawned, awaiting completion")
|
||||
print(f"[main] Children execution complete in {giambio.clock() - start:.2f} seconds")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
giambio.run(main, debugger=Debugger())
|
|
@ -1,34 +0,0 @@
|
|||
import giambio
|
||||
from debugger import Debugger
|
||||
|
||||
|
||||
# TODO: How to create a race condition of 2 exceptions at the same time?
|
||||
|
||||
async def child():
|
||||
print("[child] Child spawned!! Sleeping for 2 seconds")
|
||||
await giambio.sleep(2)
|
||||
print("[child] Had a nice nap!")
|
||||
|
||||
|
||||
async def child1():
|
||||
print("[child 1] Child spawned!! Sleeping for 2 seconds")
|
||||
await giambio.sleep(2)
|
||||
print("[child 1] Had a nice nap!")
|
||||
raise Exception("bruh")
|
||||
|
||||
|
||||
async def main():
|
||||
start = giambio.clock()
|
||||
try:
|
||||
async with giambio.create_pool() as pool:
|
||||
pool.spawn(child)
|
||||
pool.spawn(child1)
|
||||
print("[main] Children spawned, awaiting completion")
|
||||
except Exception as error:
|
||||
# Because exceptions just *work*!
|
||||
print(f"[main] Exception from child caught! {repr(error)}")
|
||||
print(f"[main] Children execution complete in {giambio.clock() - start:.2f} seconds")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
giambio.run(main, debugger=())
|
|
@ -6,13 +6,14 @@ async def child():
|
|||
print("[child] Child spawned!! Sleeping for 2 seconds")
|
||||
await giambio.sleep(2)
|
||||
print("[child] Had a nice nap!")
|
||||
raise TypeError("rip") # Watch the exception magically propagate!
|
||||
# raise TypeError("rip")
|
||||
|
||||
|
||||
async def child1():
|
||||
print("[child 1] Child spawned!! Sleeping for 2 seconds")
|
||||
await giambio.sleep(2)
|
||||
print("[child 1] Child spawned!! Sleeping for 4 seconds")
|
||||
await giambio.sleep(4)
|
||||
print("[child 1] Had a nice nap!")
|
||||
raise TypeError("rip")
|
||||
|
||||
|
||||
async def main():
|
||||
|
@ -29,4 +30,4 @@ async def main():
|
|||
|
||||
|
||||
if __name__ == "__main__":
|
||||
giambio.run(main, debugger=Debugger())
|
||||
giambio.run(main, debugger=())
|
||||
|
|
|
@ -21,6 +21,12 @@ async def child2():
|
|||
print("[child 2] Had a nice nap!")
|
||||
|
||||
|
||||
async def child3():
|
||||
print("[child 3] Child spawned!! Sleeping for 6 seconds")
|
||||
await giambio.sleep(6)
|
||||
print("[child 3] Had a nice nap!")
|
||||
|
||||
|
||||
async def main():
|
||||
start = giambio.clock()
|
||||
try:
|
||||
|
@ -29,7 +35,12 @@ async def main():
|
|||
pool.spawn(child1)
|
||||
print("[main] Children spawned, awaiting completion")
|
||||
async with giambio.create_pool() as new_pool:
|
||||
# This pool won't be affected from exceptions
|
||||
# in outer pools. This is a guarantee that giambio
|
||||
# ensures: an exception will only be propagated
|
||||
# after all enclosed task pools have exited
|
||||
new_pool.spawn(child2)
|
||||
new_pool.spawn(child3)
|
||||
print("[main] 3rd child spawned")
|
||||
except Exception as error:
|
||||
# Because exceptions just *work*!
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import giambio
|
||||
from giambio.socket import AsyncSocket
|
||||
from debugger import Debugger
|
||||
import socket
|
||||
import logging
|
||||
import sys
|
||||
|
@ -45,9 +46,8 @@ if __name__ == "__main__":
|
|||
port = int(sys.argv[1]) if len(sys.argv) > 1 else 1500
|
||||
logging.basicConfig(level=20, format="[%(levelname)s] %(asctime)s %(message)s", datefmt="%d/%m/%Y %p")
|
||||
try:
|
||||
giambio.run(serve, ("localhost", port))
|
||||
giambio.run(serve, ("localhost", port), debugger=None)
|
||||
except (Exception, KeyboardInterrupt) as error: # Exceptions propagate!
|
||||
raise
|
||||
if isinstance(error, KeyboardInterrupt):
|
||||
logging.info("Ctrl+C detected, exiting")
|
||||
else:
|
||||
|
|
|
@ -2,8 +2,8 @@ import giambio
|
|||
|
||||
|
||||
async def child():
|
||||
print("[child] Child spawned!! Sleeping for 2 seconds")
|
||||
await giambio.sleep(2)
|
||||
print("[child] Child spawned!! Sleeping for 4 seconds")
|
||||
await giambio.sleep(4)
|
||||
print("[child] Had a nice nap!")
|
||||
|
||||
|
||||
|
|
Loading…
Reference in New Issue