|  | @@ -22,19 +22,24 @@ import sys
 | 
	
		
			
				|  |  |  import threading
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  from datetime import datetime
 | 
	
		
			
				|  |  | -from heapq import heappush, heappop
 | 
	
		
			
				|  |  | +from decimal import Decimal
 | 
	
		
			
				|  |  | +from heapq import heapify, heappush, heappop
 | 
	
		
			
				|  |  |  from itertools import islice
 | 
	
		
			
				|  |  | +from operator import itemgetter
 | 
	
		
			
				|  |  |  from time import time
 | 
	
		
			
				|  |  | +from weakref import ref
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  from kombu.clocks import timetuple
 | 
	
		
			
				|  |  | -from kombu.utils import kwdict
 | 
	
		
			
				|  |  | +from kombu.utils import cached_property, kwdict
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  from celery import states
 | 
	
		
			
				|  |  | -from celery.datastructures import AttributeDict
 | 
	
		
			
				|  |  | -from celery.five import items, values
 | 
	
		
			
				|  |  | +from celery.five import class_property, items, values
 | 
	
		
			
				|  |  | +from celery.utils import deprecated
 | 
	
		
			
				|  |  |  from celery.utils.functional import LRUCache
 | 
	
		
			
				|  |  |  from celery.utils.log import get_logger
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | +PYPY = hasattr(sys, 'pypy_version_info')
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |  # The window (in percentage) is added to the workers heartbeat
 | 
	
		
			
				|  |  |  # frequency.  If the time between updates exceeds this window,
 | 
	
		
			
				|  |  |  # then the worker is considered to be offline.
 | 
	
	
		
			
				|  | @@ -55,15 +60,25 @@ logger = get_logger(__name__)
 | 
	
		
			
				|  |  |  warn = logger.warning
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  R_STATE = '<State: events={0.event_count} tasks={0.task_count}>'
 | 
	
		
			
				|  |  | -R_WORKER = '<Worker: {0.hostname} ({0.status_string})'
 | 
	
		
			
				|  |  | -R_TASK = '<Task: {0.name}({0.uuid}) {0.state}>'
 | 
	
		
			
				|  |  | +R_WORKER = '<Worker: {0.hostname} ({0.status_string} clock:{0.clock})'
 | 
	
		
			
				|  |  | +R_TASK = '<Task: {0.name}({0.uuid}) {0.state} clock:{0.clock}>'
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  __all__ = ['Worker', 'Task', 'State', 'heartbeat_expires']
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  def heartbeat_expires(timestamp, freq=60,
 | 
	
		
			
				|  |  | -                      expire_window=HEARTBEAT_EXPIRE_WINDOW):
 | 
	
		
			
				|  |  | -    return timestamp + freq * (expire_window / 1e2)
 | 
	
		
			
				|  |  | +                      expire_window=HEARTBEAT_EXPIRE_WINDOW,
 | 
	
		
			
				|  |  | +                      Decimal=Decimal, float=float, isinstance=isinstance):
 | 
	
		
			
				|  |  | +    # some json implementations returns decimal.Decimal objects,
 | 
	
		
			
				|  |  | +    # which are not compatible with float.
 | 
	
		
			
				|  |  | +    freq = float(freq) if isinstance(freq, Decimal) else freq
 | 
	
		
			
				|  |  | +    if isinstance(timestamp, Decimal):
 | 
	
		
			
				|  |  | +        timestamp = float(timestamp)
 | 
	
		
			
				|  |  | +    return timestamp + (freq * (expire_window / 1e2))
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +def _depickle_task(cls, fields):
 | 
	
		
			
				|  |  | +    return cls(**(fields if CAN_KWDICT else kwdict(fields)))
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  def with_unique_field(attr):
 | 
	
	
		
			
				|  | @@ -89,45 +104,71 @@ def with_unique_field(attr):
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  @with_unique_field('hostname')
 | 
	
		
			
				|  |  | -class Worker(AttributeDict):
 | 
	
		
			
				|  |  | +class Worker(object):
 | 
	
		
			
				|  |  |      """Worker State."""
 | 
	
		
			
				|  |  |      heartbeat_max = 4
 | 
	
		
			
				|  |  |      expire_window = HEARTBEAT_EXPIRE_WINDOW
 | 
	
		
			
				|  |  | -    pid = None
 | 
	
		
			
				|  |  | -    _defaults = {'hostname': None, 'pid': None, 'freq': 60}
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -    def __init__(self, **fields):
 | 
	
		
			
				|  |  | -        dict.__init__(self, self._defaults, **fields)
 | 
	
		
			
				|  |  | -        self.heartbeats = []
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -    def on_online(self, timestamp=None, local_received=None, **kwargs):
 | 
	
		
			
				|  |  | -        """Callback for the :event:`worker-online` event."""
 | 
	
		
			
				|  |  | -        self.update(**kwargs)
 | 
	
		
			
				|  |  | -        self.update_heartbeat(local_received, timestamp)
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -    def on_offline(self, **kwargs):
 | 
	
		
			
				|  |  | -        """Callback for the :event:`worker-offline` event."""
 | 
	
		
			
				|  |  | -        self.update(**kwargs)
 | 
	
		
			
				|  |  | -        self.heartbeats = []
 | 
	
		
			
				|  |  | +    _fields = ('hostname', 'pid', 'freq', 'heartbeats', 'clock',
 | 
	
		
			
				|  |  | +               'active', 'processed', 'loadavg', 'sw_ident',
 | 
	
		
			
				|  |  | +               'sw_ver', 'sw_sys')
 | 
	
		
			
				|  |  | +    if not PYPY:
 | 
	
		
			
				|  |  | +        __slots__ = _fields + ('event', '__dict__', '__weakref__')
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    def __init__(self, hostname=None, pid=None, freq=60,
 | 
	
		
			
				|  |  | +                 heartbeats=None, clock=0, active=None, processed=None,
 | 
	
		
			
				|  |  | +                 loadavg=None, sw_ident=None, sw_ver=None, sw_sys=None):
 | 
	
		
			
				|  |  | +        self.hostname = hostname
 | 
	
		
			
				|  |  | +        self.pid = pid
 | 
	
		
			
				|  |  | +        self.freq = freq
 | 
	
		
			
				|  |  | +        self.heartbeats = [] if heartbeats is None else heartbeats
 | 
	
		
			
				|  |  | +        self.clock = clock or 0
 | 
	
		
			
				|  |  | +        self.active = active
 | 
	
		
			
				|  |  | +        self.processed = processed
 | 
	
		
			
				|  |  | +        self.loadavg = loadavg
 | 
	
		
			
				|  |  | +        self.sw_ident = sw_ident
 | 
	
		
			
				|  |  | +        self.sw_ver = sw_ver
 | 
	
		
			
				|  |  | +        self.sw_sys = sw_sys
 | 
	
		
			
				|  |  | +        self.event = self._create_event_handler()
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -    def on_heartbeat(self, timestamp=None, local_received=None, **kwargs):
 | 
	
		
			
				|  |  | -        """Callback for the :event:`worker-heartbeat` event."""
 | 
	
		
			
				|  |  | -        self.update(**kwargs)
 | 
	
		
			
				|  |  | -        self.update_heartbeat(local_received, timestamp)
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -    def update_heartbeat(self, received, timestamp):
 | 
	
		
			
				|  |  | -        if not received or not timestamp:
 | 
	
		
			
				|  |  | -            return
 | 
	
		
			
				|  |  | -        drift = abs(int(received) - int(timestamp))
 | 
	
		
			
				|  |  | -        if drift > HEARTBEAT_DRIFT_MAX:
 | 
	
		
			
				|  |  | -            warn(DRIFT_WARNING, self.hostname, drift,
 | 
	
		
			
				|  |  | -                 datetime.fromtimestamp(received),
 | 
	
		
			
				|  |  | -                 datetime.fromtimestamp(timestamp))
 | 
	
		
			
				|  |  | -        heartbeats, hbmax = self.heartbeats, self.heartbeat_max
 | 
	
		
			
				|  |  | -        if not heartbeats or (received and received > heartbeats[-1]):
 | 
	
		
			
				|  |  | -            heappush(heartbeats, received)
 | 
	
		
			
				|  |  | -            if len(heartbeats) > hbmax:
 | 
	
		
			
				|  |  | -                heartbeats[:] = heartbeats[hbmax:]
 | 
	
		
			
				|  |  | +    def __reduce__(self):
 | 
	
		
			
				|  |  | +        return self.__class__, (self.hostname, self.pid, self.freq,
 | 
	
		
			
				|  |  | +                                self.heartbeats, self.clock, self.active,
 | 
	
		
			
				|  |  | +                                self.processed, self.loadavg, self.sw_ident,
 | 
	
		
			
				|  |  | +                                self.sw_ver, self.sw_sys)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    def _create_event_handler(self):
 | 
	
		
			
				|  |  | +        _set = object.__setattr__
 | 
	
		
			
				|  |  | +        heartbeats = self.heartbeats
 | 
	
		
			
				|  |  | +        hbmax = self.heartbeat_max
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        def event(type_, timestamp=None,
 | 
	
		
			
				|  |  | +                  local_received=None, fields=None,
 | 
	
		
			
				|  |  | +                  max_drift=HEARTBEAT_DRIFT_MAX, items=items, abs=abs,
 | 
	
		
			
				|  |  | +                  heappush=heappush, heappop=heappop, int=int, len=len):
 | 
	
		
			
				|  |  | +            fields = fields or {}
 | 
	
		
			
				|  |  | +            for k, v in items(fields):
 | 
	
		
			
				|  |  | +                _set(self, k, v)
 | 
	
		
			
				|  |  | +            if type_ == 'offline':
 | 
	
		
			
				|  |  | +                heartbeats[:] = []
 | 
	
		
			
				|  |  | +            else:
 | 
	
		
			
				|  |  | +                if not local_received or not timestamp:
 | 
	
		
			
				|  |  | +                    return
 | 
	
		
			
				|  |  | +                drift = abs(int(local_received) - int(timestamp))
 | 
	
		
			
				|  |  | +                if drift > HEARTBEAT_DRIFT_MAX:
 | 
	
		
			
				|  |  | +                    warn(DRIFT_WARNING, self.hostname, drift,
 | 
	
		
			
				|  |  | +                         datetime.fromtimestamp(local_received),
 | 
	
		
			
				|  |  | +                         datetime.fromtimestamp(timestamp))
 | 
	
		
			
				|  |  | +                if not heartbeats or (
 | 
	
		
			
				|  |  | +                        local_received and local_received > heartbeats[-1]):
 | 
	
		
			
				|  |  | +                    heappush(heartbeats, local_received)
 | 
	
		
			
				|  |  | +                    if len(heartbeats) > hbmax:
 | 
	
		
			
				|  |  | +                        heappop(heartbeats)
 | 
	
		
			
				|  |  | +        return event
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    def update(self, f, **kw):
 | 
	
		
			
				|  |  | +        for k, v in items(dict(f, **kw) if kw else f):
 | 
	
		
			
				|  |  | +            setattr(self, k, v)
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |      def __repr__(self):
 | 
	
		
			
				|  |  |          return R_WORKER.format(self)
 | 
	
	
		
			
				|  | @@ -142,17 +183,53 @@ class Worker(AttributeDict):
 | 
	
		
			
				|  |  |                                   self.freq, self.expire_window)
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |      @property
 | 
	
		
			
				|  |  | -    def alive(self):
 | 
	
		
			
				|  |  | -        return bool(self.heartbeats and time() < self.heartbeat_expires)
 | 
	
		
			
				|  |  | +    def alive(self, nowfun=time):
 | 
	
		
			
				|  |  | +        return bool(self.heartbeats and nowfun() < self.heartbeat_expires)
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |      @property
 | 
	
		
			
				|  |  |      def id(self):
 | 
	
		
			
				|  |  |          return '{0.hostname}.{0.pid}'.format(self)
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | +    @deprecated(3.2, 3.3)
 | 
	
		
			
				|  |  | +    def update_heartbeat(self, received, timestamp):
 | 
	
		
			
				|  |  | +        self.event(None, timestamp, received)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    @deprecated(3.2, 3.3)
 | 
	
		
			
				|  |  | +    def on_online(self, timestamp=None, local_received=None, **fields):
 | 
	
		
			
				|  |  | +        self.event('online', timestamp, local_received, fields)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    @deprecated(3.2, 3.3)
 | 
	
		
			
				|  |  | +    def on_offline(self, timestamp=None, local_received=None, **fields):
 | 
	
		
			
				|  |  | +        self.event('offline', timestamp, local_received, fields)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    @deprecated(3.2, 3.3)
 | 
	
		
			
				|  |  | +    def on_heartbeat(self, timestamp=None, local_received=None, **fields):
 | 
	
		
			
				|  |  | +        self.event('heartbeat', timestamp, local_received, fields)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    @class_property
 | 
	
		
			
				|  |  | +    def _defaults(cls):
 | 
	
		
			
				|  |  | +        """Deprecated, to be removed in 3.3"""
 | 
	
		
			
				|  |  | +        source = cls()
 | 
	
		
			
				|  |  | +        return dict((k, getattr(source, k)) for k in cls._fields)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  @with_unique_field('uuid')
 | 
	
		
			
				|  |  | -class Task(AttributeDict):
 | 
	
		
			
				|  |  | +class Task(object):
 | 
	
		
			
				|  |  |      """Task State."""
 | 
	
		
			
				|  |  | +    name = received = sent = started = succeeded = failed = retried = \
 | 
	
		
			
				|  |  | +        revoked = args = kwargs = eta = expires = retries = worker = result = \
 | 
	
		
			
				|  |  | +        exception = timestamp = runtime = traceback = exchange = \
 | 
	
		
			
				|  |  | +        routing_key = client = None
 | 
	
		
			
				|  |  | +    state = states.PENDING
 | 
	
		
			
				|  |  | +    clock = 0
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    _fields = ('uuid', 'name', 'state', 'received', 'sent', 'started',
 | 
	
		
			
				|  |  | +               'succeeded', 'failed', 'retried', 'revoked', 'args', 'kwargs',
 | 
	
		
			
				|  |  | +               'eta', 'expires', 'retries', 'worker', 'result', 'exception',
 | 
	
		
			
				|  |  | +               'timestamp', 'runtime', 'traceback', 'exchange', 'routing_key',
 | 
	
		
			
				|  |  | +               'clock', 'client')
 | 
	
		
			
				|  |  | +    if not PYPY:
 | 
	
		
			
				|  |  | +        __slots__ = ('__dict__', '__weakref__')
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |      #: How to merge out of order events.
 | 
	
		
			
				|  |  |      #: Disorder is detected by logical ordering (e.g. :event:`task-received`
 | 
	
	
		
			
				|  | @@ -166,116 +243,153 @@ class Task(AttributeDict):
 | 
	
		
			
				|  |  |                                       'retries', 'eta', 'expires')}
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |      #: meth:`info` displays these fields by default.
 | 
	
		
			
				|  |  | -    _info_fields = ('args', 'kwargs', 'retries', 'result',
 | 
	
		
			
				|  |  | -                    'eta', 'runtime', 'expires', 'exception',
 | 
	
		
			
				|  |  | -                    'exchange', 'routing_key')
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -    #: Default values.
 | 
	
		
			
				|  |  | -    _defaults = dict(uuid=None, name=None, state=states.PENDING,
 | 
	
		
			
				|  |  | -                     received=False, sent=False, started=False,
 | 
	
		
			
				|  |  | -                     succeeded=False, failed=False, retried=False,
 | 
	
		
			
				|  |  | -                     revoked=False, args=None, kwargs=None, eta=None,
 | 
	
		
			
				|  |  | -                     expires=None, retries=None, worker=None, result=None,
 | 
	
		
			
				|  |  | -                     exception=None, timestamp=None, runtime=None,
 | 
	
		
			
				|  |  | -                     traceback=None, exchange=None, routing_key=None,
 | 
	
		
			
				|  |  | -                     clock=0)
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -    def __init__(self, **fields):
 | 
	
		
			
				|  |  | -        dict.__init__(self, self._defaults, **fields)
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -    def update(self, state, timestamp, fields, _state=states.state):
 | 
	
		
			
				|  |  | -        """Update state from new event.
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -        :param state: State from event.
 | 
	
		
			
				|  |  | -        :param timestamp: Timestamp from event.
 | 
	
		
			
				|  |  | -        :param fields: Event data.
 | 
	
		
			
				|  |  | +    _info_fields = ('args', 'kwargs', 'retries', 'result', 'eta', 'runtime',
 | 
	
		
			
				|  |  | +                    'expires', 'exception', 'exchange', 'routing_key')
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    def __init__(self, uuid=None, **kwargs):
 | 
	
		
			
				|  |  | +        self.uuid = uuid
 | 
	
		
			
				|  |  | +        if kwargs:
 | 
	
		
			
				|  |  | +            for k, v in items(kwargs):
 | 
	
		
			
				|  |  | +                setattr(self, k, v)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    def event(self, type_, timestamp=None, local_received=None, fields=None,
 | 
	
		
			
				|  |  | +              precedence=states.precedence, items=items, dict=dict,
 | 
	
		
			
				|  |  | +              PENDING=states.PENDING, RECEIVED=states.RECEIVED,
 | 
	
		
			
				|  |  | +              STARTED=states.STARTED, FAILURE=states.FAILURE,
 | 
	
		
			
				|  |  | +              RETRY=states.RETRY, SUCCESS=states.SUCCESS,
 | 
	
		
			
				|  |  | +              REVOKED=states.REVOKED):
 | 
	
		
			
				|  |  | +        fields = fields or {}
 | 
	
		
			
				|  |  | +        if type_ == 'sent':
 | 
	
		
			
				|  |  | +            state, self.sent = PENDING, timestamp
 | 
	
		
			
				|  |  | +        elif type_ == 'received':
 | 
	
		
			
				|  |  | +            state, self.received = RECEIVED, timestamp
 | 
	
		
			
				|  |  | +        elif type_ == 'started':
 | 
	
		
			
				|  |  | +            state, self.started = STARTED, timestamp
 | 
	
		
			
				|  |  | +        elif type_ == 'failed':
 | 
	
		
			
				|  |  | +            state, self.failed = FAILURE, timestamp
 | 
	
		
			
				|  |  | +        elif type_ == 'retried':
 | 
	
		
			
				|  |  | +            state, self.retried = RETRY, timestamp
 | 
	
		
			
				|  |  | +        elif type_ == 'succeeded':
 | 
	
		
			
				|  |  | +            state, self.succeeded = SUCCESS, timestamp
 | 
	
		
			
				|  |  | +        elif type_ == 'revoked':
 | 
	
		
			
				|  |  | +            state, self.revoked = REVOKED, timestamp
 | 
	
		
			
				|  |  | +        else:
 | 
	
		
			
				|  |  | +            state = type_.upper()
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -        """
 | 
	
		
			
				|  |  | -        time_received = fields.get('local_received') or 0
 | 
	
		
			
				|  |  | -        if self.worker and time_received:
 | 
	
		
			
				|  |  | -            self.worker.update_heartbeat(time_received, timestamp)
 | 
	
		
			
				|  |  | -        if state != states.RETRY and self.state != states.RETRY and \
 | 
	
		
			
				|  |  | -                _state(state) < _state(self.state):
 | 
	
		
			
				|  |  | +        # note that precedence here is reversed
 | 
	
		
			
				|  |  | +        # see implementation in celery.states.state.__lt__
 | 
	
		
			
				|  |  | +        if state != RETRY and self.state != RETRY and \
 | 
	
		
			
				|  |  | +                precedence(state) > precedence(self.state):
 | 
	
		
			
				|  |  |              # this state logically happens-before the current state, so merge.
 | 
	
		
			
				|  |  | -            self.merge(state, timestamp, fields)
 | 
	
		
			
				|  |  | +            keep = self.merge_rules.get(state)
 | 
	
		
			
				|  |  | +            if keep is not None:
 | 
	
		
			
				|  |  | +                fields = dict(
 | 
	
		
			
				|  |  | +                    (k, v) for k, v in items(fields) if k in keep
 | 
	
		
			
				|  |  | +                )
 | 
	
		
			
				|  |  | +            for key, value in items(fields):
 | 
	
		
			
				|  |  | +                setattr(self, key, value)
 | 
	
		
			
				|  |  |          else:
 | 
	
		
			
				|  |  |              self.state = state
 | 
	
		
			
				|  |  |              self.timestamp = timestamp
 | 
	
		
			
				|  |  | -            super(Task, self).update(fields)
 | 
	
		
			
				|  |  | +            for key, value in items(fields):
 | 
	
		
			
				|  |  | +                setattr(self, key, value)
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -    def merge(self, state, timestamp, fields):
 | 
	
		
			
				|  |  | -        """Merge with out of order event."""
 | 
	
		
			
				|  |  | -        keep = self.merge_rules.get(state)
 | 
	
		
			
				|  |  | -        if keep is not None:
 | 
	
		
			
				|  |  | -            fields = dict((key, fields.get(key)) for key in keep)
 | 
	
		
			
				|  |  | -            super(Task, self).update(fields)
 | 
	
		
			
				|  |  | +    def info(self, fields=None, extra=[]):
 | 
	
		
			
				|  |  | +        """Information about this task suitable for on-screen display."""
 | 
	
		
			
				|  |  | +        fields = self._info_fields if fields is None else fields
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        def _keys():
 | 
	
		
			
				|  |  | +            for key in list(fields) + list(extra):
 | 
	
		
			
				|  |  | +                value = getattr(self, key, None)
 | 
	
		
			
				|  |  | +                if value is not None:
 | 
	
		
			
				|  |  | +                    yield key, value
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | +        return dict(_keys())
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    def __repr__(self):
 | 
	
		
			
				|  |  | +        return R_TASK.format(self)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    def as_dict(self):
 | 
	
		
			
				|  |  | +        get = object.__getattribute__
 | 
	
		
			
				|  |  | +        return dict(
 | 
	
		
			
				|  |  | +            (k, get(self, k)) for k in self._fields
 | 
	
		
			
				|  |  | +        )
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    def __reduce__(self):
 | 
	
		
			
				|  |  | +        return _depickle_task, (self.__class__, self.as_dict())
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    @property
 | 
	
		
			
				|  |  | +    def origin(self):
 | 
	
		
			
				|  |  | +        return self.client if self.worker is None else self.worker.id
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    @property
 | 
	
		
			
				|  |  | +    def ready(self):
 | 
	
		
			
				|  |  | +        return self.state in states.READY_STATES
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    @deprecated(3.2, 3.3)
 | 
	
		
			
				|  |  |      def on_sent(self, timestamp=None, **fields):
 | 
	
		
			
				|  |  | -        """Callback for the :event:`task-sent` event."""
 | 
	
		
			
				|  |  | -        self.sent = timestamp
 | 
	
		
			
				|  |  | -        self.update(states.PENDING, timestamp, fields)
 | 
	
		
			
				|  |  | +        self.event('sent', timestamp, fields)
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | +    @deprecated(3.2, 3.3)
 | 
	
		
			
				|  |  |      def on_received(self, timestamp=None, **fields):
 | 
	
		
			
				|  |  | -        """Callback for the :event:`task-received` event."""
 | 
	
		
			
				|  |  | -        self.received = timestamp
 | 
	
		
			
				|  |  | -        self.update(states.RECEIVED, timestamp, fields)
 | 
	
		
			
				|  |  | +        self.event('received', timestamp, fields)
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | +    @deprecated(3.2, 3.3)
 | 
	
		
			
				|  |  |      def on_started(self, timestamp=None, **fields):
 | 
	
		
			
				|  |  | -        """Callback for the :event:`task-started` event."""
 | 
	
		
			
				|  |  | -        self.started = timestamp
 | 
	
		
			
				|  |  | -        self.update(states.STARTED, timestamp, fields)
 | 
	
		
			
				|  |  | +        self.event('started', timestamp, fields)
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | +    @deprecated(3.2, 3.3)
 | 
	
		
			
				|  |  |      def on_failed(self, timestamp=None, **fields):
 | 
	
		
			
				|  |  | -        """Callback for the :event:`task-failed` event."""
 | 
	
		
			
				|  |  | -        self.failed = timestamp
 | 
	
		
			
				|  |  | -        self.update(states.FAILURE, timestamp, fields)
 | 
	
		
			
				|  |  | +        self.event('failed', timestamp, fields)
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | +    @deprecated(3.2, 3.3)
 | 
	
		
			
				|  |  |      def on_retried(self, timestamp=None, **fields):
 | 
	
		
			
				|  |  | -        """Callback for the :event:`task-retried` event."""
 | 
	
		
			
				|  |  | -        self.retried = timestamp
 | 
	
		
			
				|  |  | -        self.update(states.RETRY, timestamp, fields)
 | 
	
		
			
				|  |  | +        self.event('retried', timestamp, fields)
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | +    @deprecated(3.2, 3.3)
 | 
	
		
			
				|  |  |      def on_succeeded(self, timestamp=None, **fields):
 | 
	
		
			
				|  |  | -        """Callback for the :event:`task-succeeded` event."""
 | 
	
		
			
				|  |  | -        self.succeeded = timestamp
 | 
	
		
			
				|  |  | -        self.update(states.SUCCESS, timestamp, fields)
 | 
	
		
			
				|  |  | +        self.event('succeeded', timestamp, fields)
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | +    @deprecated(3.2, 3.3)
 | 
	
		
			
				|  |  |      def on_revoked(self, timestamp=None, **fields):
 | 
	
		
			
				|  |  | -        """Callback for the :event:`task-revoked` event."""
 | 
	
		
			
				|  |  | -        self.revoked = timestamp
 | 
	
		
			
				|  |  | -        self.update(states.REVOKED, timestamp, fields)
 | 
	
		
			
				|  |  | +        self.event('revoked', timestamp, fields)
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | +    @deprecated(3.2, 3.3)
 | 
	
		
			
				|  |  |      def on_unknown_event(self, shortype, timestamp=None, **fields):
 | 
	
		
			
				|  |  | -        self.update(shortype.upper(), timestamp, fields)
 | 
	
		
			
				|  |  | +        self.event(shortype, timestamp, fields)
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -    def info(self, fields=None, extra=[]):
 | 
	
		
			
				|  |  | -        """Information about this task suitable for on-screen display."""
 | 
	
		
			
				|  |  | -        fields = self._info_fields if fields is None else fields
 | 
	
		
			
				|  |  | +    @deprecated(3.2, 3.3)
 | 
	
		
			
				|  |  | +    def update(self, state, timestamp, fields,
 | 
	
		
			
				|  |  | +               _state=states.state, RETRY=states.RETRY):
 | 
	
		
			
				|  |  | +        return self.event(state, timestamp, None, fields)
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -        def _keys():
 | 
	
		
			
				|  |  | -            for key in list(fields) + list(extra):
 | 
	
		
			
				|  |  | -                value = getattr(self, key, None)
 | 
	
		
			
				|  |  | -                if value is not None:
 | 
	
		
			
				|  |  | -                    yield key, value
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -        return dict(_keys())
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -    def __repr__(self):
 | 
	
		
			
				|  |  | -        return R_TASK.format(self)
 | 
	
		
			
				|  |  | +    @deprecated(3.2, 3.3)
 | 
	
		
			
				|  |  | +    def merge(self, state, timestamp, fields):
 | 
	
		
			
				|  |  | +        keep = self.merge_rules.get(state)
 | 
	
		
			
				|  |  | +        if keep is not None:
 | 
	
		
			
				|  |  | +            fields = dict((k, v) for k, v in items(fields) if k in keep)
 | 
	
		
			
				|  |  | +        for key, value in items(fields):
 | 
	
		
			
				|  |  | +            setattr(self, key, value)
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -    @property
 | 
	
		
			
				|  |  | -    def ready(self):
 | 
	
		
			
				|  |  | -        return self.state in states.READY_STATES
 | 
	
		
			
				|  |  | +    @class_property
 | 
	
		
			
				|  |  | +    def _defaults(cls):
 | 
	
		
			
				|  |  | +        """Deprecated, to be removed in 3.3."""
 | 
	
		
			
				|  |  | +        source = cls()
 | 
	
		
			
				|  |  | +        return dict((k, getattr(source, k)) for k in source._fields)
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  class State(object):
 | 
	
		
			
				|  |  |      """Records clusters state."""
 | 
	
		
			
				|  |  | +    Worker = Worker
 | 
	
		
			
				|  |  | +    Task = Task
 | 
	
		
			
				|  |  |      event_count = 0
 | 
	
		
			
				|  |  |      task_count = 0
 | 
	
		
			
				|  |  | +    heap_multiplier = 4
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |      def __init__(self, callback=None,
 | 
	
		
			
				|  |  |                   workers=None, tasks=None, taskheap=None,
 | 
	
		
			
				|  |  | -                 max_workers_in_memory=5000, max_tasks_in_memory=10000):
 | 
	
		
			
				|  |  | +                 max_workers_in_memory=5000, max_tasks_in_memory=10000,
 | 
	
		
			
				|  |  | +                 on_node_join=None, on_node_leave=None):
 | 
	
		
			
				|  |  |          self.event_callback = callback
 | 
	
		
			
				|  |  |          self.workers = (LRUCache(max_workers_in_memory)
 | 
	
		
			
				|  |  |                          if workers is None else workers)
 | 
	
	
		
			
				|  | @@ -284,10 +398,16 @@ class State(object):
 | 
	
		
			
				|  |  |          self._taskheap = [] if taskheap is None else taskheap
 | 
	
		
			
				|  |  |          self.max_workers_in_memory = max_workers_in_memory
 | 
	
		
			
				|  |  |          self.max_tasks_in_memory = max_tasks_in_memory
 | 
	
		
			
				|  |  | +        self.on_node_join = on_node_join
 | 
	
		
			
				|  |  | +        self.on_node_leave = on_node_leave
 | 
	
		
			
				|  |  |          self._mutex = threading.Lock()
 | 
	
		
			
				|  |  | -        self.handlers = {'task': self.task_event,
 | 
	
		
			
				|  |  | -                         'worker': self.worker_event}
 | 
	
		
			
				|  |  | -        self._get_handler = self.handlers.__getitem__
 | 
	
		
			
				|  |  | +        self.handlers = {}
 | 
	
		
			
				|  |  | +        self._seen_types = set()
 | 
	
		
			
				|  |  | +        self.rebuild_taskheap()
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    @cached_property
 | 
	
		
			
				|  |  | +    def _event(self):
 | 
	
		
			
				|  |  | +        return self._create_dispatcher()
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |      def freeze_while(self, fun, *args, **kwargs):
 | 
	
		
			
				|  |  |          clear_after = kwargs.pop('clear_after', False)
 | 
	
	
		
			
				|  | @@ -330,11 +450,12 @@ class State(object):
 | 
	
		
			
				|  |  |          """
 | 
	
		
			
				|  |  |          try:
 | 
	
		
			
				|  |  |              worker = self.workers[hostname]
 | 
	
		
			
				|  |  | -            worker.update(kwargs)
 | 
	
		
			
				|  |  | +            if kwargs:
 | 
	
		
			
				|  |  | +                worker.update(kwargs)
 | 
	
		
			
				|  |  |              return worker, False
 | 
	
		
			
				|  |  |          except KeyError:
 | 
	
		
			
				|  |  | -            worker = self.workers[hostname] = Worker(
 | 
	
		
			
				|  |  | -                hostname=hostname, **kwargs)
 | 
	
		
			
				|  |  | +            worker = self.workers[hostname] = self.Worker(
 | 
	
		
			
				|  |  | +                hostname, **kwargs)
 | 
	
		
			
				|  |  |              return worker, True
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |      def get_or_create_task(self, uuid):
 | 
	
	
		
			
				|  | @@ -342,61 +463,112 @@ class State(object):
 | 
	
		
			
				|  |  |          try:
 | 
	
		
			
				|  |  |              return self.tasks[uuid], False
 | 
	
		
			
				|  |  |          except KeyError:
 | 
	
		
			
				|  |  | -            task = self.tasks[uuid] = Task(uuid=uuid)
 | 
	
		
			
				|  |  | +            task = self.tasks[uuid] = self.Task(uuid)
 | 
	
		
			
				|  |  |              return task, True
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -    def worker_event(self, type, fields):
 | 
	
		
			
				|  |  | -        """Process worker event."""
 | 
	
		
			
				|  |  | -        try:
 | 
	
		
			
				|  |  | -            hostname = fields['hostname']
 | 
	
		
			
				|  |  | -        except KeyError:
 | 
	
		
			
				|  |  | -            pass
 | 
	
		
			
				|  |  | -        else:
 | 
	
		
			
				|  |  | -            worker, created = self.get_or_create_worker(hostname)
 | 
	
		
			
				|  |  | -            handler = getattr(worker, 'on_' + type, None)
 | 
	
		
			
				|  |  | -            if handler:
 | 
	
		
			
				|  |  | -                handler(**(fields if CAN_KWDICT else kwdict(fields)))
 | 
	
		
			
				|  |  | -            return worker, created
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -    def task_event(self, type, fields, timetuple=timetuple):
 | 
	
		
			
				|  |  | -        """Process task event."""
 | 
	
		
			
				|  |  | -        uuid = fields['uuid']
 | 
	
		
			
				|  |  | -        hostname = fields['hostname']
 | 
	
		
			
				|  |  | -        worker, _ = self.get_or_create_worker(hostname)
 | 
	
		
			
				|  |  | -        task, created = self.get_or_create_task(uuid)
 | 
	
		
			
				|  |  | -        task.worker = worker
 | 
	
		
			
				|  |  | -        maxtasks = self.max_tasks_in_memory * 2
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -        taskheap = self._taskheap
 | 
	
		
			
				|  |  | -        timestamp = fields.get('timestamp') or 0
 | 
	
		
			
				|  |  | -        clock = 0 if type == 'sent' else fields.get('clock')
 | 
	
		
			
				|  |  | -        heappush(taskheap, timetuple(clock, timestamp, worker.id, task))
 | 
	
		
			
				|  |  | -        if len(taskheap) > maxtasks:
 | 
	
		
			
				|  |  | -            heappop(taskheap)
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -        handler = getattr(task, 'on_' + type, None)
 | 
	
		
			
				|  |  | -        if type == 'received':
 | 
	
		
			
				|  |  | -            self.task_count += 1
 | 
	
		
			
				|  |  | -        if handler:
 | 
	
		
			
				|  |  | -            handler(**fields)
 | 
	
		
			
				|  |  | -        else:
 | 
	
		
			
				|  |  | -            task.on_unknown_event(type, **fields)
 | 
	
		
			
				|  |  | -        return created
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  |      def event(self, event):
 | 
	
		
			
				|  |  |          with self._mutex:
 | 
	
		
			
				|  |  | -            return self._dispatch_event(event)
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -    def _dispatch_event(self, event, kwdict=kwdict):
 | 
	
		
			
				|  |  | -        self.event_count += 1
 | 
	
		
			
				|  |  | -        event = kwdict(event)
 | 
	
		
			
				|  |  | -        group, _, subject = event['type'].partition('-')
 | 
	
		
			
				|  |  | -        try:
 | 
	
		
			
				|  |  | -            self._get_handler(group)(subject, event)
 | 
	
		
			
				|  |  | -        except KeyError:
 | 
	
		
			
				|  |  | -            pass
 | 
	
		
			
				|  |  | -        if self.event_callback:
 | 
	
		
			
				|  |  | -            self.event_callback(self, event)
 | 
	
		
			
				|  |  | +            return self._event(event)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    def task_event(self, type_, fields):
 | 
	
		
			
				|  |  | +        """Deprecated, use :meth:`event`."""
 | 
	
		
			
				|  |  | +        return self._event(dict(fields, type='-'.join(['task', type_])))[0]
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    def worker_event(self, type_, fields):
 | 
	
		
			
				|  |  | +        """Deprecated, use :meth:`event`."""
 | 
	
		
			
				|  |  | +        return self._event(dict(fields, type='-'.join(['worker', type_])))[0]
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    def _create_dispatcher(self):
 | 
	
		
			
				|  |  | +        get_handler = self.handlers.__getitem__
 | 
	
		
			
				|  |  | +        event_callback = self.event_callback
 | 
	
		
			
				|  |  | +        wfields = itemgetter('hostname', 'timestamp', 'local_received')
 | 
	
		
			
				|  |  | +        tfields = itemgetter('uuid', 'hostname', 'timestamp',
 | 
	
		
			
				|  |  | +                             'local_received', 'clock')
 | 
	
		
			
				|  |  | +        taskheap = self._taskheap
 | 
	
		
			
				|  |  | +        # Removing events from task heap is an O(n) operation,
 | 
	
		
			
				|  |  | +        # so easier to just account for the common number of events
 | 
	
		
			
				|  |  | +        # for each task (PENDING->RECEIVED->STARTED->final)
 | 
	
		
			
				|  |  | +        #: an O(n) operation
 | 
	
		
			
				|  |  | +        max_events_in_heap = self.max_tasks_in_memory * self.heap_multiplier
 | 
	
		
			
				|  |  | +        add_type = self._seen_types.add
 | 
	
		
			
				|  |  | +        on_node_join, on_node_leave = self.on_node_join, self.on_node_leave
 | 
	
		
			
				|  |  | +        tasks, Task = self.tasks, self.Task
 | 
	
		
			
				|  |  | +        workers, Worker = self.workers, self.Worker
 | 
	
		
			
				|  |  | +        # avoid updating LRU entry at getitem
 | 
	
		
			
				|  |  | +        get_worker, get_task = workers.data.__getitem__, tasks.data.__getitem__
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        def _event(event,
 | 
	
		
			
				|  |  | +                   timetuple=timetuple, KeyError=KeyError, created=True):
 | 
	
		
			
				|  |  | +            self.event_count += 1
 | 
	
		
			
				|  |  | +            if event_callback:
 | 
	
		
			
				|  |  | +                event_callback(self, event)
 | 
	
		
			
				|  |  | +            group, _, subject = event['type'].partition('-')
 | 
	
		
			
				|  |  | +            try:
 | 
	
		
			
				|  |  | +                handler = get_handler(group)
 | 
	
		
			
				|  |  | +            except KeyError:
 | 
	
		
			
				|  |  | +                pass
 | 
	
		
			
				|  |  | +            else:
 | 
	
		
			
				|  |  | +                return handler(subject, event), subject
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +            if group == 'worker':
 | 
	
		
			
				|  |  | +                try:
 | 
	
		
			
				|  |  | +                    hostname, timestamp, local_received = wfields(event)
 | 
	
		
			
				|  |  | +                except KeyError:
 | 
	
		
			
				|  |  | +                    pass
 | 
	
		
			
				|  |  | +                else:
 | 
	
		
			
				|  |  | +                    try:
 | 
	
		
			
				|  |  | +                        worker, created = get_worker(hostname), False
 | 
	
		
			
				|  |  | +                    except KeyError:
 | 
	
		
			
				|  |  | +                        if subject == 'offline':
 | 
	
		
			
				|  |  | +                            worker, created = None, False
 | 
	
		
			
				|  |  | +                        else:
 | 
	
		
			
				|  |  | +                            worker = workers[hostname] = Worker(hostname)
 | 
	
		
			
				|  |  | +                    if worker:
 | 
	
		
			
				|  |  | +                        worker.event(subject, timestamp, local_received, event)
 | 
	
		
			
				|  |  | +                    if on_node_join and (created or subject == 'online'):
 | 
	
		
			
				|  |  | +                        on_node_join(worker)
 | 
	
		
			
				|  |  | +                    if on_node_leave and subject == 'offline':
 | 
	
		
			
				|  |  | +                        on_node_leave(worker)
 | 
	
		
			
				|  |  | +                    return (worker, created), subject
 | 
	
		
			
				|  |  | +            elif group == 'task':
 | 
	
		
			
				|  |  | +                (uuid, hostname, timestamp,
 | 
	
		
			
				|  |  | +                 local_received, clock) = tfields(event)
 | 
	
		
			
				|  |  | +                # task-sent event is sent by client, not worker
 | 
	
		
			
				|  |  | +                is_client_event = subject == 'sent'
 | 
	
		
			
				|  |  | +                try:
 | 
	
		
			
				|  |  | +                    task, created = get_task(uuid), False
 | 
	
		
			
				|  |  | +                except KeyError:
 | 
	
		
			
				|  |  | +                    task = tasks[uuid] = Task(uuid)
 | 
	
		
			
				|  |  | +                if is_client_event:
 | 
	
		
			
				|  |  | +                    task.client = hostname
 | 
	
		
			
				|  |  | +                else:
 | 
	
		
			
				|  |  | +                    try:
 | 
	
		
			
				|  |  | +                        worker, created = get_worker(hostname), False
 | 
	
		
			
				|  |  | +                    except KeyError:
 | 
	
		
			
				|  |  | +                        worker = workers[hostname] = Worker(hostname)
 | 
	
		
			
				|  |  | +                    task.worker = worker
 | 
	
		
			
				|  |  | +                    if worker is not None and local_received:
 | 
	
		
			
				|  |  | +                        worker.event(None, local_received, timestamp)
 | 
	
		
			
				|  |  | +                origin = hostname if is_client_event else worker.id
 | 
	
		
			
				|  |  | +                heappush(taskheap,
 | 
	
		
			
				|  |  | +                         timetuple(clock, timestamp, origin, ref(task)))
 | 
	
		
			
				|  |  | +                if len(taskheap) > max_events_in_heap:
 | 
	
		
			
				|  |  | +                    heappop(taskheap)
 | 
	
		
			
				|  |  | +                if subject == 'received':
 | 
	
		
			
				|  |  | +                    self.task_count += 1
 | 
	
		
			
				|  |  | +                task.event(subject, timestamp, local_received, event)
 | 
	
		
			
				|  |  | +                task_name = task.name
 | 
	
		
			
				|  |  | +                if task_name is not None:
 | 
	
		
			
				|  |  | +                    add_type(task_name)
 | 
	
		
			
				|  |  | +                return (task, created), subject
 | 
	
		
			
				|  |  | +        return _event
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    def rebuild_taskheap(self, timetuple=timetuple, heapify=heapify):
 | 
	
		
			
				|  |  | +        heap = self._taskheap[:] = [
 | 
	
		
			
				|  |  | +            timetuple(t.clock, t.timestamp, t.origin, ref(t))
 | 
	
		
			
				|  |  | +            for t in values(self.tasks)
 | 
	
		
			
				|  |  | +        ]
 | 
	
		
			
				|  |  | +        heapify(heap)
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |      def itertasks(self, limit=None):
 | 
	
		
			
				|  |  |          for index, row in enumerate(items(self.tasks)):
 | 
	
	
		
			
				|  | @@ -409,10 +581,12 @@ class State(object):
 | 
	
		
			
				|  |  |          in ``(uuid, Task)`` tuples."""
 | 
	
		
			
				|  |  |          seen = set()
 | 
	
		
			
				|  |  |          for evtup in islice(reversed(self._taskheap), 0, limit):
 | 
	
		
			
				|  |  | -            uuid = evtup[3].uuid
 | 
	
		
			
				|  |  | -            if uuid not in seen:
 | 
	
		
			
				|  |  | -                yield uuid, evtup[3]
 | 
	
		
			
				|  |  | -                seen.add(uuid)
 | 
	
		
			
				|  |  | +            task = evtup[3]()
 | 
	
		
			
				|  |  | +            if task is not None:
 | 
	
		
			
				|  |  | +                uuid = task.uuid
 | 
	
		
			
				|  |  | +                if uuid not in seen:
 | 
	
		
			
				|  |  | +                    yield uuid, task
 | 
	
		
			
				|  |  | +                    seen.add(uuid)
 | 
	
		
			
				|  |  |      tasks_by_timestamp = tasks_by_time
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |      def tasks_by_type(self, name, limit=None):
 | 
	
	
		
			
				|  | @@ -439,8 +613,7 @@ class State(object):
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |      def task_types(self):
 | 
	
		
			
				|  |  |          """Return a list of all seen task types."""
 | 
	
		
			
				|  |  | -        return list(sorted(set(task.name for task in values(self.tasks)
 | 
	
		
			
				|  |  | -                               if task.name is not None)))
 | 
	
		
			
				|  |  | +        return sorted(self._seen_types)
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |      def alive_workers(self):
 | 
	
		
			
				|  |  |          """Return a list of (seemingly) alive workers."""
 | 
	
	
		
			
				|  | @@ -451,6 +624,7 @@ class State(object):
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |      def __reduce__(self):
 | 
	
		
			
				|  |  |          return self.__class__, (
 | 
	
		
			
				|  |  | -            self.event_callback, self.workers, self.tasks, self._taskheap,
 | 
	
		
			
				|  |  | +            self.event_callback, self.workers, self.tasks, None,
 | 
	
		
			
				|  |  |              self.max_workers_in_memory, self.max_tasks_in_memory,
 | 
	
		
			
				|  |  | +            self.on_node_join, self.on_node_leave,
 | 
	
		
			
				|  |  |          )
 |