261 lines
6.5 KiB
Python
261 lines
6.5 KiB
Python
"""
|
|
aiosched: Yet another Python async scheduler
|
|
|
|
Copyright (C) 2022 nocturn9x
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
https:www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
"""
|
|
|
|
import inspect
|
|
from types import coroutine
|
|
from typing import Any, Callable, Coroutine
|
|
from aiosched.task import Task, TaskState
|
|
from aiosched.errors import SchedulerError
|
|
from selectors import EVENT_READ, EVENT_WRITE
|
|
|
|
|
|
@coroutine
|
|
def syscall(method: str, *args, **kwargs) -> Any | None:
|
|
"""
|
|
Lowest-level primitive to interact with the event loop:
|
|
calls a loop method with the provided arguments. This
|
|
function should not be used directly, but through abstraction
|
|
layers. All positional and keyword arguments are passed to
|
|
the method itself and its return value is provided once the
|
|
loop yields control back to us
|
|
|
|
:param method: The loop method to call
|
|
:type method: str
|
|
:returns: The result of the method call, if any
|
|
"""
|
|
|
|
result = yield method, args, kwargs
|
|
return result
|
|
|
|
|
|
async def schedule(task: Task):
|
|
"""
|
|
Reschedules a task that had been
|
|
previously suspended
|
|
"""
|
|
|
|
await syscall("schedule", task)
|
|
|
|
|
|
async def spawn(func: Callable[..., Coroutine[Any, Any, Any]], *args, **kwargs) -> Task:
|
|
"""
|
|
Spawns a task from a coroutine and returns it. The coroutine
|
|
is put on the running queue and is executed as soon as possible.
|
|
Any positional and keyword arguments are passed along to the coroutine
|
|
|
|
:param func: The coroutine function to instantiate. Note that this should NOT
|
|
be a coroutine object: the arguments to the coroutine should be passed to
|
|
spawn as well
|
|
:return:
|
|
"""
|
|
|
|
if inspect.iscoroutine(func):
|
|
raise TypeError(
|
|
"Looks like you tried to call spawn(your_func(arg1, arg2, ...)), that is wrong!"
|
|
"\nWhat you wanna do, instead, is this: spawn(your_func, arg1, arg2, ...)"
|
|
)
|
|
elif inspect.iscoroutinefunction(func):
|
|
return await syscall("spawn", func, *args, **kwargs)
|
|
else:
|
|
raise TypeError("func must be a coroutine function")
|
|
|
|
|
|
async def sleep(delay: int | float):
|
|
"""
|
|
Puts the calling task to sleep for the
|
|
given amount of time. If the delay is equal
|
|
to zero, this call acts as a checkpoint to
|
|
perform task switching and is useful to release
|
|
pressure from the scheduler in highly concurrent
|
|
environments
|
|
|
|
:param delay: The amount of time (in seconds) that
|
|
the task has to be put to sleep for. Must be
|
|
greater than zero
|
|
:type delay: int | float
|
|
"""
|
|
|
|
await syscall("sleep", delay)
|
|
|
|
|
|
async def checkpoint():
|
|
"""
|
|
Shorthand for sleep(0)
|
|
"""
|
|
|
|
await sleep(0)
|
|
|
|
|
|
async def suspend():
|
|
"""
|
|
Suspends the calling task indefinitely.
|
|
The task can be unsuspended by a timer,
|
|
an event or an incoming I/O operation
|
|
"""
|
|
|
|
await syscall("suspend")
|
|
|
|
|
|
async def current_task() -> Task:
|
|
"""
|
|
Returns the currently running
|
|
task object
|
|
"""
|
|
|
|
return await syscall("get_current_task")
|
|
|
|
|
|
async def wait(task: Task) -> Any | None:
|
|
"""
|
|
Waits for the completion of a
|
|
given task and returns its
|
|
return value. Can be called
|
|
multiple times by multiple tasks.
|
|
Returns immediately if the task has
|
|
completed already, but exceptions are
|
|
propagated only once. Returns the task's
|
|
return value, if it has one (returned once
|
|
for each call).
|
|
|
|
:param task: The task to wait for
|
|
:type task: :class: Task
|
|
:returns: The task's return value, if any
|
|
"""
|
|
|
|
if task is await current_task():
|
|
raise SchedulerError("a task cannot join itself")
|
|
await syscall("wait", task)
|
|
if task.exc and task.state != TaskState.CANCELLED and task.propagate:
|
|
# The task raised an error that wasn't directly caused by a cancellation:
|
|
# raise it, but do so only the first time wait was called
|
|
task.propagate = False
|
|
raise task.exc
|
|
return task.result
|
|
|
|
|
|
async def cancel(task: Task, block: bool = False):
|
|
"""
|
|
Cancels the given task. Note that
|
|
cancellations may not happen immediately
|
|
if the task is blocked in an uninterruptible
|
|
state. If block equals False, the default,
|
|
this function returns immediately, otherwise
|
|
it waits for the task to receive the cancellation
|
|
|
|
:param task: The task to wait for
|
|
:type task: :class: Task
|
|
:param block: Whether to wait for the task to be
|
|
actually cancelled or not, defaults to False
|
|
:type block: bool, optional
|
|
"""
|
|
|
|
if task.done():
|
|
return
|
|
await syscall("cancel", task)
|
|
if block:
|
|
await wait(task)
|
|
if not task.state == TaskState.CANCELLED:
|
|
raise SchedulerError(f"task {task.name!r} ignored cancellation")
|
|
|
|
|
|
async def closing(stream):
|
|
"""
|
|
Notifies the event loop that the
|
|
given stream is about to be closed,
|
|
causing all callers waiting on it
|
|
to error out with an exception instead
|
|
of blocking forever
|
|
"""
|
|
|
|
await syscall("notify_closing", stream)
|
|
|
|
|
|
async def wait_readable(stream):
|
|
"""
|
|
Waits until the given stream is
|
|
readable
|
|
"""
|
|
|
|
await syscall("perform_io", stream, EVENT_READ)
|
|
|
|
|
|
async def wait_writable(stream):
|
|
"""
|
|
Waits until the given stream is
|
|
writable
|
|
"""
|
|
|
|
await syscall("perform_io", stream, EVENT_WRITE)
|
|
|
|
|
|
async def io_release(stream):
|
|
"""
|
|
Signals to the event loop to
|
|
release a given I/O resource
|
|
"""
|
|
|
|
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)
|
|
|
|
|
|
async def set_scope(scope):
|
|
"""
|
|
Sets the current task scope
|
|
"""
|
|
|
|
await syscall("set_scope", scope)
|
|
|
|
|
|
async def close_scope(scope):
|
|
"""
|
|
Closes the current task scope
|
|
"""
|
|
|
|
await syscall("close_scope", scope)
|
|
|
|
|
|
async def get_current_scope():
|
|
"""
|
|
Returns the current task scope
|
|
"""
|
|
|
|
return await syscall("get_current_scope")
|
|
|
|
|
|
async def throw(task, ctx):
|
|
"""
|
|
Throws the given exception in the given task
|
|
"""
|
|
|
|
await syscall("throw", task, ctx)
|