Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Activity api #2

Merged
merged 20 commits into from
Apr 25, 2019
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 30 additions & 4 deletions docs/source/glossary.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,23 @@
Glossary of Terms
=================

.. Using references in the glossary itself:
When mentioning other items, always reference them.
When mention the current item, never reference it.
maxfischer2781 marked this conversation as resolved.
Show resolved Hide resolved


.. glossary::

Activity
Ongoing action that drives forward a simulation - either through time or events.
Activities may be suspended and resumed as desired, or interrupted involuntarily.
Ongoing action that drives forward a simulation - either through :term:`time` or :term:`events <event>`.
Activities may be :term:`suspended <Suspension>` and resumed as desired, or interrupted involuntarily.

Time
Representation of the progression of a simulation.
Whereas the unit of time is arbitrary, its value always grows.

Time may only pass while all :term:`activities <Activity>` are *suspended*.
Time may only pass while all :term:`activities <Activity>`
are :term:`postponed <Postponement>` until a later time, not :term:`turn`.
maxfischer2781 marked this conversation as resolved.
Show resolved Hide resolved
An :term:`activity` may actively wait for the progression of time,
or implicitly delay until an event happens at a future point in time.

Expand All @@ -23,10 +28,31 @@ Glossary of Terms
Event
A well-defined occurrence at a specific point in :term:`time`.
Events may occur
as result of activities ("dinner is done"),
as result of activities ("when dinner is done"),
as time passes ("after 20 time units"),
or
at predefined points in time ("at 2000 time units"),

Notification
Information sent to an :term:`activity`, usually in response to an :term:`event`.
Notifications can only be received when an :term:`activity` is :term:`suspended <Suspension>`.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Who can receive the notification? The activity? What about postponed activities?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Being postponed is a special case of being suspended. Should it be made clear that both apply?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, definition of Postponement and Suspension should maybe made more clear. You could put into the description of Postponement that this is a special case of being suspended. This might help.


Postponement
:term:`Suspension` of an :term:`activity` until a later :term:`turn` at the same :term:`time`.
When an :term:`activity` is postponed,
other :term:`activities <Activity>` may run but :term:`time` does not advance.
If there are no other :term:`activities <Activity>` to resume,
a postponed :term:`activity` is resumed immediately.
maxfischer2781 marked this conversation as resolved.
Show resolved Hide resolved

:note: μSim guarantees that all its primitives postpone on asynchronous operations.
This ensures that activities are reliably and deterministically interwoven.

Suspension
Pause in the execution of an :term:`activity`,
maxfischer2781 marked this conversation as resolved.
Show resolved Hide resolved
allowing other :term:`activities <activity>` or :term:`time` to advance.
A suspended activity is only resumed when it receives a :term:`notification`.

Suspension can only occur as part of asynchronous statements:
waiting for the target of an ``await`` statement,
fetching the next item of an ``async for`` statement,
and entering/exiting an ``async with`` block.
2 changes: 1 addition & 1 deletion docs/source/tutorial/03_scopes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ We again use ``usim.time`` to track and influence the progression of our simulat
... await (time + 1) # 3
... drivers.do(deliver_one(3))
... print('Sent deliveries at', time.now) # 4.1
... print('-- Done deliveries at', time.now) # 4.2
... print('-- Done deliveries at', time.now) # 4.2

Scopes can be difficult because they are inherently about doing several things at once.
It helps to step through individual points of notice:
Expand Down
21 changes: 21 additions & 0 deletions docs/source/tutorial/04_cancel_scope.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@

Interlude 01: Interrupting Scopes
---------------------------------

.. code:: python3

>>> from usim import time, until as out
maxfischer2781 marked this conversation as resolved.
Show resolved Hide resolved
>>>
>>> async def deliver_one(which):
... print('Delivering', which, 'at', time.now)
... await (time + 5)
... print('Delivered', which, 'at', time.now)
>>>
>>> async def deliver_all(count=3):
... print('-- Start deliveries at', time.now)
... async with out(time + 10) as deliveries: # 1
maxfischer2781 marked this conversation as resolved.
Show resolved Hide resolved
... for delivery in range(count): # 2
... deliveries.do(deliver_one(delivery))
... await (time + 3)
... print('Sent deliveries at', time.now) # 4.1
... print('-- Done deliveries at', time.now) # 4.2
143 changes: 104 additions & 39 deletions usim/_primitives/activity.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from functools import wraps
import enum
from typing import Coroutine, TypeVar, Generic, Optional, Tuple, Any, List
from typing import Coroutine, TypeVar, Awaitable, Optional, Tuple, Any, List

from .._core.loop import __LOOP_STATE__, Interrupt
from .condition import Condition
Expand Down Expand Up @@ -51,16 +51,37 @@ def __transcript__(self) -> ActivityCancelled:


class ActivityExit(BaseException):
...
"""A :py:class:`~.Activity` forcefully exited"""


class Activity(Condition, Generic[RT]):
class Activity(Awaitable[RT]):
"""
Active coroutine that allows others to listen for its completion
Concurrently running activity that allows multiple activities to await its completion
maxfischer2781 marked this conversation as resolved.
Show resolved Hide resolved

:note: Simulation code should never instantiate this class directly.
A :py:class:`Activity` represents an activity that is concurrently run in a :py:class:`~.Scope`.
maxfischer2781 marked this conversation as resolved.
Show resolved Hide resolved
This allows to store or pass an an :py:class:`Activity`, in order to check its progress.
maxfischer2781 marked this conversation as resolved.
Show resolved Hide resolved
Other activities can ``await`` a :py:class:`Activity`,
maxfischer2781 marked this conversation as resolved.
Show resolved Hide resolved
which returns any results or exceptions on completion, similar to a regular activity.
maxfischer2781 marked this conversation as resolved.
Show resolved Hide resolved

.. code:: python3

await my_activity() # await a bare activity

async with Scope() as scope:
activity = scope.do(my_activity())
await activity # await a rich activity
maxfischer2781 marked this conversation as resolved.
Show resolved Hide resolved

In contrast to a regular activity, it is possible to

* :py:meth:`~.Activity.cancel` an :py:class:`Activity` before completion,
* ``await`` the result of an :py:class:`Activity` multiple times,
and
* ``await`` that an is an :py:class:`Activity` is :py:attr:`~.Activity.done`.
maxfischer2781 marked this conversation as resolved.
Show resolved Hide resolved

:note: This class should not be instantiated directly.
Always use a :py:class:`~.Scope` to create it.
"""
__slots__ = ('payload', '_result', '_execution', '_cancellations')
__slots__ = ('payload', '_result', '__runner__', '_cancellations', '_done')

def __init__(self, payload: Coroutine[Any, Any, RT]):
@wraps(payload)
Expand All @@ -78,28 +99,29 @@ async def payload_wrapper():
self._result = result, None
for cancellation in self._cancellations:
cancellation.revoke()
self.__trigger__()
super().__init__()
self._done.__set_done__()
self._cancellations = [] # type: List[CancelActivity]
self._result = None # type: Optional[Tuple[RT, BaseException]]
self.payload = payload
self._execution = payload_wrapper()
self._done = Done(self)
self.__runner__ = payload_wrapper() # type: Coroutine[Any, Any, RT]

@property
async def result(self) -> RT:
"""
Wait for the completion of this :py:class:`Activity` and return its result

:returns: the result of the activity
:raises: :py:exc:`CancelActivity` if the activity was cancelled
"""
await self
def __await__(self):
yield from self._done.__await__()
result, error = self._result
if error is not None:
raise error
else:
return result

@property
def done(self) -> 'Done':
"""
:py:class:`~.Condition` whether the :py:class:`~.Activity` has stopped running.
This includes completion, cancellation and failure.
"""
return self._done

@property
def status(self) -> ActivityState:
"""The current status of this activity"""
Expand All @@ -109,36 +131,50 @@ def status(self) -> ActivityState:
return ActivityState.CANCELLED if isinstance(error, ActivityCancelled) else ActivityState.FAILED
return ActivityState.SUCCESS
# a stripped-down version of `inspect.getcoroutinestate`
if self._execution.cr_frame.f_lasti == -1:
if self.__runner__.cr_frame.f_lasti == -1:
return ActivityState.CREATED
return ActivityState.RUNNING

def __bool__(self):
return self._result is not None

def __invert__(self):
return NotDone(self)

def __runner__(self):
return self._execution

def __close__(self, reason=ActivityExit('activity closed')):
"""Close the underlying coroutine"""
"""
Close the underlying coroutine

This is similar to calling :py:meth:`Coroutine.close`,
but ensures that waiting activities are properly notified.
"""
if self._result is None:
self._execution.close()
self.__runner__.close()
self._result = None, reason
self._done.__set_done__()

def cancel(self, *token) -> None:
"""Cancel this activity during the current time step"""
"""
Cancel this activity during the current time step

If the :py:class:`~.Activity` is running,
a :py:class:`~.CancelActivity` is raised once the activity suspends.
The activity may catch and react to :py:class:`~.CancelActivity`,
but should not suppress it.

If the :py:class:`~.Activity` is :py:attr:`~.Activity.done` before :py:class:`~.CancelActivity` is raised,
the cancellation is ignored.
This also means that cancelling an activity multiple is allowed,
maxfischer2781 marked this conversation as resolved.
Show resolved Hide resolved
but only the first successful cancellation is stored as the cancellation cause.

If the :py:class:`~.Activity` has not started running, it is cancelled immediately.
This prevents any code execution, even before the first suspension.

:warning: The timing of cancelling an Activity before it started running may change in the future.
"""
if self._result is None:
if self.status is ActivityState.CREATED:
self._result = None, ActivityCancelled(self, *token)
self.__trigger__()
self._done.__set_done__()
else:
cancellation = CancelActivity(self, *token)
self._cancellations.append(cancellation)
cancellation.scheduled = True
__LOOP_STATE__.LOOP.schedule(self._execution, signal=cancellation)
__LOOP_STATE__.LOOP.schedule(self.__runner__, signal=cancellation)

def __repr__(self):
return '<%s of %s (%s)>' % (
Expand All @@ -158,19 +194,48 @@ def __del__(self):
# error message or traceback.
# In order not to detract with auxiliary, useless resource
# warnings, we clean up silently to hide our abstraction.
self._execution.close()
self.__runner__.close()


class NotDone(Condition):
class Done(Condition):
"""Whether a :py:class:`Activity` has stopped running"""
maxfischer2781 marked this conversation as resolved.
Show resolved Hide resolved
__slots__ = ('_activity', '_value', '_inverse')

def __init__(self, activity: Activity):
super().__init__()
self.activity = activity
self._activity = activity
self._value = False
self._inverse = NotDone(self)

def __bool__(self):
return self._value

def __invert__(self):
return self._inverse

def __set_done__(self):
"""Set the boolean value of this condition"""
assert not self._value
self._value = True
self.__trigger__()

def __repr__(self):
return '<%s for %r>' % (self.__class__.__name__, self._activity)


class NotDone(Condition):
"""Whether a :py:class:`Activity` has not stopped running"""
maxfischer2781 marked this conversation as resolved.
Show resolved Hide resolved
__slots__ = ('_done',)

def __init__(self, done: Done):
super().__init__()
self._done = done

def __bool__(self):
return not self.activity
return not self._done

def __invert__(self):
return self.activity
return self._done

def __repr__(self):
return '<%s for %r>' % (self.__class__.__name__, self.activity)
return '<%s for %r>' % (self.__class__.__name__, self._done._activity)
50 changes: 46 additions & 4 deletions usim/_primitives/condition.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,48 @@

class Condition(Notification):
"""
A logical condition that triggers when ``True``
An asynchronous logical condition

Every :py:class:`~.Condition` can be used both in a
maxfischer2781 marked this conversation as resolved.
Show resolved Hide resolved
asynchronous *and* boolean context.
In an asynchronous context,
such as ``await``,
a :py:class:`~.Condition` triggers when :py:const:`True`.
maxfischer2781 marked this conversation as resolved.
Show resolved Hide resolved
In a boolean context,
such as ``if``,
a :py:class:`~.Condition` provides its current boolean value.

.. code:: python

if condition: # resume with current value
print(condition, 'is met')
else:
print(condition, 'is not met')

await condition # resume when condition is True

async with until(condition): # abort if condition becomes False
async with out(condition): # interrupt when condition is True
...

Every :py:class:`~.Condition` supports the bitwise operators
``~a`` (not),
``a & b`` (and), and
``a | b`` (or)
to derive a new :py:class:`~.Condition`.
While it is possible to use the boolean operators
``not``, ``and``, and ``or``,
they immediately evaluate any :py:class:`~.Condition` in a boolean context.

.. code:: python

await (a & b) # resume when both a and b are True
await (a | b) # resume when one of a or b are True
await (a & ~b) # resume when a is True and b is False

c = a & b # derive new Condition...
await c # that can be awaited

d = a and b # force boolean evaluation
"""
__slots__ = ()

Expand Down Expand Up @@ -88,7 +122,11 @@ def __repr__(self):


class All(Connective):
"""Logical AND of all sub-conditions"""
"""
Logical AND of all sub-conditions

The expression ``a & b & c`` is equivalent to ``All(a, b, c)``.
"""
__slots__ = ()

def __bool__(self):
Expand All @@ -102,7 +140,11 @@ def __str__(self):


class Any(Connective):
"""Logical OR of all sub-conditions"""
"""
Logical OR of all sub-conditions

The expression ``a | b | c`` is equivalent to ``Any(a, b, c)``.
"""
__slots__ = ()

def __bool__(self):
Expand Down
4 changes: 2 additions & 2 deletions usim/_primitives/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ async def graceful(containing_scope: Scope):
"""
child_activity = Activity(payload)
__LOOP_STATE__.LOOP.schedule(
child_activity.__runner__(),
child_activity.__runner__,
delay=after, at=at
)
if not volatile:
Expand All @@ -77,7 +77,7 @@ async def graceful(containing_scope: Scope):

async def _await_children(self):
for child in self._children:
await child
await child.done

def _cancel_children(self):
for child in self._children:
Expand Down
Loading