Various bug fixes
This commit is contained in:
parent
3ac6ea58cb
commit
a1b40bd340
|
@ -31,7 +31,7 @@ from aiosched.errors import (
|
||||||
ResourceBroken,
|
ResourceBroken,
|
||||||
)
|
)
|
||||||
from aiosched.context import TaskContext
|
from aiosched.context import TaskContext
|
||||||
from selectors import DefaultSelector, BaseSelector
|
from selectors import DefaultSelector, BaseSelector, EVENT_READ, EVENT_WRITE
|
||||||
|
|
||||||
|
|
||||||
class FIFOKernel:
|
class FIFOKernel:
|
||||||
|
@ -120,18 +120,13 @@ class FIFOKernel:
|
||||||
# There's tasks sleeping and/or on the
|
# There's tasks sleeping and/or on the
|
||||||
# ready queue!
|
# ready queue!
|
||||||
return False
|
return False
|
||||||
if self.selector.get_map():
|
if self.get_active_io_count():
|
||||||
for key in self.selector.get_map().values():
|
# We don't just do any([self.paused, self.run_ready, self.selector.get_map()])
|
||||||
# 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,
|
||||||
# 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
|
||||||
# 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
|
||||||
# waiting on. This avoids issues such as the event loop never exiting if the
|
# user forgets to close a socket, for example
|
||||||
# user forgets to close a socket, for example
|
return False
|
||||||
key.data: Task
|
|
||||||
if key.data.done():
|
|
||||||
continue
|
|
||||||
elif self.get_task_io(key.data):
|
|
||||||
return False
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def close(self, force: bool = False):
|
def close(self, force: bool = False):
|
||||||
|
@ -193,15 +188,15 @@ class FIFOKernel:
|
||||||
self.debugger.before_io(timeout)
|
self.debugger.before_io(timeout)
|
||||||
# Get sockets that are ready and schedule their tasks
|
# Get sockets that are ready and schedule their tasks
|
||||||
for key, _ in self.selector.select(timeout):
|
for key, _ in self.selector.select(timeout):
|
||||||
key.data: Task
|
key.data: dict[int, Task]
|
||||||
if key.data.state == TaskState.IO:
|
for task in key.data.values():
|
||||||
# We don't reschedule a task that wasn't
|
# We don't reschedule a task that wasn't
|
||||||
# blocking on I/O before: this way if a
|
# blocking on I/O before: this way if a
|
||||||
# task waits on a socket and then goes to
|
# task waits on a socket and then goes to
|
||||||
# sleep, it won't be woken up early if the
|
# sleep, it won't be woken up early if the
|
||||||
# resource becomes available before its
|
# resource becomes available before its
|
||||||
# deadline expires
|
# deadline expires
|
||||||
self.run_ready.append(key.data) # Resource ready? Schedule its task
|
self.run_ready.append(task) # 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):
|
||||||
|
@ -261,10 +256,15 @@ class FIFOKernel:
|
||||||
# Sets the currently running task
|
# Sets the currently running task
|
||||||
self.current_task = self.run_ready.popleft()
|
self.current_task = self.run_ready.popleft()
|
||||||
while self.current_task.done():
|
while self.current_task.done():
|
||||||
# We need to make sure we don't try to execute
|
# We make sure not to schedule
|
||||||
# exited tasks that are on the running queue
|
# any terminated tasks. Might want
|
||||||
|
# to eventually get rid of this code,
|
||||||
|
# but for now it does the job
|
||||||
if not self.run_ready:
|
if not self.run_ready:
|
||||||
return # No more tasks to run!
|
# We'll let run() handle the I/O
|
||||||
|
# or the shutdown if necessary, as
|
||||||
|
# there are no more runnable tasks
|
||||||
|
return
|
||||||
self.current_task = self.run_ready.popleft()
|
self.current_task = self.run_ready.popleft()
|
||||||
# We nullify the exception object just in case the
|
# We nullify the exception object just in case the
|
||||||
# entry point raised and caught an error so that
|
# entry point raised and caught an error so that
|
||||||
|
@ -326,13 +326,13 @@ class FIFOKernel:
|
||||||
# the closest deadline to avoid starving sleeping tasks
|
# the closest deadline to avoid starving sleeping tasks
|
||||||
# or missing deadlines
|
# or missing deadlines
|
||||||
if self.selector.get_map():
|
if self.selector.get_map():
|
||||||
self.wait_io()
|
self.handle_errors(self.wait_io)
|
||||||
if self.paused:
|
if self.paused:
|
||||||
# Next we check for deadlines
|
# Next we check for deadlines
|
||||||
self.awake_tasks()
|
self.handle_errors(self.awake_tasks)
|
||||||
else:
|
else:
|
||||||
# Otherwise, while there are tasks ready to run, we run them!
|
# Otherwise, while there are tasks ready to run, we run them!
|
||||||
self.handle_task_run(self.run_task_step)
|
self.handle_errors(self.run_task_step)
|
||||||
|
|
||||||
def start(
|
def start(
|
||||||
self, func: Callable[..., Coroutine[Any, Any, Any]], *args, **kwargs
|
self, func: Callable[..., Coroutine[Any, Any, Any]], *args, **kwargs
|
||||||
|
@ -369,6 +369,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)
|
self.debugger.on_io_unschedule(resource)
|
||||||
|
self.reschedule_running()
|
||||||
|
|
||||||
def io_release_task(self, task: Task):
|
def io_release_task(self, task: Task):
|
||||||
"""
|
"""
|
||||||
|
@ -377,19 +378,26 @@ class FIFOKernel:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
for key in filter(
|
for key in filter(
|
||||||
lambda k: k.data == task, dict(self.selector.get_map()).values()
|
lambda k: task in k.data.values(), dict(self.selector.get_map()).values()
|
||||||
):
|
):
|
||||||
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 = ()
|
||||||
|
|
||||||
def get_task_io(self, task: Task) -> list:
|
def get_active_io_count(self) -> int:
|
||||||
"""
|
"""
|
||||||
Returns the streams currently in use by
|
Returns the number of streams that are currently
|
||||||
the given task
|
being used by any active task
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return list(map(lambda k: k.fileobj, filter(lambda k: k.data == task, self.selector.get_map().values())))
|
result = 0
|
||||||
|
for key in (self.selector.get_map() or {}).values():
|
||||||
|
key.data: dict[int, Task]
|
||||||
|
for task in key.data.values():
|
||||||
|
if task.done():
|
||||||
|
continue
|
||||||
|
result += 1
|
||||||
|
return result
|
||||||
|
|
||||||
def notify_closing(self, stream, broken: bool = False):
|
def notify_closing(self, stream, broken: bool = False):
|
||||||
"""
|
"""
|
||||||
|
@ -407,10 +415,11 @@ class FIFOKernel:
|
||||||
lambda o: o.fileobj == stream,
|
lambda o: o.fileobj == stream,
|
||||||
dict(self.selector.get_map()).values(),
|
dict(self.selector.get_map()).values(),
|
||||||
):
|
):
|
||||||
if k.data != self.current_task:
|
for task in k.data.values():
|
||||||
# We don't want to raise an error inside
|
if task is not self.current_task:
|
||||||
# the task that's trying to close the stream!
|
# We don't want to raise an error inside
|
||||||
self.handle_task_run(partial(k.data.throw, exc), k.data)
|
# the task that's trying to close the stream!
|
||||||
|
self.handle_errors(partial(k.data.throw, exc), k.data)
|
||||||
self.reschedule_running()
|
self.reschedule_running()
|
||||||
|
|
||||||
def cancel(self, task: Task):
|
def cancel(self, task: Task):
|
||||||
|
@ -420,14 +429,14 @@ class FIFOKernel:
|
||||||
it fails
|
it fails
|
||||||
"""
|
"""
|
||||||
|
|
||||||
self.io_release_task(task)
|
self.handle_errors(partial(task.throw, Cancelled(task)), task)
|
||||||
self.paused.discard(task)
|
|
||||||
self.handle_task_run(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)
|
||||||
|
self.paused.discard(task)
|
||||||
self.reschedule_running()
|
self.reschedule_running()
|
||||||
|
|
||||||
def handle_task_run(self, func: Callable, task: Task | None = None):
|
def handle_errors(self, func: Callable, task: Task | None = None):
|
||||||
"""
|
"""
|
||||||
Convenience method for handling various exceptions
|
Convenience method for handling various exceptions
|
||||||
from tasks
|
from tasks
|
||||||
|
@ -470,6 +479,7 @@ class FIFOKernel:
|
||||||
task = task or self.current_task
|
task = task or self.current_task
|
||||||
task.exc = err
|
task.exc = err
|
||||||
task.state = TaskState.CRASHED
|
task.state = TaskState.CRASHED
|
||||||
|
self.debugger.on_exception_raised(task, err)
|
||||||
self.wait(task)
|
self.wait(task)
|
||||||
|
|
||||||
def sleep(self, seconds: int | float):
|
def sleep(self, seconds: int | float):
|
||||||
|
@ -499,7 +509,8 @@ 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()
|
if task is not self.current_task:
|
||||||
|
self.reschedule_running()
|
||||||
|
|
||||||
def join(self, task: Task):
|
def join(self, task: Task):
|
||||||
"""
|
"""
|
||||||
|
@ -575,19 +586,19 @@ class FIFOKernel:
|
||||||
|
|
||||||
self.current_task.state = TaskState.IO
|
self.current_task.state = TaskState.IO
|
||||||
if self.current_task.last_io:
|
if self.current_task.last_io:
|
||||||
# Since, most of the time, tasks will perform multiple
|
# Since most of the time tasks will perform multiple
|
||||||
# I/O operations on a given resource, unregistering them
|
# I/O operations on a given resource, unregistering them
|
||||||
# every time isn't a sensible approach. A quick and
|
# every time isn't a sensible approach. A quick and
|
||||||
# easy optimization to address this problem is to
|
# easy optimization to address this problem is to
|
||||||
# store the last I/O operation that the task performed
|
# store the last I/O operation that the task performed,
|
||||||
# together with the resource itself, inside the task
|
# together with the resource itself, inside the task
|
||||||
# object. If the task then tries to perform the same
|
# object. If the task then tries to perform the same
|
||||||
# operation on the same resource again, then this method
|
# operation on the same resource again, this method then
|
||||||
# returns immediately as it is already being watched by
|
# returns immediately as the resource is already being watched
|
||||||
# the selector. If the resource is the same, but the
|
# by the selector. If the resource is the same, but the
|
||||||
# event type has changed, then we modify the resource's
|
# event type has changed, then we modify the resource's
|
||||||
# associated event. Only if the resource is different from
|
# associated event. Only if the resource is different from
|
||||||
# the last one used, then this method will register a new
|
# the last one used then this method will register a new
|
||||||
# one
|
# one
|
||||||
if self.current_task.last_io == (evt_type, resource):
|
if self.current_task.last_io == (evt_type, resource):
|
||||||
# Selector is already listening for that event on
|
# Selector is already listening for that event on
|
||||||
|
@ -595,7 +606,7 @@ class FIFOKernel:
|
||||||
return
|
return
|
||||||
elif self.current_task.last_io[1] == resource:
|
elif self.current_task.last_io[1] == resource:
|
||||||
# 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, {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)
|
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:
|
||||||
|
@ -603,21 +614,26 @@ class FIFOKernel:
|
||||||
# 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, {evt_type: self.current_task})
|
||||||
self.debugger.on_io_schedule(resource, evt_type)
|
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)
|
||||||
if key.data == self.current_task or evt_type != key.events:
|
if key.data[key.events] == self.current_task:
|
||||||
# If the task that registered the stream
|
# If the task that registered the stream
|
||||||
# changed their mind on what they want
|
# changed their mind on what they want
|
||||||
# to do with it, who are we to deny their
|
# to do with it, who are we to deny their
|
||||||
# request? We also modify the event in
|
# request?
|
||||||
|
self.selector.modify(resource, key.events | evt_type, {EVENT_READ: self.current_task,
|
||||||
|
EVENT_WRITE: self.current_task})
|
||||||
|
self.debugger.on_io_schedule(resource, evt_type)
|
||||||
|
elif key.events != evt_type:
|
||||||
|
# We also modify the event in
|
||||||
# our selector so that one task can read
|
# our selector so that one task can read
|
||||||
# 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, key.events | evt_type, {evt_type: self.current_task,
|
||||||
self.debugger.on_io_schedule(resource, evt_type)
|
key.events: list(key.data.values())[0]})
|
||||||
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),
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
|
import sys
|
||||||
|
import logging
|
||||||
import aiosched
|
import aiosched
|
||||||
from debugger import Debugger
|
from debugger import Debugger
|
||||||
import logging
|
|
||||||
import sys
|
|
||||||
|
|
||||||
# A test to check for asynchronous I/O
|
# A test to check for asynchronous I/O
|
||||||
|
|
||||||
|
|
Reference in New Issue