Source code for m4us.core.schedulers

# -*- coding: utf-8 -*-

#---Header---------------------------------------------------------------------

# This file is part of Message For You Sir (m4us).
# Copyright © 2009-2012 Krys Lawrence
#
# Message For You Sir is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or (at your
# option) any later version.
#
# Message For You Sir is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Affero General Public License
# for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with Message For You Sir.  If not, see <http://www.gnu.org/licenses/>.


"""Provides a variety of `scheduler` classes to coordinate `coroutines`.

`Schedulers` are responsible for the main program loop, cycling through each
registered `coroutine` in turn.  They also send `post office` `messages` into
the registered `coroutines` and post emitted `messages` back to the `post
office`.

"""


#---Imports--------------------------------------------------------------------

#---  Standard library imports
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
## pylint: disable=W0622, W0611
from future_builtins import ascii, filter, hex, map, oct, zip  ## NOQA
## pylint: enable=W0622, W0611

import collections

#---  Third-party imports
## pylint: disable=F0401
from zope import interface
## pylint: enable=F0401

#---  Project imports
from  . import messages, utils, exceptions, interfaces


#---Globals--------------------------------------------------------------------


#---Functions------------------------------------------------------------------


#---Classes--------------------------------------------------------------------

@interface.implementer(interfaces.IScheduler)
@interface.provider(interfaces.ISchedulerFactory)
[docs]class Scheduler(object): """The standard `scheduler` class to run `coroutines`. This is a simple and reasonably efficient implementation of :class:`~m4us.core.interfaces.IScheduler`. Unless you have a special need, this is this probably `scheduler` you want to use. :param m4us.core.interfaces.IPostOffice post_office: The `post office` object to use for `message` routing. :raises exceptions.TypeError: If the given `post office` does not provide or cannot be adapted to :class:`~m4us.core.interfaces.IPostOffice`. :implements: :class:`m4us.core.interfaces.IScheduler` :provides: :class:`m4us.core.interfaces.ISchedulerFactory` .. seealso:: The :class:`~m4us.core.interfaces.IPostOffice` `interface` for details on `post offices`. """ def __init__(self, post_office, add_ignores_duplicates=False, remove_ignores_missing=False): """See class docstring for this method's documentation.""" # Raises TypeError if not adaptable to IPostOffice. interfaces.IPostOffice(post_office) self._post_office = post_office self._ignore_duplicates = add_ignores_duplicates self._ignore_missing = remove_ignores_missing # We use a deque for it's rotate() and popleft() (and remove() by # extension) which are more efficient than a regular list. self._run_queue = collections.deque() self._shutting_downs = set()
[docs] def register(self, first_coroutine, *other_coroutines): """Register one or more `coroutines` with the `scheduler`. .. seealso:: The :class:`~m4us.core.interfaces.IScheduler` `interface` for details about this method. """ for coroutine in (first_coroutine,) + other_coroutines: # Raise TypeError of not adaptable to ICoroutine interfaces.ICoroutine(coroutine) if coroutine in self._run_queue: if self._ignore_duplicates: continue ## pylint: disable=E1101 raise exceptions.DuplicateError(coroutine=coroutine) ## pylint: enable=E1101 self._run_queue.append(coroutine)
[docs] def unregister(self, first_coroutine, *other_coroutines): """Unregister one or more `coroutines` from the `scheduler`. .. seealso:: The :class:`~m4us.core.interfaces.IScheduler` `interface` for details about this method. """ for coroutine in (first_coroutine,) + other_coroutines: try: self._run_queue.remove(coroutine) except ValueError: if self._ignore_missing: continue ## pylint: disable=E1101 raise exceptions.NotAddedError(coroutine=coroutine) ## pylint: enable=E1101 coroutine.close()
[docs] def step(self): """Run one `coroutine`. .. note:: `Coroutines` are run in a `round-robin`_ style, in the order that they have been added. `Coroutines` should not, however, rely on this implementation detail. .. seealso:: The :class:`~m4us.core.interfaces.IScheduler` `interface` for details about this method. .. _round-robin: http://en.wikipedia.org/wiki/Round-robin_scheduling """ # Note: It is assumed that either the original coroutine can store it's # state or that the same coroutine adapter is returned with every call # to ICoroutine. It is the responsibility of the adapter and/or the # adapter registry to ensure the same adapted coroutine is returned. original_coroutine = self._run_queue[0] coroutine = interfaces.ICoroutine(original_coroutine) post_office = interfaces.IPostOffice(self._post_office) inbox_messages = self._get_inbox_messages(original_coroutine) try: for inbox_message in inbox_messages: ## pylint: disable=E1121 outbox_message = coroutine.send(inbox_message) ## pylint: enable=E1121 if outbox_message is None: continue outbox, message = outbox_message # If the message is an IShutdown, the corutine should now be in # the "shutting down" state. if utils.is_shutdown(outbox, message, 'signal'): self._shutting_downs.add(original_coroutine) try: ## pylint: disable=E1121 post_office.post(original_coroutine, outbox, message) ## pylint: enable=E1121 ## pylint: disable=E1101 except exceptions.NoLinkError: ## pylint: enable=E1101 # If we get a NoLinkError and the message is an IShutdown, # then we assume it is sent from a consumer and we drop it. # If it is not an IShutdown, however, it is an error. if not interfaces.IShutdown(message, False): raise except StopIteration: self.unregister(original_coroutine) if original_coroutine in self._shutting_downs: self._shutting_downs.remove(original_coroutine) else: self._run_queue.rotate(-1)
def _get_inbox_messages(self, original_coroutine): """Return an iterable of `inbox` `messages` waiting to be delivered. if the `coroutine` is shutting down, then only a single :class:`~m4us.core.interfaces.IShutdown` ``control`` `message` will be in the returned iterable. Otherwise, if there are any waiting `messages` in the `post office`, they will be returned. If there are no `post office` `messages` for the `coroutine`, and the `coroutine` is not `lazy`, then only a :obj:`None` ``control`` `message` will be in the returned interable. :param m4us.core.interfaces.ICoroutine original_coroutine: The (unadapted) `coroutine` for which to check for `messages`. This should be the `coroutine` object as it was registered with the `post office`, not an adapter or anything. :returns: An iterable of `inbox` messages to be delivered to the given `coroutine`. :rtype: :class:`collections.Iterable` :raises m4us.core.exceptions.NeverRunError: If the `post office` raises :exc:`m4us.core.exceptions.NotASinkError` and the `coroutine` is `lazy`. """ post_office = interfaces.IPostOffice(self._post_office) is_lazy = not interfaces.INotLazy(original_coroutine, False) if original_coroutine in self._shutting_downs: inbox_messages = (('control', messages.Shutdown()),) return inbox_messages try: ## pylint: disable=E1121 inbox_messages = post_office.retrieve(original_coroutine) ## pylint: enable=E1121 ## pylint: disable=E1101 except exceptions.NotASinkError: ## pylint: enable=E1101 # If we get a NotASinkError from PostOffice.retrieve, and the # coroutine is lazy, then it will never be run. if is_lazy: ## pylint: disable=E1101 raise exceptions.NeverRunError( coroutine=original_coroutine) ## pylint: enable=E1101 # Otherwise, if it is not lazy, then we assume that it is a # producer and will be handled like any other non-lazy # coroutine. inbox_messages = () # If a coroutine is not lazy and there are no messages, then we # send None to control instead. if not inbox_messages and not is_lazy: inbox_messages = (('control', None),) return inbox_messages
[docs] def cycle(self): """Cycle once through the main loop, running all eligible `coroutines`. .. warning:: The current implementation of this method can handle a shrinking run queue due to `coroutines` shutting down, but it has not yet been designed to handle the dynamic adding or removing of `coroutines` within a the cycle execution. It's behaviour in those situations is currently undefined. .. seealso:: The :class:`~m4us.core.interfaces.IScheduler` `interface` for details about this method. .. seealso:: The :meth:`step` method for details how the `coroutines` are run. """ # TODO: Handle changes in _run_queue length? for _ in xrange(len(self._run_queue)): self.step()
[docs] def run(self, cycles=None): """Start the `scheduler`, running all registered `coroutines`. .. warning:: If *cycles* is not specified and any of the `coroutines` do not cascade :class:`~m4us.core.interfaces.IShutdown` `messages` or shutdown properly, then this method will likely loop forever. .. seealso:: The :class:`~m4us.core.interfaces.IScheduler` `interface` for details about this method. .. seealso:: The :meth:`cycle` method for details how the `coroutines` are run. .. seealso:: The :class:`~m4us.core.interfaces.IShutdown` `interface` for details on shutdown messages. """ if cycles is not None: for _ in xrange(cycles): self.cycle() return while self._run_queue: self.cycle() #---Module initialization------------------------------------------------------ #---Late Imports--------------------------------------------------------------- #---Late Globals--------------------------------------------------------------- #---Late Functions------------------------------------------------------------- #---Late Classes--------------------------------------------------------------- #---Late Module initialization-------------------------------------------------