Updated README, added debugging utility and simplified example

This commit is contained in:
nocturn9x 2020-11-18 12:13:46 +01:00
parent a5764255ac
commit d60a372af5
12 changed files with 567 additions and 138 deletions

263
README.md
View File

@ -190,44 +190,34 @@ To demonstrate this, have a look a this example
```python
import giambio
async def countdown(n: int):
print(f"Counting down from {n}!")
while n > 0:
print(f"Down {n}")
n -= 1
await giambio.sleep(1)
print("Countdown over")
return 0
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 countup(stop: int):
print(f"Counting up to {stop}!")
x = 0
while x < stop:
print(f"Up {x}")
x += 1
await giambio.sleep(2)
print("Countup over")
return 1
async def main():
start = giambio.clock()
async with giambio.create_pool() as pool:
pool.spawn(countdown, 10)
pool.spawn(countup, 5)
print("Children spawned, awaiting completion")
print(f"Task execution complete in {giambio.clock() - start:2f} seconds")
pool.spawn(child)
pool.spawn(child1)
print("[main] Children spawned, awaiting completion")
print(f"[main] Children execution complete in {giambio.clock() - start:.2f} seconds")
if __name__ == "__main__":
giambio.run(main)
```
There is a lot going on here, and we'll explain every bit of it step by step:
- First, we imported giambio and defined two async functions: `countup` and `countdown`
- These two functions do exactly what their name suggests, but for the purposes of
this tutorial, `countup` will be running twice as slow as `countdown` (see the call
to `await giambio.sleep(2)`?)
- First, we imported giambio and defined two async functions: `child` and `child1`
- These two functions will just print something and then sleep for 2 seconds
- Here comes the real fun: `async with`? What's going on there?
As it turns out, Python 3.5 didn't just add async functions, but also quite a bit
of related new syntax. One of the things that was added is asynchronous context managers.
@ -261,34 +251,18 @@ exceptions in giambio always behave as expected
Ok, so, let's try running this snippet and see what we get:
```
Children spawned, awaiting completion
Counting down from 10!
Down 10
Counting up to 5!
Up 0
Down 9
Up 1
Down 8
Down 7
Up 2
Down 6
Down 5
Up 3
Down 4
Down 3
Up 4
Down 2
Down 1
Countup over
Countdown over
Task execution complete in 10.07 seconds
[child] Child spawned!! Sleeping for 2 seconds
[child 1] Child spawned!! Sleeping for 2 seconds
[child] Had a nice nap!
[child 1] Had a nice nap!
[main] Children execution complete in 2.01 seconds
```
(Your output might have some lines swapped compared to this)
You see how `countup` and `countdown` both start and finish
together? Moreover, even though each function slept for about 10
seconds (therefore 20 seconds total), the program just took 10
You see how `child` and `child1` both start and finish
together? Moreover, even though each function slept for about 2
seconds (therefore 4 seconds total), the program just took 2
seconds to complete, so our children are really running at the same time.
If you've ever done thread programming, this will feel like home, and that's good:
@ -358,20 +332,191 @@ then giambio will switch less frequently, hurting concurrency. It turns out that
for that is calling `await giambio.sleep(0)`; This will implicitly let giambio kick in and do its job,
and it will reschedule the caller almost immediately, because the sleep time is 0.
### Mix and match? No thanks
You may wonder whether you can mix async libraries: for instance, can we call `trio.sleep` in a
giambio application? The answer is no, we can't, and there's a reason for that. Giambio wraps all
your asynchronous code in its event loop, which is what actually runs the tasks. When you call
`await giambio.something()`, what you're doing is sending "commands" to the event loop asking it
to perform a certain thing in a given task, and to communicate your intent to the loop, the
primitives (such as `giambio.sleep`) talk a language that only giambio's event loop can understand.
### A closer look
In the above section we explained the theory behind async functions, but now we'll inspect the magic behind
`giambio.run()` and its event loop to demistify _how_ giambio makes this whole async thing happen. Luckily for us,
giambio has some useful tooling that lets us sneak peak inside the machinery of the library to better help us
understand what's going on, located at `giambio.debug.BaseDebugger`. That's an abstract class that we can customize
for our purposes and that communicates with the event loop about everything it's going, so let's code it:
```python
class Debugger(giambio.debug.BaseDebugger):
"""
A simple debugger for this test
"""
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):
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 (timeout {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}'")
```
To use our debugger class, we need to pass it to `giambio.run()` using
the `debugger` keyword argument, like so:
```python
...
if __name__ == "__main__":
giambio.run(main, debugger=Debugger())
```
__Note__: Note that we passed an _instance_ (see the parentheses?) **not** a class
Running that modified code will produce a lot of output, and it should look something like this:
```
## Started running
-> About to run a step for 'main'
>> A task named 'child' was spawned
>> A task named 'child1' was spawned
[main] Children spawned, awaiting completion
<- Ran a step for 'main'
-> About to run a step for 'child'
[child] Child spawned!! Sleeping for 2 seconds
<- Ran a step for 'child'
# About to put 'child' to sleep for 2.00 seconds
-> About to run a step for 'child1'
[child 1] Child spawned!! Sleeping for 2 seconds
<- Ran a step for 'child1'
# About to put 'child1' to sleep for 2.00 seconds
[... 2 seconds pass ...]
# Task 'child' slept for 2.01 seconds
# Task 'child1' slept for 2.01 seconds
!! About to check for I/O for up to 0.00 seconds
!! Done I/O check (timeout 0.00 seconds)
-> About to run a step for 'child'
[child] Had a nice nap!
<< Task 'child' exited
-> About to run a step for 'child1'
[child 1] Had a nice nap!
<< Task 'child1' exited
-> About to run a step for 'main'
<- Ran a step for 'main'
-> About to run a step for 'main'
[main] Children execution complete in 2.01 seconds
<< Task 'main' exited
## Finished running
```
As expected, this prints _a lot_ of stuff, but let's start going trough it:
- First, we start the event loop: That's the call to `giambio.run()`
```
## Started running
```
- After that, we start running the `main` function
```
-> About to run a step for 'main'
```
- When we run `main`, that enters the `async with` block and spawns our children,
as well as execute our call to `print`
```
>> A task named 'child' was spawned
>> A task named 'child1' was spawned
[main] Children spawned, awaiting completion
```
- After that, we hit the end of the block, so we pause and wait for our children
to complete: That's when we start switching, and `child` can now run
```
<- Ran a step for 'main'
-> About to run a step for 'child'
[child] Child spawned!! Sleeping for 2 seconds
```
- We're now at `await giambio.sleep(2)` inside `child`, and that puts it to sleep
```
<- Ran a step for 'child'
# About to put 'child' to sleep for 2.00 seconds
```
- Ok, so now `child` is asleep while `main` is waiting on its children, and `child1` can now execute,
so giambio switches again and runs that
```
-> About to run a step for 'child1'
[child 1] Child spawned!! Sleeping for 2 seconds
```
- Now we hit the call to `await giambio.sleep(2)` inside `child1`, so that also goes to sleep
```
<- Ran a step for 'child1'
# About to put 'child1' to sleep for 2.00 seconds
```
- Since there is no other work to do, giambio just waits until it wakes up the two children,
2 seconds later
```
# Task 'child' slept for 2.01 seconds
# Task 'child1' slept for 2.01 seconds
```
- Even though we're not doing any I/O here, giambio doesn't know that, so it
does some checks (and finds out there is no I/O to do)
```
!! About to check for I/O for up to 0.00 seconds
!! Done I/O check (timeout 0.00 seconds)
```
- After 2 seconds have passed giambio wakes up our children and runs them until completion
```
-> About to run a step for 'child'
[child] Had a nice nap!
<< Task 'child' exited
-> About to run a step for 'child1'
[child 1] Had a nice nap!
<< Task 'child1' exited
```
- As promised, once all children exit, the parent task resumes and runs until it exits. This also
causes the entire event loop to exit because there is nothing else to do
```
-> About to run a step for 'main'
<- Ran a step for 'main'
-> About to run a step for 'main'
[main] Children execution complete in 2.01 seconds
<< Task 'main' exited
## Finished running
```
So, in our example, our children run until they hit a call to `await giambio.sleep`, then execution control
goes back to `giambio.run`, which drives the execution for another step. This works because `giambio.sleep` and
`giambio.run` (as well as many others) work together to make this happen: `giambio.sleep` can pause the execution
of its children task and ask `giambio.run` to wake him up after a given amount of time
__Note__: You may wonder whether you can mix async libraries: for instance, can we call `trio.sleep` in a
giambio application? The answer is no, we can't, and this section explains why. When you call
`await giambio.sleep` that function talks a language that only `giambio.run` can understand.
Other libraries have other private "languages", so mixing them is not possible: doing so will cause
giambio to get very confused and most likely just explode spectacularly badly
TODO: I/O
## Contributing
This is a relatively young project and it is looking for collaborators! It's not rocket science,

View File

@ -22,7 +22,7 @@ from . import exceptions
from .traps import sleep, current_task
from .objects import Event
from .run import run, clock, wrap_socket, create_pool, get_event_loop, new_event_loop
from .util import debug
__all__ = [
"exceptions",
@ -34,5 +34,6 @@ __all__ = [
"create_pool",
"get_event_loop",
"current_task",
"new_event_loop"
"new_event_loop",
"debug"
]

View File

@ -44,6 +44,7 @@ class TaskManager:
task.parent = self.loop.current_task
self.loop.tasks.append(task)
self.tasks.append(task)
self.loop.debugger.on_task_spawn(task)
def spawn_after(self, func: types.FunctionType, n: int, *args):
"""
@ -53,8 +54,10 @@ class TaskManager:
assert n >= 0, "The time delay can't be negative"
task = Task(func(*args), func.__name__ or str(func))
task.parent = self.loop.current_task
task.sleep_start = self.loop.clock()
self.loop.paused.put(task, n)
self.tasks.append(task)
self.loop.debugger.on_task_schedule(task, n)
async def __aenter__(self):
return self
@ -66,5 +69,4 @@ class TaskManager:
except BaseException:
self.tasks.remove(task)
for to_cancel in self.tasks:
await to_cancel.cancel()
print("oof")
await to_cancel.cancel()

View File

@ -24,6 +24,7 @@ from timeit import default_timer
from .objects import Task, TimeQueue
from socket import SOL_SOCKET, SO_ERROR
from .traps import want_read, want_write
from .util.debug import BaseDebugger
from collections import deque
from .socket import AsyncSocket, WantWrite, WantRead
from selectors import DefaultSelector, EVENT_READ, EVENT_WRITE
@ -44,11 +45,16 @@ class AsyncScheduler:
A few examples are tasks cancellation and exception propagation.
"""
def __init__(self):
def __init__(self, debugger: BaseDebugger = None):
"""
Object constructor
"""
# The debugger object. If it is none we create a dummy object that immediately returns an empty
# lambda every time you access any of its attributes to avoid lots of if self.debugger clauses
if debugger:
assert issubclass(type(debugger), BaseDebugger), "The debugger must be a subclass of giambio.util.BaseDebugger"
self.debugger = debugger or type("DumbDebugger", (object, ), {"__getattr__": lambda *args: lambda *args: None})()
# Tasks that are ready to run
self.tasks = deque()
# Selector object to perform I/O multiplexing
@ -71,10 +77,7 @@ class AsyncScheduler:
Returns True if there is work to do
"""
if self.selector.get_map() or any([self.paused,
self.tasks,
self.events
]):
if any([self.paused, self.tasks, self.events, self.selector.get_map()]):
return False
return True
@ -97,23 +100,25 @@ class AsyncScheduler:
while True:
try:
if self.done():
# If we're done, which means there is no
# 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()
break
elif not self.tasks:
if self.paused:
# 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()
if self.events:
# Try to awake event-waiting tasks
self.check_events()
# While there are tasks to run
# If there are no actively running tasks
# we try to schedule the asleep ones
self.awake_sleeping()
# The next step is checking for I/O
self.check_io()
# Try to awake event-waiting tasks
self.check_events()
# Otherwise, while there are tasks ready to run, well, run them!
while self.tasks:
# Sets the currently running task
self.current_task = self.tasks.popleft()
self.debugger.before_task_step(self.current_task)
if self.current_task.cancel_pending:
self.do_cancel()
if self.to_send and self.current_task.status != "init":
@ -124,6 +129,7 @@ class AsyncScheduler:
method, *args = self.current_task.run(data)
self.current_task.status = "run"
self.current_task.steps += 1
self.debugger.after_task_step(self.current_task)
# Data has been sent, reset it to None
if self.to_send and self.current_task != "init":
self.to_send = None
@ -136,12 +142,14 @@ class AsyncScheduler:
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() # TODO: Investigate if a call to join() is needed
except StopIteration as ret:
# Coroutine ends
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()
except BaseException as err:
self.current_task.exc = err
@ -156,8 +164,10 @@ class AsyncScheduler:
"""
# TODO: Do we need anything else?
self.debugger.before_cancel(self.current_task)
self.current_task.throw(CancelledError)
def get_running(self):
"""
Returns the current task
@ -187,7 +197,11 @@ class AsyncScheduler:
# Sleep until the closest deadline in order not to waste CPU cycles
while self.paused[0][0] < self.clock():
# Reschedules tasks when their deadline has elapsed
self.tasks.append(self.paused.get())
task = self.paused.get()
slept = self.clock() - task.sleep_start
task.sleep_start = None
self.tasks.append(task)
self.debugger.after_sleep(task, slept)
if not self.paused:
break
@ -196,13 +210,16 @@ class AsyncScheduler:
Checks and schedules task to perform I/O
"""
if self.tasks or self.events: # If there are tasks or events, never wait
if self.tasks or self.events and not self.selector.get_map():
# If there are either tasks or events and no I/O, never wait
timeout = 0.0
elif self.paused: # If there are asleep tasks, wait until the closest
# deadline
elif self.paused:
# If there are asleep tasks, wait until the closest deadline
timeout = max(0.0, self.paused[0][0] - self.clock())
else:
timeout = None # If we _only_ have I/O to do, then wait indefinitely
elif self.selector.get_map():
# If there is *only* I/O, we wait a fixed amount of time
timeout = 1 # TODO: Is this ok?
self.debugger.before_io(timeout)
for key in dict(self.selector.get_map()).values():
# We make sure we don't reschedule finished tasks
if key.data.finished:
@ -213,6 +230,7 @@ class AsyncScheduler:
# 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(timeout)
def start(self, func: types.FunctionType, *args):
"""
@ -221,8 +239,10 @@ class AsyncScheduler:
entry = Task(func(*args), func.__name__ or str(func))
self.tasks.append(entry)
self.debugger.on_start()
self.run()
self.has_ran = True
self.debugger.on_exit()
if entry.exc:
raise entry.exc from None
@ -254,8 +274,10 @@ class AsyncScheduler:
Puts the caller to sleep for a given amount of seconds
"""
self.debugger.before_sleep(self.current_task, seconds)
if seconds:
self.current_task.status = "sleep"
self.current_task.sleep_start = self.clock()
self.paused.put(self.current_task, seconds)
else:
self.tasks.append(self.current_task)

View File

@ -43,6 +43,7 @@ class Task:
joined: bool= False
cancel_pending: bool = False
waiters: list = field(default_factory=list)
sleep_start: int = None
def run(self, what=None):
"""

View File

@ -22,6 +22,7 @@ from .core import AsyncScheduler
from .exceptions import GiambioError
from .context import TaskManager
from .socket import AsyncSocket
from .util.debug import BaseDebugger
from types import FunctionType, CoroutineType, GeneratorType
@ -40,7 +41,7 @@ def get_event_loop():
raise GiambioError("no event loop set") from None
def new_event_loop():
def new_event_loop(debugger: BaseDebugger):
"""
Associates a new event loop to the current thread
and deactivates the old one. This should not be
@ -50,23 +51,25 @@ def new_event_loop():
try:
loop = thread_local.loop
except AttributeError:
thread_local.loop = AsyncScheduler()
thread_local.loop = AsyncScheduler(debugger)
else:
if not loop.done():
raise GiambioError("cannot set event loop while running")
else:
thread_local.loop = AsyncScheduler()
thread_local.loop = AsyncScheduler(debugger)
def run(func: FunctionType, *args):
def run(func: FunctionType, *args, **kwargs):
"""
Starts the event loop from a synchronous entry point
"""
if isinstance(func, (CoroutineType, GeneratorType)):
raise RuntimeError("Looks like you tried to call giambio.run(your_func(arg1, arg2, ...)), that is wrong!"
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, ...)")
new_event_loop()
elif not isinstance(func, FunctionType):
raise GiambioError("gaibmio.run() requires an async function as parameter!")
new_event_loop(kwargs.get("debugger", None))
thread_local.loop.start(func, *args)
@ -76,15 +79,20 @@ def clock():
loop
"""
return thread_local.loop.clock()
try:
return thread_local.loop.clock()
except AttributeError:
raise GiambioError("Cannot call clock from outside an async context") from None
def wrap_socket(sock: socket.socket) -> AsyncSocket:
"""
Wraps a synchronous socket into a giambio.socket.AsyncSocket
"""
return thread_local.loop.wrap_socket(sock)
try:
return thread_local.loop.wrap_socket(sock)
except AttributeError:
raise GiambioError("Cannot wrap a socket from outside an async context") from None
def create_pool():
@ -95,5 +103,5 @@ def create_pool():
try:
return TaskManager(thread_local.loop)
except AttributeError:
raise RuntimeError("It appears that giambio is not running, did you call giambio.async_pool()"
raise GiambioError("It appears that giambio is not running, did you call giambio.create_pool()"
" outside of an async context?") from None

15
giambio/util/__init__.py Normal file
View File

@ -0,0 +1,15 @@
"""
Copyright (C) 2020 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
http://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.
"""

197
giambio/util/debug.py Normal file
View File

@ -0,0 +1,197 @@
"""
Tooling for debugging giambio
Copyright (C) 2020 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
http://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.
"""
from abc import ABC, abstractmethod
from giambio.objects import Task
from typing import Union
class BaseDebugger(ABC):
"""
The base for all debugger objects
"""
@abstractmethod
def on_start(self):
"""
This method is called when the event
loop starts executing
"""
raise NotImplementedError
@abstractmethod
def on_exit(self):
"""
This method is called when the event
loop exits entirely (all tasks completed)
"""
raise NotImplementedError
@abstractmethod
def on_task_schedule(self, task: Task, delay: Union[int, 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: int
"""
raise NotImplementedError
@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
"""
raise NotImplementedError
@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
"""
raise NotImplementedError
@abstractmethod
def before_task_step(self, task: Task):
"""
This method is called right before
calling its run() method
:param task: The Task object representing a
giambio Task and wrapping a coroutine
:type task: class: giambio.objects.Task
"""
raise NotImplementedError
@abstractmethod
def after_task_step(self, task: Task):
"""
This method is called right after
calling its run() method
:param task: The Task object representing a
giambio Task and wrapping a coroutine
:type task: class: giambio.objects.Task
"""
raise NotImplementedError
@abstractmethod
def before_sleep(self, task: Task, seconds: Union[int, 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
"""
raise NotImplementedError
@abstractmethod
def after_sleep(self, task: Task, seconds: Union[int, float]):
"""
This method is called before 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: int
"""
raise NotImplementedError
@abstractmethod
def before_io(self, timeout: Union[int, 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: int
"""
raise NotImplementedError
@abstractmethod
def after_io(self, timeout: Union[int, float]):
"""
This method is called right after
the event loop has checked for I/O events
:param timeout: The max. amount of seconds
that the loop has hung when using the select()
system call
:type timeout: int
"""
raise NotImplementedError
@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
"""
raise NotImplementedError
@abstractmethod
def after_cancel(self, task: Task):
"""
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
"""
raise NotImplementedError

8
pyvenv.cfg Normal file
View File

@ -0,0 +1,8 @@
home = /usr
implementation = CPython
version_info = 3.7.3.final.0
virtualenv = 20.1.0
include-system-site-packages = false
base-prefix = /usr
base-exec-prefix = /usr
base-executable = /usr/bin/python3

View File

@ -1,42 +0,0 @@
import giambio
# A test for context managers
async def countdown(n: int):
print(f"Counting down from {n}!")
while n > 0:
print(f"Down {n}")
n -= 1
await giambio.sleep(1)
# raise Exception("oh no man") # Uncomment to test propagation
print("Countdown over")
return 0
async def countup(stop: int, step: int = 1):
print(f"Counting up to {stop}!")
x = 0
while x < stop:
print(f"Up {x}")
x += 1
await giambio.sleep(step)
print("Countup over")
return 1
async def main():
start = giambio.clock()
try:
async with giambio.create_pool() as pool:
pool.spawn(countdown, 10)
pool.spawn(countup, 5, 2)
print("Children spawned, awaiting completion")
except Exception as e:
print(f"Got -> {type(e).__name__}: {e}")
print(f"Task execution complete in {giambio.clock() - start:.2f} seconds")
if __name__ == "__main__":
giambio.run(main)

View File

@ -21,7 +21,7 @@ async def serve(address: tuple):
conn, addr = await asock.accept()
logging.info(f"{addr[0]}:{addr[1]} connected")
pool.spawn(handler, conn, addr)
print("oof done")
async def handler(sock: AsyncSocket, addr: tuple):
addr = f"{addr[0]}:{addr[1]}"

72
tests/sleep.py Normal file
View File

@ -0,0 +1,72 @@
import giambio
class Debugger(giambio.debug.BaseDebugger):
"""
A simple debugger for this test
"""
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):
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 (timeout {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}'")
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 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 e:
print(f"Got -> {type(e).__name__}: {e}")
print(f"[main] Children execution complete in {giambio.clock() - start:.2f} seconds")
if __name__ == "__main__":
giambio.run(main, debugger=Debugger())