Fixes/additions to I/O mechanism, bugs still exist in network_channel test
This commit is contained in:
parent
7b134f9a1d
commit
acc436d518
|
@ -23,6 +23,7 @@ from aiosched.internals.syscalls import (
|
||||||
set_context,
|
set_context,
|
||||||
close_context,
|
close_context,
|
||||||
join,
|
join,
|
||||||
|
current_task,
|
||||||
)
|
)
|
||||||
from typing import Any, Coroutine, Callable
|
from typing import Any, Coroutine, Callable
|
||||||
|
|
||||||
|
@ -34,13 +35,13 @@ class TaskContext(Task):
|
||||||
an exception occurs. A TaskContext object behaves like
|
an exception occurs. A TaskContext object behaves like
|
||||||
a regular task and the event loop treats it like a single
|
a regular task and the event loop treats it like a single
|
||||||
unit rather than a collection of tasks (in fact, the event
|
unit rather than a collection of tasks (in fact, the event
|
||||||
loop doesn't even know whether the current task is a task
|
loop doesn't even know, nor care about, whether the current
|
||||||
context or not, which is by design). TaskContexts can be
|
task is a task context or not, which is by design). Contexts
|
||||||
nested and will cancel inner ones if an exception is raised
|
can be nested and will cancel inner ones if an exception is
|
||||||
inside them
|
raised inside them
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, silent: bool = False, gather: bool = True) -> None:
|
def __init__(self, silent: bool = False, gather: bool = True, timeout: int | float = 0.0) -> None:
|
||||||
"""
|
"""
|
||||||
Object constructor
|
Object constructor
|
||||||
"""
|
"""
|
||||||
|
@ -49,13 +50,16 @@ class TaskContext(Task):
|
||||||
self.tasks: list[Task] = []
|
self.tasks: list[Task] = []
|
||||||
# Whether we have been cancelled or not
|
# Whether we have been cancelled or not
|
||||||
self.cancelled: bool = False
|
self.cancelled: bool = False
|
||||||
# The context's entry point (needed to forward run() calls and the like)
|
# The context's entry point (needed to disguise ourselves as a task ;))
|
||||||
self.entry_point: Task | TaskContext | None = None
|
self.entry_point: Task | TaskContext | None = None
|
||||||
# Do we ignore exceptions?
|
# Do we ignore exceptions?
|
||||||
self.silent: bool = silent
|
self.silent: bool = silent
|
||||||
# Do we gather multiple exceptions from
|
# Do we gather multiple exceptions from
|
||||||
# children tasks?
|
# children tasks?
|
||||||
self.gather: bool = gather
|
self.gather: bool = gather # TODO: Implement
|
||||||
|
# For how long do we allow tasks inside us
|
||||||
|
# to run?
|
||||||
|
self.timeout: int | float = timeout # TODO: Implement
|
||||||
|
|
||||||
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,6 +82,17 @@ class TaskContext(Task):
|
||||||
await set_context(self)
|
await set_context(self)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
"""
|
||||||
|
Implements self == other
|
||||||
|
"""
|
||||||
|
|
||||||
|
if isinstance(other, TaskContext):
|
||||||
|
return super().__eq__(self, other)
|
||||||
|
elif isinstance(other, Task):
|
||||||
|
return other == self.entry_point
|
||||||
|
return False
|
||||||
|
|
||||||
async def __aexit__(self, exc_type: Exception, exc: Exception, tb):
|
async def __aexit__(self, exc_type: Exception, exc: Exception, tb):
|
||||||
"""
|
"""
|
||||||
Implements the asynchronous context manager interface, waiting
|
Implements the asynchronous context manager interface, waiting
|
||||||
|
@ -91,6 +106,11 @@ class TaskContext(Task):
|
||||||
# end of the block and wait for all
|
# end of the block and wait for all
|
||||||
# children to exit
|
# children to exit
|
||||||
if task is self.entry_point:
|
if task is self.entry_point:
|
||||||
|
# We don't wait on the entry
|
||||||
|
# point because that's us!
|
||||||
|
# Besides, even if we tried,
|
||||||
|
# wait() would raise an error
|
||||||
|
# to avoid a deadlock
|
||||||
continue
|
continue
|
||||||
await wait(task)
|
await wait(task)
|
||||||
except BaseException as exc:
|
except BaseException as exc:
|
||||||
|
|
|
@ -103,10 +103,9 @@ async def checkpoint():
|
||||||
|
|
||||||
async def suspend():
|
async def suspend():
|
||||||
"""
|
"""
|
||||||
Suspends the current task. The task is not
|
Suspends the calling task indefinitely.
|
||||||
rescheduled until some other event (for example
|
The task can be unsuspended by a timer,
|
||||||
a timer, an event or an I/O operation) reschedules
|
an event or an incoming I/O operation
|
||||||
it
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
await syscall("suspend")
|
await syscall("suspend")
|
||||||
|
@ -125,7 +124,9 @@ async def join(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
|
||||||
waiting for its completion
|
waiting for its completion. This is a low
|
||||||
|
level trap and should not be used on its
|
||||||
|
own
|
||||||
"""
|
"""
|
||||||
|
|
||||||
await syscall("join", task)
|
await syscall("join", task)
|
||||||
|
@ -140,7 +141,8 @@ async def wait(task: Task) -> Any | None:
|
||||||
Returns immediately if the task has
|
Returns immediately if the task has
|
||||||
completed already, but exceptions are
|
completed already, but exceptions are
|
||||||
propagated only once. Returns the task's
|
propagated only once. Returns the task's
|
||||||
return value, if it has one
|
return value, if it has one (returned once
|
||||||
|
for each call).
|
||||||
|
|
||||||
:param task: The task to wait for
|
:param task: The task to wait for
|
||||||
:type task: :class: Task
|
:type task: :class: Task
|
||||||
|
@ -148,7 +150,10 @@ async def wait(task: Task) -> Any | None:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
current = await current_task()
|
current = await current_task()
|
||||||
if task is current:
|
if task == current:
|
||||||
|
# We don't do an "x is y" check because
|
||||||
|
# tasks and task contexts can compare equal
|
||||||
|
# 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:
|
if current not in task.joiners:
|
||||||
# Luckily we use a set, so this has O(1)
|
# Luckily we use a set, so this has O(1)
|
||||||
|
@ -156,6 +161,8 @@ async def wait(task: Task) -> Any | None:
|
||||||
await join(task) # Waiting implies joining!
|
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:
|
||||||
|
# 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
|
task.propagate = False
|
||||||
raise task.exc
|
raise task.exc
|
||||||
return task.result
|
return task.result
|
||||||
|
|
|
@ -17,11 +17,10 @@ limitations under the License.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import socket
|
import socket
|
||||||
import ssl
|
|
||||||
import warnings
|
import warnings
|
||||||
import os
|
import os
|
||||||
import aiosched
|
import aiosched
|
||||||
from aiosched.errors import ResourceClosed
|
from aiosched.errors import ResourceClosed, ResourceBroken
|
||||||
from aiosched.internals.syscalls import (
|
from aiosched.internals.syscalls import (
|
||||||
wait_writable,
|
wait_writable,
|
||||||
wait_readable,
|
wait_readable,
|
||||||
|
@ -99,8 +98,8 @@ class AsyncStream:
|
||||||
await io_release(self.stream)
|
await io_release(self.stream)
|
||||||
self.stream.close()
|
self.stream.close()
|
||||||
self.stream = None
|
self.stream = None
|
||||||
|
await aiosched.checkpoint()
|
||||||
|
|
||||||
@property
|
|
||||||
async def fileno(self):
|
async def fileno(self):
|
||||||
"""
|
"""
|
||||||
Wrapper socket method
|
Wrapper socket method
|
||||||
|
@ -132,7 +131,7 @@ class AsyncStream:
|
||||||
this directly: stuff will break
|
this directly: stuff will break
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if self._fd != -1:
|
if self._fd != -1 and self.stream.fileno() != -1:
|
||||||
try:
|
try:
|
||||||
os.set_blocking(self._fd, False)
|
os.set_blocking(self._fd, False)
|
||||||
os.close(self._fd)
|
os.close(self._fd)
|
||||||
|
@ -153,11 +152,18 @@ class AsyncSocket(AsyncStream):
|
||||||
close_on_context_exit: bool = True,
|
close_on_context_exit: bool = True,
|
||||||
do_handshake_on_connect: bool = True,
|
do_handshake_on_connect: bool = True,
|
||||||
):
|
):
|
||||||
super().__init__(
|
# Do we perform the TCP handshake automatically
|
||||||
sock.fileno(), open_fd=False, close_on_context_exit=close_on_context_exit
|
# upon connection? This is mostly needed for SSL
|
||||||
)
|
# sockets
|
||||||
self.do_handshake_on_connect = do_handshake_on_connect
|
self.do_handshake_on_connect = do_handshake_on_connect
|
||||||
self.stream = socket.fromfd(self._fd, sock.family, sock.type, sock.proto)
|
# Do we close ourselves upon the end of a context manager?
|
||||||
|
self.close_on_context_exit = close_on_context_exit
|
||||||
|
# The socket.fromfd function copies the file descriptor
|
||||||
|
# instead of using the same one, so we'd be trying to close
|
||||||
|
# a different resource if we used sock.fileno() instead
|
||||||
|
# of self.stream.fileno() as our file descriptor
|
||||||
|
self.stream = socket.fromfd(sock.fileno(), sock.family, sock.type, sock.proto)
|
||||||
|
self._fd = self.stream.fileno()
|
||||||
self.stream.setblocking(False)
|
self.stream.setblocking(False)
|
||||||
# A socket that isn't connected doesn't
|
# A socket that isn't connected doesn't
|
||||||
# need to be closed
|
# need to be closed
|
||||||
|
@ -179,6 +185,21 @@ class AsyncSocket(AsyncStream):
|
||||||
except WriteBlock:
|
except WriteBlock:
|
||||||
await wait_writable(self.stream)
|
await wait_writable(self.stream)
|
||||||
|
|
||||||
|
async def receive_exactly(self, size: int, flags: int = 0) -> bytes:
|
||||||
|
"""
|
||||||
|
Receives exactly size bytes from a socket asynchronously.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# https://stackoverflow.com/questions/55825905/how-can-i-reliably-read-exactly-n-bytes-from-a-tcp-socket
|
||||||
|
buf = bytearray(size)
|
||||||
|
pos = 0
|
||||||
|
while pos < size:
|
||||||
|
n = await self.recv_into(memoryview(buf)[pos:], flags=flags)
|
||||||
|
if n == 0:
|
||||||
|
raise ResourceBroken("incomplete read detected")
|
||||||
|
pos += n
|
||||||
|
return bytes(buf)
|
||||||
|
|
||||||
async def connect(self, address):
|
async def connect(self, address):
|
||||||
"""
|
"""
|
||||||
Wrapper socket method
|
Wrapper socket method
|
||||||
|
@ -240,6 +261,8 @@ class AsyncSocket(AsyncStream):
|
||||||
Wrapper socket method
|
Wrapper socket method
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
if self._fd == -1:
|
||||||
|
raise ResourceClosed("I/O operation on closed socket")
|
||||||
if self.stream:
|
if self.stream:
|
||||||
self.stream.shutdown(how)
|
self.stream.shutdown(how)
|
||||||
await aiosched.checkpoint()
|
await aiosched.checkpoint()
|
||||||
|
@ -320,6 +343,19 @@ class AsyncSocket(AsyncStream):
|
||||||
except WriteBlock:
|
except WriteBlock:
|
||||||
await wait_writable(self.stream)
|
await wait_writable(self.stream)
|
||||||
|
|
||||||
|
async def recv_into(self, buffer, nbytes=0, flags=0):
|
||||||
|
"""
|
||||||
|
Wrapper socket method
|
||||||
|
"""
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
return self.stream.recv_into(buffer, nbytes, flags)
|
||||||
|
except ReadBlock:
|
||||||
|
await wait_readable(self.stream)
|
||||||
|
except WriteBlock:
|
||||||
|
await wait_writable(self.stream)
|
||||||
|
|
||||||
async def recvfrom_into(self, buffer, bytes=0, flags=0):
|
async def recvfrom_into(self, buffer, bytes=0, flags=0):
|
||||||
"""
|
"""
|
||||||
Wrapper socket method
|
Wrapper socket method
|
||||||
|
|
|
@ -113,7 +113,25 @@ class FIFOKernel:
|
||||||
to do
|
to do
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return not any([self.paused, self.run_ready, self.selector.get_map()])
|
if self.current_task and not self.current_task.done():
|
||||||
|
# Current task isn't done yet!
|
||||||
|
return False
|
||||||
|
if any([self.paused, self.run_ready]):
|
||||||
|
# There's tasks sleeping and/or on the
|
||||||
|
# ready queue!
|
||||||
|
return False
|
||||||
|
for key in self.selector.get_map().values():
|
||||||
|
# We don't just do any([self.paused, self.run_ready, self.selector.get_map()])
|
||||||
|
# because we don't want to just know if there's any resources we're waiting on,
|
||||||
|
# but if there's at least one non-terminated task that owns a resource we're
|
||||||
|
# waiting on. This avoids issues such as the event loop never exiting if the
|
||||||
|
# user forgets to close a socket, for example
|
||||||
|
key.data: Task
|
||||||
|
if key.data.done():
|
||||||
|
continue
|
||||||
|
elif self.get_task_io(key.data):
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
def close(self, force: bool = False):
|
def close(self, force: bool = False):
|
||||||
"""
|
"""
|
||||||
|
@ -159,16 +177,30 @@ class FIFOKernel:
|
||||||
timeout = 0.0
|
timeout = 0.0
|
||||||
if self.run_ready:
|
if self.run_ready:
|
||||||
# If there is work to do immediately (tasks to run) we
|
# If there is work to do immediately (tasks to run) we
|
||||||
# can't wait
|
# can't wait.
|
||||||
|
# TODO: This could cause I/O starvation in highly concurrent
|
||||||
|
# environments: maybe a more convoluted scheduling strategy
|
||||||
|
# where I/O timeouts can only be skipped n times before a
|
||||||
|
# mandatory x-second timeout occurs is needed? It should of
|
||||||
|
# course take deadlines into account so that timeouts are
|
||||||
|
# always delivered in a timely manner and tasks awake from
|
||||||
|
# sleeping at the right moment
|
||||||
timeout = 0.0
|
timeout = 0.0
|
||||||
elif self.paused:
|
elif self.paused:
|
||||||
# If there are asleep tasks or deadlines, wait until the closest date
|
# If there are asleep tasks or deadlines, wait until the closest date
|
||||||
timeout = self.paused.get_closest_deadline()
|
timeout = self.paused.get_closest_deadline() - self.clock()
|
||||||
self.debugger.before_io(timeout)
|
self.debugger.before_io(timeout)
|
||||||
io_ready = self.selector.select(timeout)
|
|
||||||
# Get sockets that are ready and schedule their tasks
|
# Get sockets that are ready and schedule their tasks
|
||||||
for key, _ in io_ready:
|
for key, _ in self.selector.select(timeout):
|
||||||
self.run_ready.append(key.data) # Resource ready? Schedule its task
|
key.data: Task
|
||||||
|
if key.data.state == TaskState.IO:
|
||||||
|
# We don't reschedule a task that wasn't
|
||||||
|
# blocking on I/O before: this way if a
|
||||||
|
# task waits on a socket and then goes to
|
||||||
|
# sleep, it won't be woken up early if the
|
||||||
|
# resource becomes available before its
|
||||||
|
# deadline expires
|
||||||
|
self.run_ready.append(key.data) # Resource ready? Schedule its task
|
||||||
self.debugger.after_io(self.clock() - before_time)
|
self.debugger.after_io(self.clock() - before_time)
|
||||||
|
|
||||||
def awake_tasks(self):
|
def awake_tasks(self):
|
||||||
|
@ -220,9 +252,9 @@ class FIFOKernel:
|
||||||
our primitives or async methods.
|
our primitives or async methods.
|
||||||
|
|
||||||
Note that this method does NOT catch any
|
Note that this method does NOT catch any
|
||||||
exception arising from tasks, nor does it
|
errors arising from tasks, nor does it take
|
||||||
take StopIteration or CancelledError into
|
StopIteration or Cancelled exceptions into
|
||||||
account: that's the job for run()!
|
account
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Sets the currently running task
|
# Sets the currently running task
|
||||||
|
@ -253,12 +285,12 @@ class FIFOKernel:
|
||||||
)
|
)
|
||||||
if not hasattr(self, method) or not callable(getattr(self, method)):
|
if not hasattr(self, method) or not callable(getattr(self, method)):
|
||||||
# This if block is meant to be triggered by other async
|
# This if block is meant to be triggered by other async
|
||||||
# libraries, which most likely have different trap names and behaviors
|
# libraries, which most likely have different method names and behaviors
|
||||||
# compared to us. If you get this exception, and you're 100% sure you're
|
# compared to us. If you get this exception, and you're 100% sure you're
|
||||||
# not mixing async primitives from other libraries, then it's a bug!
|
# not mixing async primitives from other libraries, then it's a bug!
|
||||||
self.current_task.throw(
|
self.current_task.throw(
|
||||||
InternalError(
|
InternalError(
|
||||||
"Uh oh! Something very bad just happened, did you try to mix primitives from other async libraries?"
|
"Uh oh! Something bad just happened: did you try to mix primitives from other async libraries?"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
# Sneaky method call, thanks to David Beazley for this ;)
|
# Sneaky method call, thanks to David Beazley for this ;)
|
||||||
|
@ -321,7 +353,8 @@ class FIFOKernel:
|
||||||
and self.entry_point.propagate
|
and self.entry_point.propagate
|
||||||
):
|
):
|
||||||
# Contexts already manage exceptions for us,
|
# Contexts already manage exceptions for us,
|
||||||
# no need to raise it manually
|
# no need to raise it manually. If a context
|
||||||
|
# is not used, *then* we can raise the error
|
||||||
raise self.entry_point.exc
|
raise self.entry_point.exc
|
||||||
return self.entry_point.result
|
return self.entry_point.result
|
||||||
|
|
||||||
|
@ -334,6 +367,7 @@ class FIFOKernel:
|
||||||
|
|
||||||
if self.selector.get_map() and resource in self.selector.get_map():
|
if self.selector.get_map() and resource in self.selector.get_map():
|
||||||
self.selector.unregister(resource)
|
self.selector.unregister(resource)
|
||||||
|
self.debugger.on_io_unschedule(resource)
|
||||||
|
|
||||||
def io_release_task(self, task: Task):
|
def io_release_task(self, task: Task):
|
||||||
"""
|
"""
|
||||||
|
@ -348,6 +382,14 @@ class FIFOKernel:
|
||||||
self.selector.unregister(key.fileobj)
|
self.selector.unregister(key.fileobj)
|
||||||
task.last_io = ()
|
task.last_io = ()
|
||||||
|
|
||||||
|
def get_task_io(self, task: Task) -> list:
|
||||||
|
"""
|
||||||
|
Returns the streams currently in use by
|
||||||
|
the given task
|
||||||
|
"""
|
||||||
|
|
||||||
|
return list(map(lambda k: k.fileobj, filter(lambda k: k.data == task, self.selector.get_map().values())))
|
||||||
|
|
||||||
def notify_closing(self, stream, broken: bool = False):
|
def notify_closing(self, stream, broken: bool = False):
|
||||||
"""
|
"""
|
||||||
Notifies paused tasks that a stream
|
Notifies paused tasks that a stream
|
||||||
|
@ -452,6 +494,7 @@ class FIFOKernel:
|
||||||
self.paused.discard(task)
|
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)
|
||||||
|
self.reschedule_running()
|
||||||
|
|
||||||
def join(self, task: Task):
|
def join(self, task: Task):
|
||||||
"""
|
"""
|
||||||
|
@ -491,6 +534,7 @@ class FIFOKernel:
|
||||||
ctx.tasks.append(ctx.entry_point)
|
ctx.tasks.append(ctx.entry_point)
|
||||||
self.current_task.context = ctx
|
self.current_task.context = ctx
|
||||||
self.current_task = ctx
|
self.current_task = ctx
|
||||||
|
self.debugger.on_context_creation(ctx)
|
||||||
self.reschedule_running()
|
self.reschedule_running()
|
||||||
|
|
||||||
def close_context(self, ctx: TaskContext):
|
def close_context(self, ctx: TaskContext):
|
||||||
|
@ -498,6 +542,7 @@ class FIFOKernel:
|
||||||
Closes the given context
|
Closes the given context
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
self.debugger.on_context_exit(ctx)
|
||||||
task = ctx.entry_point
|
task = ctx.entry_point
|
||||||
task.context = None
|
task.context = None
|
||||||
self.current_task = task
|
self.current_task = task
|
||||||
|
@ -547,12 +592,14 @@ class FIFOKernel:
|
||||||
# If the event to listen for has changed we just modify it
|
# If the event to listen for has changed we just modify it
|
||||||
self.selector.modify(resource, evt_type, self.current_task)
|
self.selector.modify(resource, evt_type, self.current_task)
|
||||||
self.current_task.last_io = (evt_type, resource)
|
self.current_task.last_io = (evt_type, resource)
|
||||||
|
self.debugger.on_io_schedule(resource, evt_type)
|
||||||
elif not self.current_task.last_io or self.current_task.last_io[1] != resource:
|
elif not self.current_task.last_io or self.current_task.last_io[1] != resource:
|
||||||
# The task has either registered a new resource or is doing
|
# The task has either registered a new resource or is doing
|
||||||
# I/O for the first time
|
# I/O for the first time
|
||||||
self.current_task.last_io = evt_type, resource
|
self.current_task.last_io = evt_type, resource
|
||||||
try:
|
try:
|
||||||
self.selector.register(resource, evt_type, self.current_task)
|
self.selector.register(resource, evt_type, self.current_task)
|
||||||
|
self.debugger.on_io_schedule(resource, evt_type)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
# The stream is already being used
|
# The stream is already being used
|
||||||
key = self.selector.get_key(resource)
|
key = self.selector.get_key(resource)
|
||||||
|
@ -565,6 +612,7 @@ class FIFOKernel:
|
||||||
# off a given stream while another one is
|
# off a given stream while another one is
|
||||||
# writing to it
|
# writing to it
|
||||||
self.selector.modify(resource, evt_type, self.current_task)
|
self.selector.modify(resource, evt_type, self.current_task)
|
||||||
|
self.debugger.on_io_schedule(resource, evt_type)
|
||||||
else:
|
else:
|
||||||
# One task reading and one writing on the same
|
# One task reading and one writing on the same
|
||||||
# resource is fine (think producer-consumer),
|
# resource is fine (think producer-consumer),
|
||||||
|
|
|
@ -18,11 +18,12 @@ limitations under the License.
|
||||||
from collections import deque
|
from collections import deque
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from aiosched.errors import SchedulerError
|
from aiosched.errors import SchedulerError, ResourceClosed
|
||||||
from aiosched.internals.syscalls import (
|
from aiosched.internals.syscalls import (
|
||||||
suspend,
|
suspend,
|
||||||
schedule,
|
schedule,
|
||||||
current_task,
|
current_task,
|
||||||
|
wait_readable,
|
||||||
)
|
)
|
||||||
from aiosched.task import Task
|
from aiosched.task import Task
|
||||||
from aiosched.socket import wrap_socket
|
from aiosched.socket import wrap_socket
|
||||||
|
@ -72,7 +73,8 @@ class Event:
|
||||||
|
|
||||||
class Queue:
|
class Queue:
|
||||||
"""
|
"""
|
||||||
An asynchronous FIFO queue. Not thread safe
|
An asynchronous FIFO queue. As it is based
|
||||||
|
on events, it is not thread safe
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, maxsize: int | None = None):
|
def __init__(self, maxsize: int | None = None):
|
||||||
|
@ -167,7 +169,12 @@ class Channel(ABC):
|
||||||
"""
|
"""
|
||||||
A generic, two-way, full-duplex communication channel
|
A generic, two-way, full-duplex communication channel
|
||||||
between tasks. This is just an abstract base class and
|
between tasks. This is just an abstract base class and
|
||||||
should not be instantiated directly
|
should not be instantiated directly. Please also note
|
||||||
|
that the read() and write() methods are not implemented
|
||||||
|
here because their signatures vary across subclasses
|
||||||
|
depending on the underlying communication mechanism
|
||||||
|
that is used. Implementors must provide those two methods
|
||||||
|
when subclassing Channel
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, maxsize: int | None = None):
|
def __init__(self, maxsize: int | None = None):
|
||||||
|
@ -178,26 +185,6 @@ class Channel(ABC):
|
||||||
self.maxsize = maxsize
|
self.maxsize = maxsize
|
||||||
self.closed = False
|
self.closed = False
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
async def write(self, data: str):
|
|
||||||
"""
|
|
||||||
Writes data to the channel. Blocks if the internal
|
|
||||||
queue is full until a spot is available. Does nothing
|
|
||||||
if the channel has been closed
|
|
||||||
"""
|
|
||||||
|
|
||||||
return NotImplemented
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
async def read(self):
|
|
||||||
"""
|
|
||||||
Reads data from the channel. Blocks until
|
|
||||||
a message arrives or returns immediately if
|
|
||||||
one is already waiting
|
|
||||||
"""
|
|
||||||
|
|
||||||
return NotImplemented
|
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def close(self):
|
async def close(self):
|
||||||
"""
|
"""
|
||||||
|
@ -220,9 +207,11 @@ class Channel(ABC):
|
||||||
class MemoryChannel(Channel):
|
class MemoryChannel(Channel):
|
||||||
"""
|
"""
|
||||||
A two-way communication channel between tasks.
|
A two-way communication channel between tasks.
|
||||||
Operations on this object do not perform any I/O
|
Operations on this object are based on the Queue
|
||||||
or other system call and are therefore extremely
|
class and do not involve any I/O, making this
|
||||||
efficient. Not thread safe
|
an extremely efficient way to pass data around
|
||||||
|
to tasks. Since this channel is based on queues,
|
||||||
|
it is not thread safe
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, maxsize: int | None = None):
|
def __init__(self, maxsize: int | None = None):
|
||||||
|
@ -288,7 +277,8 @@ class NetworkChannel(Channel):
|
||||||
sockets = socketpair()
|
sockets = socketpair()
|
||||||
self.reader = wrap_socket(sockets[0])
|
self.reader = wrap_socket(sockets[0])
|
||||||
self.writer = wrap_socket(sockets[1])
|
self.writer = wrap_socket(sockets[1])
|
||||||
self._internal_buffer = b""
|
self.reader.needs_closing = True
|
||||||
|
self.writer.needs_closing = True
|
||||||
|
|
||||||
async def write(self, data: bytes):
|
async def write(self, data: bytes):
|
||||||
"""
|
"""
|
||||||
|
@ -298,7 +288,7 @@ class NetworkChannel(Channel):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if self.closed:
|
if self.closed:
|
||||||
return
|
raise ValueError("I/O operation on closed channel")
|
||||||
await self.writer.send_all(data)
|
await self.writer.send_all(data)
|
||||||
|
|
||||||
async def read(self, size: int):
|
async def read(self, size: int):
|
||||||
|
@ -308,12 +298,9 @@ class NetworkChannel(Channel):
|
||||||
next read
|
next read
|
||||||
"""
|
"""
|
||||||
|
|
||||||
data = self._internal_buffer
|
if self.closed:
|
||||||
while len(data) < size:
|
raise ValueError("I/O operation on closed channel")
|
||||||
data += await self.reader.receive(size)
|
return await self.reader.receive_exactly(size)
|
||||||
self._internal_buffer = data[size:]
|
|
||||||
data = data[:size]
|
|
||||||
return data
|
|
||||||
|
|
||||||
async def close(self):
|
async def close(self):
|
||||||
"""
|
"""
|
||||||
|
@ -332,13 +319,15 @@ class NetworkChannel(Channel):
|
||||||
data to be read
|
data to be read
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# TODO: Ugly!
|
|
||||||
if self.closed:
|
if self.closed:
|
||||||
return False
|
return False
|
||||||
try:
|
elif self.reader.fileno == -1:
|
||||||
self._internal_buffer += self.reader.stream.recv(1)
|
|
||||||
except BlockingIOError:
|
|
||||||
return False
|
return False
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
await wait_readable(self.reader.stream)
|
||||||
|
except ResourceClosed:
|
||||||
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -135,5 +135,7 @@ class Task:
|
||||||
Task destructor
|
Task destructor
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
if not self.done():
|
||||||
|
warnings.warn(f"task '{self.name}' was destroyed, but it has not completed yet")
|
||||||
if self.last_io:
|
if self.last_io:
|
||||||
warnings.warn(f"task '{self.name}' was destroyed, but has pending I/O")
|
warnings.warn(f"task '{self.name}' was destroyed, but it has pending I/O")
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
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
|
||||||
|
|
||||||
|
|
||||||
class BaseDebugger(ABC):
|
class BaseDebugger(ABC):
|
||||||
|
@ -192,3 +193,52 @@ class BaseDebugger(ABC):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return NotImplemented
|
return NotImplemented
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def on_context_creation(self, ctx: TaskContext):
|
||||||
|
"""
|
||||||
|
This method is called right after a task
|
||||||
|
context is initialized, i.e. when set_context
|
||||||
|
in the event loop is called
|
||||||
|
|
||||||
|
:param ctx: The context object
|
||||||
|
:type ctx: TaskContext
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
|
||||||
|
return NotImplemented
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def on_context_exit(self, ctx: TaskContext):
|
||||||
|
"""
|
||||||
|
This method is called right before a task
|
||||||
|
context is closed, i.e. when close_context
|
||||||
|
in the event loop is called
|
||||||
|
|
||||||
|
:param ctx: The context object
|
||||||
|
:type ctx: TaskContext
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
|
||||||
|
return NotImplemented
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def on_io_schedule(self, stream, event: int):
|
||||||
|
"""
|
||||||
|
This method is called whenever the
|
||||||
|
perform_io primitive is called within
|
||||||
|
the aiosched event loop with the stream
|
||||||
|
to be registered in the selector and the
|
||||||
|
chosen event mask
|
||||||
|
"""
|
||||||
|
|
||||||
|
return NotImplemented
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def on_io_unschedule(self, stream):
|
||||||
|
"""
|
||||||
|
This method is called whenever a stream
|
||||||
|
is unregistered from the loop's I/O selector
|
||||||
|
"""
|
||||||
|
|
||||||
|
return NotImplemented
|
||||||
|
|
|
@ -75,6 +75,7 @@ async def handler(sock: aiosched.socket.AsyncSocket):
|
||||||
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)
|
||||||
|
logging.info("Handler shutting down")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
from aiosched.util.debugging import BaseDebugger
|
from aiosched.util.debugging import BaseDebugger
|
||||||
|
from selectors import EVENT_READ, EVENT_WRITE
|
||||||
|
|
||||||
|
|
||||||
class Debugger(BaseDebugger):
|
class Debugger(BaseDebugger):
|
||||||
|
@ -51,3 +52,22 @@ class Debugger(BaseDebugger):
|
||||||
|
|
||||||
def on_exception_raised(self, task, exc):
|
def on_exception_raised(self, task, exc):
|
||||||
print(f"== '{task.name}' raised {repr(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")
|
||||||
|
|
|
@ -2,30 +2,35 @@ import aiosched
|
||||||
from debugger import Debugger
|
from debugger import Debugger
|
||||||
|
|
||||||
|
|
||||||
async def sender(c: aiosched.NetworkChannel, n: int):
|
async def producer(c: aiosched.NetworkChannel, n: int):
|
||||||
|
print("[producer] Started")
|
||||||
for i in range(n):
|
for i in range(n):
|
||||||
await c.write(str(i).encode())
|
await c.write(str(i).encode())
|
||||||
print(f"Sent {i}")
|
print(f"[producer] Sent {i}")
|
||||||
await c.close()
|
await aiosched.sleep(0.5) # This makes the receiver wait on us!
|
||||||
print("Sender done")
|
#await c.close()
|
||||||
|
print("[producer] Done")
|
||||||
|
|
||||||
|
|
||||||
async def receiver(c: aiosched.NetworkChannel):
|
async def consumer(c: aiosched.NetworkChannel):
|
||||||
while True:
|
print("[receiver] Started")
|
||||||
if not await c.pending() and c.closed:
|
try:
|
||||||
print("Receiver done")
|
while await c.pending():
|
||||||
break
|
item = await c.read(1)
|
||||||
item = (await c.read(1)).decode()
|
print(f"[consumer] Received {item.decode()}")
|
||||||
print(f"Received {item}")
|
# await aiosched.sleep(2) # If you uncomment this, the except block will be triggered
|
||||||
await aiosched.sleep(1)
|
except aiosched.errors.ResourceClosed:
|
||||||
|
print("[consumer] Stream has been closed early!")
|
||||||
|
print("[consumer] Done")
|
||||||
|
|
||||||
|
|
||||||
async def main(channel: aiosched.NetworkChannel, n: int):
|
async def main(channel: aiosched.NetworkChannel, n: int):
|
||||||
print("Starting sender and receiver")
|
t = aiosched.clock()
|
||||||
|
print("[main] Starting children")
|
||||||
async with aiosched.with_context() as ctx:
|
async with aiosched.with_context() as ctx:
|
||||||
await ctx.spawn(sender, channel, n)
|
await ctx.spawn(consumer, channel)
|
||||||
await ctx.spawn(receiver, channel)
|
await ctx.spawn(producer, channel, n)
|
||||||
print("All done!")
|
print(f"[main] All done in {aiosched.clock() - t:.2f} seconds")
|
||||||
|
|
||||||
|
|
||||||
aiosched.run(
|
aiosched.run(
|
||||||
|
|
Reference in New Issue