Added many fixes for exception propagation and SIGINT handling
This commit is contained in:
parent
e730f7f27a
commit
509b555628
|
@ -17,6 +17,7 @@ limitations under the License.
|
||||||
"""
|
"""
|
||||||
from aiosched.runtime import run, get_event_loop, new_event_loop, clock, with_context
|
from aiosched.runtime import run, get_event_loop, new_event_loop, clock, with_context
|
||||||
from aiosched.internals.syscalls import spawn, wait, sleep, cancel, checkpoint, join
|
from aiosched.internals.syscalls import spawn, wait, sleep, cancel, checkpoint, join
|
||||||
|
import aiosched.util
|
||||||
import aiosched.task
|
import aiosched.task
|
||||||
import aiosched.errors
|
import aiosched.errors
|
||||||
import aiosched.context
|
import aiosched.context
|
||||||
|
@ -41,4 +42,5 @@ __all__ = [
|
||||||
"checkpoint",
|
"checkpoint",
|
||||||
"NetworkChannel",
|
"NetworkChannel",
|
||||||
"socket",
|
"socket",
|
||||||
|
"util"
|
||||||
]
|
]
|
||||||
|
|
|
@ -15,29 +15,23 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
See the License for the specific language governing permissions and
|
See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
"""
|
"""
|
||||||
from aiosched.task import Task, TaskState
|
from aiosched.task import Task
|
||||||
from aiosched.internals.syscalls import (
|
from aiosched.internals.syscalls import (
|
||||||
spawn,
|
spawn,
|
||||||
wait,
|
wait,
|
||||||
cancel,
|
cancel,
|
||||||
set_context,
|
join,
|
||||||
close_context,
|
current_task
|
||||||
join
|
|
||||||
)
|
)
|
||||||
from typing import Any, Coroutine, Callable
|
from typing import Any, Coroutine, Callable
|
||||||
|
|
||||||
|
|
||||||
class TaskContext(Task):
|
class TaskContext:
|
||||||
"""
|
"""
|
||||||
An asynchronous context manager that automatically waits
|
An asynchronous context manager that automatically waits
|
||||||
for all tasks spawned within it and cancels itself when
|
for all tasks spawned within it and cancels itself when
|
||||||
an exception occurs. A TaskContext object behaves like
|
an exception occurs. Contexts can be nested and will
|
||||||
a regular task and the event loop treats it like a single
|
cancel inner ones if an exception is raised inside them
|
||||||
unit rather than a collection of tasks (in fact, the event
|
|
||||||
loop doesn't even know, nor care about, whether the current
|
|
||||||
task is a task context or not, which is by design). Contexts
|
|
||||||
can be nested and will cancel inner ones if an exception is
|
|
||||||
raised inside them
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, silent: bool = False, gather: bool = True, timeout: int | float = 0.0) -> None:
|
def __init__(self, silent: bool = False, gather: bool = True, timeout: int | float = 0.0) -> None:
|
||||||
|
@ -59,6 +53,8 @@ class TaskContext(Task):
|
||||||
# For how long do we allow tasks inside us
|
# For how long do we allow tasks inside us
|
||||||
# to run?
|
# to run?
|
||||||
self.timeout: int | float = timeout # TODO: Implement
|
self.timeout: int | float = timeout # TODO: Implement
|
||||||
|
# Have we crashed?
|
||||||
|
self.error: BaseException | None = None
|
||||||
|
|
||||||
async def spawn(
|
async def spawn(
|
||||||
self, func: Callable[..., Coroutine[Any, Any, Any]], *args, **kwargs
|
self, func: Callable[..., Coroutine[Any, Any, Any]], *args, **kwargs
|
||||||
|
@ -78,7 +74,7 @@ class TaskContext(Task):
|
||||||
Implements the asynchronous context manager interface
|
Implements the asynchronous context manager interface
|
||||||
"""
|
"""
|
||||||
|
|
||||||
await set_context(self)
|
self.entry_point = await current_task()
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def __eq__(self, other):
|
def __eq__(self, other):
|
||||||
|
@ -114,12 +110,13 @@ class TaskContext(Task):
|
||||||
await wait(task)
|
await wait(task)
|
||||||
except BaseException as exc:
|
except BaseException as exc:
|
||||||
await self.cancel(False)
|
await self.cancel(False)
|
||||||
self.exc = exc
|
self.error = exc
|
||||||
finally:
|
finally:
|
||||||
await close_context(self)
|
|
||||||
self.entry_point.propagate = True
|
self.entry_point.propagate = True
|
||||||
if self.exc and not self.silent:
|
if self.silent:
|
||||||
raise self.exc
|
return
|
||||||
|
if self.entry_point.exc:
|
||||||
|
raise self.entry_point.exc
|
||||||
|
|
||||||
# Task method wrappers
|
# Task method wrappers
|
||||||
|
|
||||||
|
@ -139,7 +136,6 @@ class TaskContext(Task):
|
||||||
task: TaskContext
|
task: TaskContext
|
||||||
await task.cancel(propagate)
|
await task.cancel(propagate)
|
||||||
self.cancelled = True
|
self.cancelled = True
|
||||||
self.propagate = False
|
|
||||||
if propagate:
|
if propagate:
|
||||||
if isinstance(self.entry_point, Task):
|
if isinstance(self.entry_point, Task):
|
||||||
await cancel(self.entry_point)
|
await cancel(self.entry_point)
|
||||||
|
@ -158,66 +154,6 @@ class TaskContext(Task):
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@property
|
|
||||||
def state(self) -> int:
|
|
||||||
return self.entry_point.state
|
|
||||||
|
|
||||||
@state.setter
|
|
||||||
def state(self, state: int):
|
|
||||||
self.entry_point.state = state
|
|
||||||
|
|
||||||
@property
|
|
||||||
def result(self) -> Any:
|
|
||||||
return self.entry_point.result
|
|
||||||
|
|
||||||
@result.setter
|
|
||||||
def result(self, result: Any):
|
|
||||||
self.entry_point.result = result
|
|
||||||
|
|
||||||
@property
|
|
||||||
def exc(self) -> BaseException:
|
|
||||||
return self.entry_point.exc
|
|
||||||
|
|
||||||
@exc.setter
|
|
||||||
def exc(self, exc: BaseException):
|
|
||||||
self.entry_point.exc = exc
|
|
||||||
|
|
||||||
@property
|
|
||||||
def propagate(self) -> bool:
|
|
||||||
return self.entry_point.propagate
|
|
||||||
|
|
||||||
@propagate.setter
|
|
||||||
def propagate(self, val: bool):
|
|
||||||
self.entry_point.propagate = val
|
|
||||||
|
|
||||||
@property
|
|
||||||
def name(self):
|
|
||||||
return self.entry_point.name
|
|
||||||
|
|
||||||
def throw(self, err: BaseException):
|
|
||||||
for task in self.tasks:
|
|
||||||
if task is self.entry_point:
|
|
||||||
continue
|
|
||||||
try:
|
|
||||||
task.exc = err
|
|
||||||
task.state = TaskState.CRASHED
|
|
||||||
task.throw(err)
|
|
||||||
finally:
|
|
||||||
continue
|
|
||||||
self.entry_point.throw(err)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def joiners(self) -> set[Task]:
|
|
||||||
return self.entry_point.joiners
|
|
||||||
|
|
||||||
@joiners.setter
|
|
||||||
def joiners(self, joiners: set[Task]):
|
|
||||||
self.entry_point.joiners = joiners
|
|
||||||
|
|
||||||
@property
|
|
||||||
def coroutine(self):
|
|
||||||
return self.entry_point.coroutine
|
|
||||||
|
|
||||||
def __hash__(self):
|
def __hash__(self):
|
||||||
return self.entry_point.__hash__()
|
return self.entry_point.__hash__()
|
||||||
|
|
||||||
|
@ -239,10 +175,7 @@ class TaskContext(Task):
|
||||||
|
|
||||||
result = "TaskContext(["
|
result = "TaskContext(["
|
||||||
for i, task in enumerate(self.tasks):
|
for i, task in enumerate(self.tasks):
|
||||||
if task is self.entry_point:
|
result += repr(task)
|
||||||
result += repr(self.entry_point)
|
|
||||||
else:
|
|
||||||
result += repr(task)
|
|
||||||
if i < len(self.tasks) - 1:
|
if i < len(self.tasks) - 1:
|
||||||
result += ", "
|
result += ", "
|
||||||
result += "])"
|
result += "])"
|
||||||
|
|
|
@ -149,16 +149,11 @@ async def wait(task: Task) -> Any | None:
|
||||||
:returns: The task's return value, if any
|
:returns: The task's return value, if any
|
||||||
"""
|
"""
|
||||||
|
|
||||||
current = await current_task()
|
if task == await current_task():
|
||||||
if task == current:
|
|
||||||
# We don't do an "x is y" check because
|
# We don't do an "x is y" check because
|
||||||
# tasks and task contexts can compare equal
|
# tasks and task contexts can compare equal
|
||||||
# despite having different memory addresses
|
# despite having different memory addresses
|
||||||
raise SchedulerError("a task cannot join itself")
|
raise SchedulerError("a task cannot join itself")
|
||||||
if current not in task.joiners:
|
|
||||||
# Luckily we use a set, so this has O(1)
|
|
||||||
# complexity on average
|
|
||||||
await join(task) # Waiting implies joining!
|
|
||||||
await syscall("wait", task)
|
await syscall("wait", task)
|
||||||
if task.exc and task.state != TaskState.CANCELLED and task.propagate:
|
if task.exc and task.state != TaskState.CANCELLED and task.propagate:
|
||||||
# The task raised an error that wasn't directly caused by a cancellation:
|
# The task raised an error that wasn't directly caused by a cancellation:
|
||||||
|
@ -228,19 +223,3 @@ async def io_release(stream):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
await syscall("io_release", stream)
|
await syscall("io_release", stream)
|
||||||
|
|
||||||
|
|
||||||
async def set_context(ctx):
|
|
||||||
"""
|
|
||||||
Sets the current task context
|
|
||||||
"""
|
|
||||||
|
|
||||||
await syscall("set_context", ctx)
|
|
||||||
|
|
||||||
|
|
||||||
async def close_context(ctx):
|
|
||||||
"""
|
|
||||||
Closes the current task context
|
|
||||||
"""
|
|
||||||
|
|
||||||
await syscall("close_context", ctx)
|
|
||||||
|
|
|
@ -32,7 +32,6 @@ from aiosched.errors import (
|
||||||
ResourceClosed,
|
ResourceClosed,
|
||||||
ResourceBroken,
|
ResourceBroken,
|
||||||
)
|
)
|
||||||
from aiosched.context import TaskContext
|
|
||||||
from selectors import DefaultSelector, BaseSelector, EVENT_READ, EVENT_WRITE
|
from selectors import DefaultSelector, BaseSelector, EVENT_READ, EVENT_WRITE
|
||||||
|
|
||||||
|
|
||||||
|
@ -121,6 +120,11 @@ class FIFOKernel:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
self._sigint_handled = True
|
self._sigint_handled = True
|
||||||
|
# We reschedule the current task
|
||||||
|
# immediately no matter what it's
|
||||||
|
# doing so that we process the
|
||||||
|
# exception immediately
|
||||||
|
self.reschedule_running()
|
||||||
|
|
||||||
def done(self) -> bool:
|
def done(self) -> bool:
|
||||||
"""
|
"""
|
||||||
|
@ -280,23 +284,23 @@ class FIFOKernel:
|
||||||
# there are no more runnable tasks
|
# there are no more runnable tasks
|
||||||
return
|
return
|
||||||
self.current_task = self.run_ready.popleft()
|
self.current_task = self.run_ready.popleft()
|
||||||
# We nullify the exception object just in case the
|
|
||||||
# entry point raised and caught an error so that
|
|
||||||
# self.start() doesn't raise it again at the end
|
|
||||||
self.current_task.exc = None
|
|
||||||
self._running = True
|
self._running = True
|
||||||
# Some debugging and internal chatter here
|
|
||||||
self.current_task.state = TaskState.RUN
|
|
||||||
self.current_task.steps += 1
|
|
||||||
if self._sigint_handled:
|
if self._sigint_handled:
|
||||||
self._sigint_handled = False
|
self._sigint_handled = False
|
||||||
|
self.reschedule_running()
|
||||||
self.current_task.throw(KeyboardInterrupt())
|
self.current_task.throw(KeyboardInterrupt())
|
||||||
self.join(self.current_task)
|
|
||||||
elif self.current_task.pending_cancellation:
|
elif self.current_task.pending_cancellation:
|
||||||
# We perform the deferred cancellation
|
# We perform the deferred cancellation
|
||||||
# if it was previously scheduled
|
# if it was previously scheduled
|
||||||
self.cancel(self.current_task)
|
self.cancel(self.current_task)
|
||||||
|
elif exc := self.current_task.pending_exception:
|
||||||
|
self.current_task.pending_exception = None
|
||||||
|
self.reschedule_running()
|
||||||
|
self.current_task.throw(exc)
|
||||||
else:
|
else:
|
||||||
|
# Some debugging and internal chatter here
|
||||||
|
self.current_task.steps += 1
|
||||||
|
self.current_task.state = TaskState.RUN
|
||||||
self.debugger.before_task_step(self.current_task)
|
self.debugger.before_task_step(self.current_task)
|
||||||
# Run a single step with the calculation (i.e. until a yield
|
# Run a single step with the calculation (i.e. until a yield
|
||||||
# somewhere)
|
# somewhere)
|
||||||
|
@ -351,6 +355,8 @@ class FIFOKernel:
|
||||||
task = next(iter(next(iter(self.selector.get_map().values())).data.values()))
|
task = next(iter(next(iter(self.selector.get_map().values())).data.values()))
|
||||||
elif self.paused:
|
elif self.paused:
|
||||||
task, *_ = self.paused.get()
|
task, *_ = self.paused.get()
|
||||||
|
else:
|
||||||
|
task = self.current_task
|
||||||
self.run_ready.append(task)
|
self.run_ready.append(task)
|
||||||
self.handle_errors(self.run_task_step)
|
self.handle_errors(self.run_task_step)
|
||||||
elif not self.run_ready:
|
elif not self.run_ready:
|
||||||
|
@ -382,7 +388,6 @@ class FIFOKernel:
|
||||||
try:
|
try:
|
||||||
self.run()
|
self.run()
|
||||||
finally:
|
finally:
|
||||||
self.debugger.on_exit()
|
|
||||||
signal.signal(signal.SIGINT, old)
|
signal.signal(signal.SIGINT, old)
|
||||||
if (
|
if (
|
||||||
self.entry_point.exc
|
self.entry_point.exc
|
||||||
|
@ -393,6 +398,7 @@ class FIFOKernel:
|
||||||
# no need to raise it manually. If a context
|
# no need to raise it manually. If a context
|
||||||
# is not used, *then* we can raise the error
|
# is not used, *then* we can raise the error
|
||||||
raise self.entry_point.exc
|
raise self.entry_point.exc
|
||||||
|
self.debugger.on_exit()
|
||||||
return self.entry_point.result
|
return self.entry_point.result
|
||||||
|
|
||||||
def io_release(self, resource):
|
def io_release(self, resource):
|
||||||
|
@ -415,9 +421,12 @@ class FIFOKernel:
|
||||||
for each I/O resource the given task owns
|
for each I/O resource the given task owns
|
||||||
"""
|
"""
|
||||||
|
|
||||||
for key in filter(
|
for key in dict(self.selector.get_map()).values():
|
||||||
lambda k: task in k.data.values(), dict(self.selector.get_map()).values()
|
if task not in key.data.values():
|
||||||
):
|
continue
|
||||||
|
if len(key.data.values()) == 2:
|
||||||
|
if key.data.values()[0] != task or key.data.values[1] != task:
|
||||||
|
continue
|
||||||
self.notify_closing(key.fileobj, broken=True)
|
self.notify_closing(key.fileobj, broken=True)
|
||||||
self.selector.unregister(key.fileobj)
|
self.selector.unregister(key.fileobj)
|
||||||
task.last_io = ()
|
task.last_io = ()
|
||||||
|
@ -457,8 +466,7 @@ class FIFOKernel:
|
||||||
if task is not self.current_task:
|
if task is not self.current_task:
|
||||||
# We don't want to raise an error inside
|
# We don't want to raise an error inside
|
||||||
# the task that's trying to close the stream!
|
# the task that's trying to close the stream!
|
||||||
for t in k.data:
|
self.handle_errors(partial(task.throw, exc), task)
|
||||||
self.handle_errors(partial(t.throw, exc), k.data)
|
|
||||||
self.reschedule_running()
|
self.reschedule_running()
|
||||||
|
|
||||||
def cancel(self, task: Task):
|
def cancel(self, task: Task):
|
||||||
|
@ -471,8 +479,9 @@ class FIFOKernel:
|
||||||
self.handle_errors(partial(task.throw, Cancelled(task)), task)
|
self.handle_errors(partial(task.throw, Cancelled(task)), task)
|
||||||
if task.state != TaskState.CANCELLED:
|
if task.state != TaskState.CANCELLED:
|
||||||
task.pending_cancellation = True
|
task.pending_cancellation = True
|
||||||
self.io_release_task(task)
|
else:
|
||||||
self.paused.discard(task)
|
self.io_release_task(task)
|
||||||
|
self.paused.discard(task)
|
||||||
self.reschedule_running()
|
self.reschedule_running()
|
||||||
|
|
||||||
def handle_errors(self, func: Callable, task: Task | None = None):
|
def handle_errors(self, func: Callable, task: Task | None = None):
|
||||||
|
@ -520,8 +529,6 @@ class FIFOKernel:
|
||||||
task.state = TaskState.CRASHED
|
task.state = TaskState.CRASHED
|
||||||
self.debugger.on_exception_raised(task, err)
|
self.debugger.on_exception_raised(task, err)
|
||||||
self.wait(task)
|
self.wait(task)
|
||||||
if isinstance(err, KeyboardInterrupt):
|
|
||||||
raise
|
|
||||||
|
|
||||||
def sleep(self, seconds: int | float):
|
def sleep(self, seconds: int | float):
|
||||||
"""
|
"""
|
||||||
|
@ -546,16 +553,21 @@ class FIFOKernel:
|
||||||
executing
|
executing
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
if task != self.current_task:
|
||||||
|
task.joiners.add(self.current_task)
|
||||||
if task.done():
|
if task.done():
|
||||||
self.paused.discard(task)
|
|
||||||
self.io_release_task(task)
|
self.io_release_task(task)
|
||||||
self.run_ready.extend(task.joiners)
|
self.run_ready.extend(task.joiners)
|
||||||
|
for joiner in task.joiners:
|
||||||
|
joiner.pending_exception = task.exc
|
||||||
|
|
||||||
def join(self, task: Task):
|
def join(self, task: Task):
|
||||||
"""
|
"""
|
||||||
Tells the event loop that the current task
|
Tells the event loop that the current task
|
||||||
wants to wait on the given one, but without
|
wants to wait on the given one, but without
|
||||||
actually waiting for its completion
|
actually waiting for its completion. This is
|
||||||
|
an internal method and should not be used outside
|
||||||
|
the kernel machinery
|
||||||
"""
|
"""
|
||||||
|
|
||||||
task.joiners.add(self.current_task)
|
task.joiners.add(self.current_task)
|
||||||
|
@ -573,36 +585,6 @@ class FIFOKernel:
|
||||||
self.reschedule_running()
|
self.reschedule_running()
|
||||||
self.debugger.on_task_spawn(task)
|
self.debugger.on_task_spawn(task)
|
||||||
|
|
||||||
def set_context(self, ctx: TaskContext):
|
|
||||||
"""
|
|
||||||
Sets the current task context. This is
|
|
||||||
implemented as simply wrapping the current
|
|
||||||
task inside the context and replacing the
|
|
||||||
Task object with the TaskContext one. This
|
|
||||||
may also wrap another task context into a
|
|
||||||
new one, but the loop doesn't need to care
|
|
||||||
about that: the API is designed exactly for
|
|
||||||
this
|
|
||||||
"""
|
|
||||||
|
|
||||||
ctx.entry_point = self.current_task
|
|
||||||
ctx.tasks.append(ctx.entry_point)
|
|
||||||
self.current_task.context = ctx
|
|
||||||
self.current_task = ctx
|
|
||||||
self.debugger.on_context_creation(ctx)
|
|
||||||
self.reschedule_running()
|
|
||||||
|
|
||||||
def close_context(self, ctx: TaskContext):
|
|
||||||
"""
|
|
||||||
Closes the given context
|
|
||||||
"""
|
|
||||||
|
|
||||||
self.debugger.on_context_exit(ctx)
|
|
||||||
task = ctx.entry_point
|
|
||||||
task.context = None
|
|
||||||
self.current_task = task
|
|
||||||
self.reschedule_running()
|
|
||||||
|
|
||||||
def get_current_task(self):
|
def get_current_task(self):
|
||||||
"""
|
"""
|
||||||
Returns the current task to an asynchronous
|
Returns the current task to an asynchronous
|
||||||
|
|
|
@ -80,9 +80,11 @@ class Task:
|
||||||
# Is this task within a context? This is needed to fix a bug that would occur when
|
# Is this task within a context? This is needed to fix a bug that would occur when
|
||||||
# the event loop tries to raise the exception caused by first task that kicked the
|
# the event loop tries to raise the exception caused by first task that kicked the
|
||||||
# loop even if that context already ignored said error
|
# loop even if that context already ignored said error
|
||||||
context: "TaskContext" = None
|
context: "TaskContext" = field(default=None, repr=False)
|
||||||
# We propagate exception only at the first call to wait()
|
# We propagate exception only at the first call to wait()
|
||||||
propagate: bool = True
|
propagate: bool = True
|
||||||
|
# Do we have any exceptions pending?
|
||||||
|
pending_exception: Exception | None = None
|
||||||
|
|
||||||
def run(self, what: Any | None = None):
|
def run(self, what: Any | None = None):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
from aiosched.util import debugging
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ["debugging",
|
||||||
|
]
|
|
@ -1,6 +1,7 @@
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from aiosched.task import Task
|
from aiosched.task import Task
|
||||||
from aiosched.context import TaskContext
|
from aiosched.context import TaskContext
|
||||||
|
from selectors import EVENT_READ, EVENT_WRITE
|
||||||
|
|
||||||
|
|
||||||
class BaseDebugger(ABC):
|
class BaseDebugger(ABC):
|
||||||
|
@ -242,3 +243,74 @@ class BaseDebugger(ABC):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return NotImplemented
|
return NotImplemented
|
||||||
|
|
||||||
|
|
||||||
|
class SimpleDebugger(BaseDebugger):
|
||||||
|
"""
|
||||||
|
A simple debugger for aiosched
|
||||||
|
"""
|
||||||
|
|
||||||
|
def on_start(self):
|
||||||
|
print("## Started running")
|
||||||
|
|
||||||
|
def on_exit(self):
|
||||||
|
print("## Finished running")
|
||||||
|
|
||||||
|
def on_task_schedule(self, task, delay: int):
|
||||||
|
print(
|
||||||
|
f">> A task named '{task.name}' was scheduled to run in {delay:.2f} seconds"
|
||||||
|
)
|
||||||
|
|
||||||
|
def on_task_spawn(self, task):
|
||||||
|
print(f">> A task named '{task.name}' was spawned")
|
||||||
|
|
||||||
|
def on_task_exit(self, task):
|
||||||
|
print(f"<< Task '{task.name}' exited")
|
||||||
|
|
||||||
|
def before_task_step(self, task):
|
||||||
|
print(f"-> About to run a step for '{task.name}'")
|
||||||
|
|
||||||
|
def after_task_step(self, task):
|
||||||
|
print(f"<- Ran a step for '{task.name}'")
|
||||||
|
|
||||||
|
def before_sleep(self, task, seconds):
|
||||||
|
print(f"# About to put '{task.name}' to sleep for {seconds:.2f} seconds")
|
||||||
|
|
||||||
|
def after_sleep(self, task, seconds):
|
||||||
|
print(f"# Task '{task.name}' slept for {seconds:.2f} seconds")
|
||||||
|
|
||||||
|
def before_io(self, timeout):
|
||||||
|
if timeout is None:
|
||||||
|
timeout = float("inf")
|
||||||
|
print(f"!! About to check for I/O for up to {timeout:.2f} seconds")
|
||||||
|
|
||||||
|
def after_io(self, timeout):
|
||||||
|
print(f"!! Done I/O check (waited for {timeout:.2f} seconds)")
|
||||||
|
|
||||||
|
def before_cancel(self, task):
|
||||||
|
print(f"// About to cancel '{task.name}'")
|
||||||
|
|
||||||
|
def after_cancel(self, task):
|
||||||
|
print(f"// Cancelled '{task.name}'")
|
||||||
|
|
||||||
|
def on_exception_raised(self, task, exc):
|
||||||
|
print(f"== '{task.name}' raised {repr(exc)}")
|
||||||
|
|
||||||
|
def on_context_creation(self, ctx):
|
||||||
|
print(f"=> A new context was created by {ctx.entry_point.name!r}")
|
||||||
|
|
||||||
|
def on_context_exit(self, ctx):
|
||||||
|
print(f"=> A context was closed by {ctx.entry_point.name}")
|
||||||
|
|
||||||
|
def on_io_schedule(self, stream, event: int):
|
||||||
|
evt = ""
|
||||||
|
if event == EVENT_READ:
|
||||||
|
evt = "reading"
|
||||||
|
elif event == EVENT_WRITE:
|
||||||
|
evt = "writing"
|
||||||
|
elif event == EVENT_WRITE | EVENT_READ:
|
||||||
|
evt = "reading or writing"
|
||||||
|
print(f"|| Stream {stream!r} was scheduled for {evt}")
|
||||||
|
|
||||||
|
def on_io_unschedule(self, stream):
|
||||||
|
print(f"|| Stream {stream!r} was unscheduled")
|
|
@ -1,6 +1,5 @@
|
||||||
import random
|
import random
|
||||||
import aiosched
|
import aiosched
|
||||||
from debugger import Debugger
|
|
||||||
|
|
||||||
|
|
||||||
async def child(name: str, n: int):
|
async def child(name: str, n: int):
|
||||||
|
|
|
@ -2,8 +2,6 @@ import aiosched
|
||||||
import logging
|
import logging
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
|
||||||
from debugger import Debugger
|
|
||||||
# An asynchronous chatroom
|
# An asynchronous chatroom
|
||||||
|
|
||||||
clients: dict[aiosched.socket.AsyncSocket, list[str, str]] = {}
|
clients: dict[aiosched.socket.AsyncSocket, list[str, str]] = {}
|
||||||
|
@ -46,10 +44,14 @@ async def handler(sock: aiosched.socket.AsyncSocket):
|
||||||
name = ""
|
name = ""
|
||||||
async with sock: # Closes the socket automatically
|
async with sock: # Closes the socket automatically
|
||||||
await sock.send_all(b"Welcome to the chatroom pal, may you tell me your name?\n> ")
|
await sock.send_all(b"Welcome to the chatroom pal, may you tell me your name?\n> ")
|
||||||
while True:
|
cond = True
|
||||||
|
while cond:
|
||||||
while not name.endswith("\n"):
|
while not name.endswith("\n"):
|
||||||
name = (await sock.receive(64)).decode()
|
name = (await sock.receive(64)).decode()
|
||||||
name = name[:-1]
|
if name == "":
|
||||||
|
cond = False
|
||||||
|
break
|
||||||
|
name = name.rstrip("\n")
|
||||||
if name not in names:
|
if name not in names:
|
||||||
names.add(name)
|
names.add(name)
|
||||||
clients[sock][0] = name
|
clients[sock][0] = name
|
||||||
|
@ -66,14 +68,24 @@ async def handler(sock: aiosched.socket.AsyncSocket):
|
||||||
data = await sock.receive(1024)
|
data = await sock.receive(1024)
|
||||||
if not data:
|
if not data:
|
||||||
break
|
break
|
||||||
logging.info(f"Got: {data!r} from {address}")
|
decoded = data.decode().rstrip("\n")
|
||||||
for i, client_sock in enumerate(clients):
|
if decoded.startswith("/"):
|
||||||
if client_sock != sock and clients[client_sock][0]:
|
logging.info(f"{name} issued server command {decoded}")
|
||||||
logging.info(f"Sending {data!r} to {':'.join(map(str, await client_sock.getpeername()))}")
|
match decoded[1:]:
|
||||||
if not data.endswith(b"\n"):
|
case "bye":
|
||||||
data += b"\n"
|
await sock.send_all(b"Bye!\n")
|
||||||
await client_sock.send_all(f"[{name}] ({address}): {data.decode()}> ".encode())
|
break
|
||||||
logging.info(f"Sent {data!r} to {i} clients")
|
case _:
|
||||||
|
await sock.send_all(b"Unknown command\n")
|
||||||
|
else:
|
||||||
|
logging.info(f"Got: {data!r} from {address}")
|
||||||
|
for i, client_sock in enumerate(clients):
|
||||||
|
if client_sock != sock and clients[client_sock][0]:
|
||||||
|
logging.info(f"Sending {data!r} to {':'.join(map(str, await client_sock.getpeername()))}")
|
||||||
|
if not data.endswith(b"\n"):
|
||||||
|
data += b"\n"
|
||||||
|
await client_sock.send_all(f"[{name}] ({address}): {data.decode()}> ".encode())
|
||||||
|
logging.info(f"Sent {data!r} to {i} clients")
|
||||||
logging.info(f"Connection from {address} closed")
|
logging.info(f"Connection from {address} closed")
|
||||||
clients.pop(sock)
|
clients.pop(sock)
|
||||||
names.discard(name)
|
names.discard(name)
|
||||||
|
@ -88,7 +100,7 @@ if __name__ == "__main__":
|
||||||
datefmt="%d/%m/%Y %p",
|
datefmt="%d/%m/%Y %p",
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
aiosched.run(serve, ("0.0.0.0", port), debugger=())
|
aiosched.run(serve, ("0.0.0.0", port), debugger=None)
|
||||||
except (Exception, KeyboardInterrupt) as error: # Exceptions propagate!
|
except (Exception, KeyboardInterrupt) as error: # Exceptions propagate!
|
||||||
if isinstance(error, KeyboardInterrupt):
|
if isinstance(error, KeyboardInterrupt):
|
||||||
logging.info("Ctrl+C detected, exiting")
|
logging.info("Ctrl+C detected, exiting")
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import aiosched
|
import aiosched
|
||||||
from raw_catch import child_raises
|
from raw_catch import child_raises
|
||||||
from debugger import Debugger
|
|
||||||
|
|
||||||
|
|
||||||
async def main(children: list[tuple[str, int]]):
|
async def main(children: list[tuple[str, int]]):
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import aiosched
|
import aiosched
|
||||||
from raw_catch import child
|
from raw_catch import child
|
||||||
from debugger import Debugger
|
|
||||||
|
|
||||||
|
|
||||||
async def main(children: list[tuple[str, int]]):
|
async def main(children: list[tuple[str, int]]):
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import aiosched
|
import aiosched
|
||||||
from raw_wait import child
|
from raw_wait import child
|
||||||
from debugger import Debugger
|
|
||||||
|
|
||||||
|
|
||||||
async def main(children: list[tuple[str, int]]):
|
async def main(children: list[tuple[str, int]]):
|
||||||
|
|
|
@ -1,73 +0,0 @@
|
||||||
from aiosched.util.debugging import BaseDebugger
|
|
||||||
from selectors import EVENT_READ, EVENT_WRITE
|
|
||||||
|
|
||||||
|
|
||||||
class Debugger(BaseDebugger):
|
|
||||||
"""
|
|
||||||
A simple debugger for aiosched
|
|
||||||
"""
|
|
||||||
|
|
||||||
def on_start(self):
|
|
||||||
print("## Started running")
|
|
||||||
|
|
||||||
def on_exit(self):
|
|
||||||
print("## Finished running")
|
|
||||||
|
|
||||||
def on_task_schedule(self, task, delay: int):
|
|
||||||
print(
|
|
||||||
f">> A task named '{task.name}' was scheduled to run in {delay:.2f} seconds"
|
|
||||||
)
|
|
||||||
|
|
||||||
def on_task_spawn(self, task):
|
|
||||||
print(f">> A task named '{task.name}' was spawned")
|
|
||||||
|
|
||||||
def on_task_exit(self, task):
|
|
||||||
print(f"<< Task '{task.name}' exited")
|
|
||||||
|
|
||||||
def before_task_step(self, task):
|
|
||||||
print(f"-> About to run a step for '{task.name}'")
|
|
||||||
|
|
||||||
def after_task_step(self, task):
|
|
||||||
print(f"<- Ran a step for '{task.name}'")
|
|
||||||
|
|
||||||
def before_sleep(self, task, seconds):
|
|
||||||
print(f"# About to put '{task.name}' to sleep for {seconds:.2f} seconds")
|
|
||||||
|
|
||||||
def after_sleep(self, task, seconds):
|
|
||||||
print(f"# Task '{task.name}' slept for {seconds:.2f} seconds")
|
|
||||||
|
|
||||||
def before_io(self, timeout):
|
|
||||||
if timeout is None:
|
|
||||||
timeout = float("inf")
|
|
||||||
print(f"!! About to check for I/O for up to {timeout:.2f} seconds")
|
|
||||||
|
|
||||||
def after_io(self, timeout):
|
|
||||||
print(f"!! Done I/O check (waited for {timeout:.2f} seconds)")
|
|
||||||
|
|
||||||
def before_cancel(self, task):
|
|
||||||
print(f"// About to cancel '{task.name}'")
|
|
||||||
|
|
||||||
def after_cancel(self, task):
|
|
||||||
print(f"// Cancelled '{task.name}'")
|
|
||||||
|
|
||||||
def on_exception_raised(self, task, exc):
|
|
||||||
print(f"== '{task.name}' raised {repr(exc)}")
|
|
||||||
|
|
||||||
def on_context_creation(self, ctx):
|
|
||||||
print(f"=> A new context was created by {ctx.entry_point.name!r}")
|
|
||||||
|
|
||||||
def on_context_exit(self, ctx):
|
|
||||||
print(f"=> A context was closed by {ctx.entry_point.name}")
|
|
||||||
|
|
||||||
def on_io_schedule(self, stream, event: int):
|
|
||||||
evt = ""
|
|
||||||
if event == EVENT_READ:
|
|
||||||
evt = "reading"
|
|
||||||
elif event == EVENT_WRITE:
|
|
||||||
evt = "writing"
|
|
||||||
elif event == EVENT_WRITE | EVENT_READ:
|
|
||||||
evt = "reading or writing"
|
|
||||||
print(f"|| Stream {stream!r} was scheduled for {evt}")
|
|
||||||
|
|
||||||
def on_io_unschedule(self, stream):
|
|
||||||
print(f"|| Stream {stream!r} was unscheduled")
|
|
|
@ -1,7 +1,7 @@
|
||||||
import sys
|
import sys
|
||||||
import logging
|
import logging
|
||||||
import aiosched
|
import aiosched
|
||||||
from debugger import Debugger
|
|
||||||
|
|
||||||
# A test to check for asynchronous I/O
|
# A test to check for asynchronous I/O
|
||||||
|
|
||||||
|
@ -51,6 +51,9 @@ async def handler(sock: aiosched.socket.AsyncSocket, client_address: tuple):
|
||||||
elif data == b"exit\n":
|
elif data == b"exit\n":
|
||||||
await sock.send_all(b"I'm dead dude\n")
|
await sock.send_all(b"I'm dead dude\n")
|
||||||
raise TypeError("Oh, no, I'm gonna die!")
|
raise TypeError("Oh, no, I'm gonna die!")
|
||||||
|
elif data == b"fatal\n":
|
||||||
|
await sock.send_all(b"What a dick\n")
|
||||||
|
raise KeyboardInterrupt("He told me to do it!")
|
||||||
logging.info(f"Got: {data!r} from {address}")
|
logging.info(f"Got: {data!r} from {address}")
|
||||||
await sock.send_all(b"Got: " + data)
|
await sock.send_all(b"Got: " + data)
|
||||||
logging.info(f"Echoed back {data!r} to {address}")
|
logging.info(f"Echoed back {data!r} to {address}")
|
||||||
|
@ -65,7 +68,7 @@ if __name__ == "__main__":
|
||||||
datefmt="%d/%m/%Y %H:%M:%S %p",
|
datefmt="%d/%m/%Y %H:%M:%S %p",
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
aiosched.run(serve, ("localhost", port), debugger=())
|
aiosched.run(serve, ("localhost", port), debugger=None)
|
||||||
except (Exception, KeyboardInterrupt) as error: # Exceptions propagate!
|
except (Exception, KeyboardInterrupt) as error: # Exceptions propagate!
|
||||||
if isinstance(error, KeyboardInterrupt):
|
if isinstance(error, KeyboardInterrupt):
|
||||||
logging.info("Ctrl+C detected, exiting")
|
logging.info("Ctrl+C detected, exiting")
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
from debugger import Debugger
|
|
||||||
import aiosched
|
import aiosched
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import aiosched
|
import aiosched
|
||||||
from debugger import Debugger
|
|
||||||
|
|
||||||
|
|
||||||
async def sender(c: aiosched.MemoryChannel, n: int):
|
async def sender(c: aiosched.MemoryChannel, n: int):
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import aiosched
|
import aiosched
|
||||||
from raw_catch import child_raises
|
from raw_catch import child_raises
|
||||||
from raw_wait import child as successful
|
from raw_wait import child as successful
|
||||||
from debugger import Debugger
|
|
||||||
|
|
||||||
|
|
||||||
async def main(
|
async def main(
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import aiosched
|
import aiosched
|
||||||
from raw_catch import child_raises
|
from raw_catch import child_raises
|
||||||
from debugger import Debugger
|
|
||||||
|
|
||||||
|
|
||||||
# TODO: This crashes 1 second later than it should be
|
# TODO: This crashes 1 second later than it should be
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import aiosched
|
import aiosched
|
||||||
from raw_wait import child
|
from raw_wait import child
|
||||||
from debugger import Debugger
|
|
||||||
|
|
||||||
|
|
||||||
async def main(
|
async def main(
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import aiosched
|
import aiosched
|
||||||
from debugger import Debugger
|
|
||||||
|
|
||||||
|
|
||||||
async def producer(c: aiosched.NetworkChannel, n: int):
|
async def producer(c: aiosched.NetworkChannel, n: int):
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import aiosched
|
import aiosched
|
||||||
from debugger import Debugger
|
|
||||||
|
|
||||||
|
|
||||||
async def producer(q: aiosched.Queue, n: int):
|
async def producer(q: aiosched.Queue, n: int):
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import aiosched
|
import aiosched
|
||||||
from debugger import Debugger
|
|
||||||
|
|
||||||
|
|
||||||
async def child_raises(name: str, n: int):
|
async def child_raises(name: str, n: int):
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import aiosched
|
import aiosched
|
||||||
from debugger import Debugger
|
|
||||||
|
|
||||||
|
|
||||||
async def child(name: str, n: int):
|
async def child(name: str, n: int):
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
from debugger import Debugger
|
|
||||||
import aiosched
|
import aiosched
|
||||||
import socket as sock
|
import socket as sock
|
||||||
import ssl
|
import ssl
|
||||||
|
|
Reference in New Issue