|
@@ -11,7 +11,9 @@ from __future__ import absolute_import, print_function, unicode_literals
|
|
|
import sys
|
|
|
import time
|
|
|
|
|
|
-from collections import defaultdict, Mapping, MutableMapping, MutableSet
|
|
|
+from collections import (
|
|
|
+ Callable, Mapping, MutableMapping, MutableSet, defaultdict,
|
|
|
+)
|
|
|
from heapq import heapify, heappush, heappop
|
|
|
from itertools import chain
|
|
|
|
|
@@ -19,7 +21,7 @@ from billiard.einfo import ExceptionInfo # noqa
|
|
|
from kombu.utils.encoding import safe_str, bytes_to_str
|
|
|
from kombu.utils.limits import TokenBucket # noqa
|
|
|
|
|
|
-from celery.five import items
|
|
|
+from celery.five import items, values
|
|
|
from celery.utils.functional import LRUCache, first, uniq # noqa
|
|
|
from celery.utils.text import match_case
|
|
|
|
|
@@ -30,6 +32,10 @@ except ImportError:
|
|
|
pass
|
|
|
LazySettings = LazyObject # noqa
|
|
|
|
|
|
+__all__ = ['GraphFormatter', 'CycleError', 'DependencyGraph',
|
|
|
+ 'AttributeDictMixin', 'AttributeDict', 'DictAttribute',
|
|
|
+ 'ConfigurationView', 'LimitedSet']
|
|
|
+
|
|
|
DOT_HEAD = """
|
|
|
{IN}{type} {id} {{
|
|
|
{INp}graph [{attrs}]
|
|
@@ -41,9 +47,11 @@ DOT_ATTRSEP = ', '
|
|
|
DOT_DIRS = {'graph': '--', 'digraph': '->'}
|
|
|
DOT_TAIL = '{IN}}}'
|
|
|
|
|
|
-__all__ = ['GraphFormatter', 'CycleError', 'DependencyGraph',
|
|
|
- 'AttributeDictMixin', 'AttributeDict', 'DictAttribute',
|
|
|
- 'ConfigurationView', 'LimitedSet']
|
|
|
+REPR_LIMITED_SET = """\
|
|
|
+<{name}({size}): maxlen={0.maxlen}, expires={0.expires}, minlen={0.minlen}>\
|
|
|
+"""
|
|
|
+
|
|
|
+sentinel = object()
|
|
|
|
|
|
|
|
|
def force_mapping(m):
|
|
@@ -578,7 +586,6 @@ class ConfigurationView(AttributeDictMixin):
|
|
|
|
|
|
def values(self):
|
|
|
return list(self._iterate_values())
|
|
|
-
|
|
|
MutableMapping.register(ConfigurationView)
|
|
|
|
|
|
|
|
@@ -588,10 +595,34 @@ class LimitedSet(object):
|
|
|
Good for when you need to test for membership (`a in set`),
|
|
|
but the set should not grow unbounded.
|
|
|
|
|
|
- This version is now changed to be more enforcing those limits.
|
|
|
- Maxlen is enforced all the time. But you can also configure
|
|
|
- minlen now, which is minimal residual size of set.
|
|
|
+ Maxlen is enforced at all times, so if the limit is reached
|
|
|
+ we will also remove non-expired items.
|
|
|
+
|
|
|
+ You can also configure minlen, which is the minimal residual size
|
|
|
+ of the set.
|
|
|
+
|
|
|
+ All arguments are optional, with exception of minlen, which must
|
|
|
+ be smaller than maxlen. Unconfigured limits will not be enforced.
|
|
|
+
|
|
|
+ :keyword maxlen: Optional max number of items.
|
|
|
+
|
|
|
+ Adding more items than maxlen will result in immediate
|
|
|
+ removal of items sorted by oldest insertion time.
|
|
|
+
|
|
|
+ :keyword expires: TTL for all items.
|
|
|
+
|
|
|
+ Items aging over expiration are purged as keys are inserted.
|
|
|
|
|
|
+ :keyword minlen: Minimal residual size of this set.
|
|
|
+ .. versionadded:: 4.0
|
|
|
+
|
|
|
+ Older expired items will be deleted, only after the set
|
|
|
+ exceeds minlen number of items.
|
|
|
+
|
|
|
+ :keyword data: Initial data to initialize set with.
|
|
|
+ Can be an iterable of ``(key, value)`` pairs,
|
|
|
+ a dict (``{key: insertion_time}``), or another instance
|
|
|
+ of :class:`LimitedSet`.
|
|
|
|
|
|
Example::
|
|
|
|
|
@@ -611,39 +642,18 @@ class LimitedSet(object):
|
|
|
4000
|
|
|
>>>> 57000 in s # even this item is gone now
|
|
|
False
|
|
|
- """
|
|
|
-
|
|
|
- REMOVED = object() # just a placeholder for removed items
|
|
|
- _MAX_HEAP_PERCENTS_OVERLOAD = 15 #
|
|
|
|
|
|
- def __init__(self, maxlen=0, expires=0, minlen=0, data=None):
|
|
|
- """Initialize LimitedSet.
|
|
|
-
|
|
|
- All arguments are optional, with exception of minlen, which must
|
|
|
- be smaller than maxlen. Unconfigured limits will not be enforced.
|
|
|
+ """
|
|
|
|
|
|
- :keyword maxlen: max size of this set. Adding more items than maxlen
|
|
|
- results in immediate removing of older items.
|
|
|
- :keyword expires: TTL for an item.
|
|
|
- Items aging over expiration are purged.
|
|
|
- :keyword minlen: minimal residual size of this set.
|
|
|
- Oldest expired items will be delete
|
|
|
- only until minlen size is reached.
|
|
|
- :keyword data: data to initialize set with. Can be iterable of keys,
|
|
|
- dict {key:inserted_time} or another LimitedSet.
|
|
|
+ max_heap_percent_overload = 15
|
|
|
|
|
|
- """
|
|
|
- if maxlen is None:
|
|
|
- maxlen = 0
|
|
|
- if minlen is None:
|
|
|
- minlen = 0
|
|
|
- if expires is None:
|
|
|
- expires = 0
|
|
|
- self.maxlen = maxlen
|
|
|
- self.minlen = minlen
|
|
|
- self.expires = expires
|
|
|
+ def __init__(self, maxlen=0, expires=0, data=None, minlen=0):
|
|
|
+ self.maxlen = 0 if maxlen is None else maxlen
|
|
|
+ self.minlen = 0 if minlen is None else minlen
|
|
|
+ self.expires = 0 if expires is None else expires
|
|
|
self._data = {}
|
|
|
self._heap = []
|
|
|
+
|
|
|
# make shortcuts
|
|
|
self.__len__ = self._data.__len__
|
|
|
self.__contains__ = self._data.__contains__
|
|
@@ -653,14 +663,17 @@ class LimitedSet(object):
|
|
|
self.update(data)
|
|
|
|
|
|
if not self.maxlen >= self.minlen >= 0:
|
|
|
- raise ValueError('Minlen should be positive number, '
|
|
|
- 'smaller or equal to maxlen.')
|
|
|
+ raise ValueError(
|
|
|
+ 'minlen must be a positive number, less or equal to maxlen.')
|
|
|
if self.expires < 0:
|
|
|
- raise ValueError('Expires should not be negative!')
|
|
|
+ raise ValueError('expires cannot be negative!')
|
|
|
|
|
|
def _refresh_heap(self):
|
|
|
"""Time consuming recreating of heap. Do not run this too often."""
|
|
|
- self._heap[:] = [entry for entry in self._data.values()]
|
|
|
+ self._heap[:] = [
|
|
|
+ entry for entry in values(self._data)
|
|
|
+ if entry is not sentinel
|
|
|
+ ]
|
|
|
heapify(self._heap)
|
|
|
|
|
|
def clear(self):
|
|
@@ -669,12 +682,11 @@ class LimitedSet(object):
|
|
|
self._heap[:] = []
|
|
|
|
|
|
def add(self, item, now=None):
|
|
|
- 'Add a new item or update the time of an existing item'
|
|
|
- if not now:
|
|
|
- now = time.time()
|
|
|
+ """Add a new item, or reset the expiry time of an existing item."""
|
|
|
+ now = now or time.time()
|
|
|
if item in self._data:
|
|
|
self.discard(item)
|
|
|
- entry = [now, item]
|
|
|
+ entry = (now, item)
|
|
|
self._data[item] = entry
|
|
|
heappush(self._heap, entry)
|
|
|
if self.maxlen and len(self._data) >= self.maxlen:
|
|
@@ -687,43 +699,41 @@ class LimitedSet(object):
|
|
|
self._refresh_heap()
|
|
|
self.purge()
|
|
|
elif isinstance(other, dict):
|
|
|
- # revokes are sent like dict!
|
|
|
- for key, inserted in other.items():
|
|
|
+ # revokes are sent as a dict
|
|
|
+ for key, inserted in items(other):
|
|
|
if isinstance(inserted, list):
|
|
|
# in case someone uses ._data directly for sending update
|
|
|
inserted = inserted[0]
|
|
|
if not isinstance(inserted, float):
|
|
|
- raise ValueError('Expecting float timestamp, got type '
|
|
|
- '"{0}" with value: {1}'.format(
|
|
|
- type(inserted), inserted))
|
|
|
+ raise ValueError(
|
|
|
+ 'Expecting float timestamp, got type '
|
|
|
+ '{0!r} with value: {1}'.format(
|
|
|
+ type(inserted), inserted))
|
|
|
self.add(key, inserted)
|
|
|
else:
|
|
|
- # AVOID THIS, it could keep old data if more parties
|
|
|
+ # XXX AVOID THIS, it could keep old data if more parties
|
|
|
# exchange them all over and over again
|
|
|
for obj in other:
|
|
|
self.add(obj)
|
|
|
|
|
|
def discard(self, item):
|
|
|
- 'Mark an existing item as REMOVED. If KeyError is not found, pass.'
|
|
|
- entry = self._data.pop(item, self.REMOVED)
|
|
|
- if entry is self.REMOVED:
|
|
|
- return
|
|
|
- entry[-1] = self.REMOVED
|
|
|
- if self._heap_overload > self._MAX_HEAP_PERCENTS_OVERLOAD:
|
|
|
- self._refresh_heap()
|
|
|
-
|
|
|
+ # mark an existing item as removed. If KeyError is not found, pass.
|
|
|
+ entry = self._data.pop(item, sentinel)
|
|
|
+ if entry is not sentinel:
|
|
|
+ entry[-1] = sentinel
|
|
|
+ if self._heap_overload > self.max_heap_percent_overload:
|
|
|
+ self._refresh_heap()
|
|
|
pop_value = discard
|
|
|
|
|
|
def purge(self, now=None):
|
|
|
"""Check oldest items and remove them if needed.
|
|
|
|
|
|
:keyword now: Time of purging -- by default right now.
|
|
|
- This can be usefull for unittesting.
|
|
|
+ This can be useful for unit testing.
|
|
|
+
|
|
|
"""
|
|
|
- if not now:
|
|
|
- now = time.time()
|
|
|
- if hasattr(now, '__call__'):
|
|
|
- now = now() # if we got this now as function, evaluate it
|
|
|
+ now = now or time.time()
|
|
|
+ now = now() if isinstance(now, Callable) else now
|
|
|
if self.maxlen:
|
|
|
while len(self._data) > self.maxlen:
|
|
|
self.pop()
|
|
@@ -732,32 +742,33 @@ class LimitedSet(object):
|
|
|
while len(self._data) > self.minlen >= 0:
|
|
|
inserted_time, _ = self._heap[0]
|
|
|
if inserted_time + self.expires > now:
|
|
|
- break # end this right now, oldest item is not expired yet
|
|
|
+ break # oldest item has not expired yet
|
|
|
self.pop()
|
|
|
|
|
|
- def pop(self):
|
|
|
- 'Remove and return the lowest time item. Return None if empty.'
|
|
|
+ def pop(self, default=None):
|
|
|
+ """Remove and return the oldest item, or :const:`None` when empty."""
|
|
|
while self._heap:
|
|
|
_, item = heappop(self._heap)
|
|
|
- if item is not self.REMOVED:
|
|
|
- del self._data[item]
|
|
|
+ if self._data.pop(item, None) is not sentinel:
|
|
|
return item
|
|
|
- return None
|
|
|
+ return default
|
|
|
|
|
|
def as_dict(self):
|
|
|
"""Whole set as serializable dictionary.
|
|
|
+
|
|
|
Example::
|
|
|
|
|
|
- >>> s=LimitedSet(maxlen=200)
|
|
|
- >>> r=LimitedSet(maxlen=200)
|
|
|
+ >>> s = LimitedSet(maxlen=200)
|
|
|
+ >>> r = LimitedSet(maxlen=200)
|
|
|
>>> for i in range(500):
|
|
|
... s.add(i)
|
|
|
...
|
|
|
>>> r.update(s.as_dict())
|
|
|
>>> r == s
|
|
|
True
|
|
|
+
|
|
|
"""
|
|
|
- return {key: inserted for inserted, key in self._data.values()}
|
|
|
+ return {key: inserted for inserted, key in values(self._data)}
|
|
|
|
|
|
def __eq__(self, other):
|
|
|
return self._data == other._data
|
|
@@ -766,15 +777,12 @@ class LimitedSet(object):
|
|
|
return not self.__eq__(other)
|
|
|
|
|
|
def __repr__(self):
|
|
|
- return 'LimitedSet(maxlen={0}, expires={1}, minlen={2})' \
|
|
|
- ' Current size:{3}'.format(
|
|
|
- self.maxlen, self.expires, self.minlen, len(self._data))
|
|
|
+ return REPR_LIMITED_SET.format(
|
|
|
+ self, name=type(self).__name__, size=len(self),
|
|
|
+ )
|
|
|
|
|
|
def __iter__(self):
|
|
|
- # return (item[1] for item in
|
|
|
- # self._heap if item[-1] is not self.REMOVED)
|
|
|
- # ^ not ordered, slow
|
|
|
- return (i for _, i in sorted(self._data.values()))
|
|
|
+ return (i for _, i in sorted(values(self._data)))
|
|
|
|
|
|
def __len__(self):
|
|
|
return len(self._data)
|
|
@@ -783,17 +791,13 @@ class LimitedSet(object):
|
|
|
return key in self._data
|
|
|
|
|
|
def __reduce__(self):
|
|
|
- """Pickle helper class.
|
|
|
-
|
|
|
- This object can be pickled and upickled."""
|
|
|
return self.__class__, (
|
|
|
- self.maxlen, self.expires, self.minlen, self.as_dict())
|
|
|
+ self.maxlen, self.expires, self.as_dict(), self.minlen)
|
|
|
|
|
|
@property
|
|
|
def _heap_overload(self):
|
|
|
"""Compute how much is heap bigger than data [percents]."""
|
|
|
- if len(self._data) == 0:
|
|
|
+ if not self._data:
|
|
|
return len(self._heap)
|
|
|
- return len(self._heap)*100/len(self._data) - 100
|
|
|
-
|
|
|
+ return len(self._heap) * 100 / len(self._data) - 100
|
|
|
MutableSet.register(LimitedSet)
|