Container clocks¶
Clocks and time are a very important instrument and required, if your agents want to delay the execution of a task for some time, schedule a task at a certain time or just need to define a timeout.
Usually, the real (or wall-clock) time is used for this. In some contexts, however, you need a different notion of time – for example if you want to couple a multi-agent system with external simulators that usually run faster than real-time.
For this reason, every agent container provides a clock via its
clock
attribute. The default clock is the
real-time clock that asyncio uses (AsyncioClock
).
An alternative clock is the ExternalClock
. The time of this clock can
be set by external processes so that the time within your agent system passes
as fast (or slow) as in that external process.
The benefit of using aiomas’ clocks compared to just using what asyncio offers
is, that you can easily switch clocks (e.g., from the AsyncioClock
to
the ExternalClock
) without touching the agents:
>>> import aiomas
>>>
>>>
>>> CLOCK = aiomas.AsyncioClock()
>>> # CLOCK = aiomas.ExternalClock('2016-01-01T00:00:00')
>>>
>>> class Sleeper(aiomas.Agent):
... async def run(self):
... # await asyncio.sleep(.5) # <-- Don't use this!
... # Depending on the clock used, this sleeps for a "real" half
... # second or whatever the ExternalClock tells you:
... await self.container.clock.sleep(.5)
>>>
>>> container = aiomas.Container.create(('127.0.0.1', 5555), clock=CLOCK)
>>> agent = Sleeper(container)
>>> aiomas.run(agent.run())
>>> container.shutdown()
(If you uncomment the ExternalClock in the example above, your program won’t terminate because there’s no process that sets its time.)
Date/time representations¶
All clocks represent time as a monotonically increasing number (not necessarily with a defined initial value) and as date/time object (for which the arrow package is used).
You can get the numeric time via the clock’s time()
method.
Its usage is comparable to that of Python’s time.monotonic()
function.
The method utcnow()
returns an Arrow
object with the current date and time in UTC.
Note
You should work with UTC dates as much as possible. Input dates with a local timezone should be converted to UTC as early as possible. If you output dates, convert them as late as possible back to local time.
Doing date and time calclulations in UTC saves you from a lot of bugs, i.e., when dealing with daylight-saving times.
This blog post by Armin Ronacher and this talk by Taavi Burns provide more background to the issue.
Sleeping¶
The container clock provides tasks that let your agent sleep for a given amount of time or until a given time is reached.
In order to sleep for a given time, you have to use the method
sleep()
with the number of seconds (as float) that you want
to sleep.
The method sleep_until()
also accepts a number in seconds
(which must be greater than the current value of time()
) or
an Arrow
date object (which must be greater than the
current value of utcnow()
).
Both methods return a future which you have to await
/ yield from
in
order to actually sleep.
Scheduling tasks¶
Comparably to sleeping, you can schedule the future execution of a task in a given period of time or at a given time.
The method call_in()
will run the specified task after
a delay dt in seconds; BaseClock.call_at()
will run the task at the
specified date (either in seconds or as Arrow
date). You
can only pass positional arguments to these methods, because that’s what the
underlying asyncio functions allow.
Both methods are normal functions that return a handle to the scheduled call.
You can use this handle to cancel()
the scheduled
execution of the task.
How to use the ExternalClock¶
Remember the first example which did not actually work if you used the
ExternalClock
? Here is a fully working version of it:
>>> import asyncio
>>> import time
>>>
>>> import aiomas
>>>
>>>
>>> CLOCK = aiomas.ExternalClock('2016-01-01T00:00:00')
>>>
>>> class Sleeper(aiomas.Agent):
... async def run(self):
... print('Gonna sleep for 1s ...')
... await self.container.clock.sleep(1)
>>>
>>>
>>> async def clock_setter(factor=0.5):
... """Let the time pass *factor* as fast as real-time."""
... while True:
... await asyncio.sleep(factor)
... CLOCK.set_time(CLOCK.time() + 1)
>>>
>>> container = aiomas.Container.create(('127.0.0.1', 5555), clock=CLOCK)
>>>
>>> # Start the process that sets the clock:
>>> t_clock_setter = asyncio.async(clock_setter())
>>>
>>> # Start the agent an measure how long he runs in real-time:
>>> agent = Sleeper(container)
>>> start = time.monotonic()
>>> aiomas.run(agent.run())
Gonna sleep for 1s ...
>>> print('Agent process finished after %.1fs' % (time.monotonic() - start))
Agent process finished after 0.5s
>>>
>>> _ = t_clock_setter.cancel()
>>> container.shutdown()
Now that we have a background process that steps the time forward, the example actually terminates.
In scenarios where you want to couple you agent system with the clock of
another system, the clock_setter()
process would not sleep but receive
clock updates from that other process and use these updates to set the agent’s
clock to a new time.
If you distribute your agent system over multiple processes, make sure that you
spread the clock updates to all agent containers. Therefore, the
Manager
agent in the aiomas.subproc
exposes
a set_time()
method that an agent in your
master process can call.