Package pyxmpp :: Module cache

[hide private]

# Source Code for Module pyxmpp.cache

      1  #
      2  # (C) Copyright 2005-2006 Jacek Konieczny <jajcus@jajcus.net>
      3  #
      4  # This program is free software; you can redistribute it and/or modify
      5  # it under the terms of the GNU Lesser General Public License Version
      6  # 2.1 as published by the Free Software Foundation.
      7  #
      8  # This program is distributed in the hope that it will be useful,
      9  # but WITHOUT ANY WARRANTY; without even the implied warranty of
     10  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     11  # GNU Lesser General Public License for more details.
     12  #
     13  # You should have received a copy of the GNU Lesser General Public
     14  # License along with this program; if not, write to the Free Software
     15  # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
     16  #
     17
     18  """Caching proxy for Jabber/XMPP objects.
     19
     20  This package provides facilities to retrieve and transparently cache
     21  cachable objects like Service Discovery responses or e.g. client version
     22  informations."""
     23
     24  __revision__ = "$Id: cache.py 647 2006-08-26 18:27:39Z jajcus $"
     25  __docformat__ = "restructuredtext en"
     26
     27  import threading
     28  from datetime import datetime, timedelta
     29
     30  _state_values = {
     31          'new': 0,
     32          'fresh': 1,
     33          'old': 2,
     34          'stale': 3,
     35          'purged': 4
     36      };
     37
     38  # locking order (anti-deadlock):
     39  # CacheSuite, Cache, CacheHandler, CacheItem
     40
     41 -class CacheItem(object):
     42      """An item in a cache.
     43
     44      :Ivariables:
     45          - `value`: item value (cached object).
     46          - `address`: item address.
     47          - `state`: current state.
     48          - `state_value`: numerical value of the current state (lower number means
     49            fresher item).
     50          - `timestamp`: time when the object was created.
     51          - `freshness_time`: time when the object stops being fresh.
     52          - `expire_time`: time when the object expires.
     53          - `purge_time`: time when the object should be purged. When 0 then
     54            item will never be automaticaly purged.
     55          - `_lock`: lock for thread safety.
     56      :Types:
     57          - `value`: `instance`
     58          - `address`: any hashable
     59          - `state`: `str`
     60          - `state_value`: `int`
     61          - `timestamp`: `datetime`
     62          - `freshness_time`: `datetime`
     63          - `expire_time`: `datetime`
     64          - `purge_time`: `datetime`
     65          - `_lock`: `threading.RLock`"""
     66      __slots__ = ['value', 'address', 'state', 'timestamp', 'freshness_time',
     67              'expire_time', 'purge_time', 'state_value', '_lock']
     68 -    def __init__(self, address, value, freshness_period, expiration_period,
     69              purge_period, state = "new"):
     70          """Initialize an CacheItem object.
     71
     72          :Parameters:
     73              - `address`: item address.
     74              - `value`: item value (cached object).
     75              - `freshness_period`: time interval after which the object stops being fresh.
     76              - `expiration_period`: time interval after which the object expires.
     77              - `purge_period`: time interval after which the object should be purged. When 0 then
     78                item will never be automaticaly purged.
     79              - `state`: initial state.
     80          :Types:
     81              - `address`: any hashable
     82              - `value`: `instance`
     83              - `freshness_period`: `timedelta`
     84              - `expiration_period`: `timedelta`
     85              - `purge_period`: `timedelta`
     86              - `state`: `str`"""
     87          if freshness_period>expiration_period:
     88              raise ValueError, "freshness_period greater then expiration_period"
     89          if expiration_period>purge_period:
     90              raise ValueError, "expiration_period greater then purge_period"
     91          self.address = address
     92          self.value = value
     93          now = datetime.utcnow()
     94          self.timestamp = now
     95          self.freshness_time = now+freshness_period
     96          self.expire_time = now+expiration_period
     97          if purge_period:
     98              self.purge_time = now+purge_period
     99          else:
    100              self.purge_time = datetime.max
    101          self.state = state
    102          self.state_value = _state_values[state]
    103          self._lock = threading.RLock()
    104
    105 -    def update_state(self):
    106          """Update current status of the item and compute time of the next
    107          state change.
    108
    109          :return: the new state.
    110          :returntype: `datetime`"""
    111          self._lock.acquire()
    112          try:
    113              now = datetime.utcnow()
    114              if self.state == 'new':
    115                  self.state = 'fresh'
    116              if self.state == 'fresh':
    117                  if now > self.freshness_time:
    118                      self.state = 'old'
    119              if self.state == 'old':
    120                  if now > self.expire_time:
    121                      self.state = 'stale'
    122              if self.state == 'stale':
    123                  if now > self.purge_time:
    124                      self.state = 'purged'
    125              self.state_value = _state_values[self.state]
    126              return self.state
    127          finally:
    128              self._lock.release()
    129
    130 -    def __cmp__(self,other):
    131          try:
    132              return cmp(
    133                      (-self.state_value, self.timestamp, id(self)),
    134                      (-other.state_value, other.timestamp, id(other))
    135                  )
    136          except AttributeError:
    137              return cmp(id(self),id(other))
    138
    139  _hour = timedelta(hours = 1)
    140
    141 -class CacheFetcher:
    142      """Base class for cache object fetchers -- classes responsible for
    143      retrieving objects from network.
    144
    145      An instance of a fetcher class is created for each object requested and
    146      not found in the cache, then `fetch` method is called to initialize
    147      the asynchronous retrieval process. Fetcher object's `got_it` method
    148      should be called on a successfull retrieval and `error` otherwise.
    149      `timeout` will be called when the request timeouts.
    150
    151      :Ivariables:
    152          - `cache`: cache object which created this fetcher.
    153          - `address`: requested item address.
    154          - `timeout_time`: timeout time.
    155          - `active`: `True` as long as the fetcher is active and requestor
    156            expects one of the handlers to be called.
    157      :Types:
    158          - `cache`: `Cache`
    159          - `address`: any hashable
    160          - `timeout_time`: `datetime`
    161          - `active`: `bool`
    162      """
    163 -    def __init__(self, cache, address,
    164              item_freshness_period, item_expiration_period, item_purge_period,
    165              object_handler, error_handler, timeout_handler, timeout_period,
    166              backup_state = None):
    167          """Initialize an `CacheFetcher` object.
    168
    169          :Parameters:
    170              - `cache`: cache object which created this fetcher.
    171              - `address`: requested item address.
    172              - `item_freshness_period`: freshness period for the requested item.
    173              - `item_expiration_period`: expiration period for the requested item.
    174              - `item_purge_period`: purge period for the requested item.
    175              - `object_handler`: function to be called after the item is fetched.
    176              - `error_handler`: function to be called on error.
    177              - `timeout_handler`: function to be called on timeout
    178              - `timeout_period`: timeout interval.
    179              - `backup_state`: when not `None` and the fetch fails than an
    180                object from cache of at least this state will be passed to the
    181                `object_handler`. If such object is not available, then
    182                `error_handler` is called.
    183          :Types:
    184              - `cache`: `Cache`
    185              - `address`: any hashable
    186              - `item_freshness_period`: `timedelta`
    187              - `item_expiration_period`: `timedelta`
    188              - `item_purge_period`: `timedelta`
    189              - `object_handler`: callable(address, value, state)
    190              - `error_handler`: callable(address, error_data)
    191              - `timeout_handler`: callable(address)
    192              - `timeout_period`: `timedelta`
    193              - `backup_state`: `bool`"""
    194          self.cache = cache
    195          self.address = address
    196          self._item_freshness_period = item_freshness_period
    197          self._item_expiration_period = item_expiration_period
    198          self._item_purge_period = item_purge_period
    199          self._object_handler = object_handler
    200          self._error_handler = error_handler
    201          self._timeout_handler = timeout_handler
    202          if timeout_period:
    203              self.timeout_time = datetime.utcnow()+timeout_period
    204          else:
    205              self.timeout_time = datetime.max
    206          self._backup_state = backup_state
    207          self.active = True
    208
    209 -    def _deactivate(self):
    210          """Remove the fetcher from cache and mark it not active."""
    211          self.cache.remove_fetcher(self)
    212          if self.active:
    213              self._deactivated()
    214
    215 -    def _deactivated(self):
    216          """Mark the fetcher inactive after it is removed from the cache."""
    217          self.active = False
    218
    219 -    def fetch(self):
    220          """Start the retrieval process.
    221
    222          This method must be implemented in any fetcher class."""
    223          raise RuntimeError, "Pure virtual method called"
    224
    225 -    def got_it(self, value, state = "new"):
    226          """Handle a successfull retrieval and call apriopriate handler.
    227
    228          Should be called when retrieval succeeds.
    229
    230          Do nothing when the fetcher is not active any more (after
    231          one of handlers was already called).
    232
    233          :Parameters:
    234              - `value`: fetched object.
    235              - `state`: initial state of the object.
    236          :Types:
    237              - `value`: any
    238              - `state`: `str`"""
    239          if not self.active:
    240              return
    241          item = CacheItem(self.address, value, self._item_freshness_period,
    242                  self._item_expiration_period, self._item_purge_period, state)
    243          self._object_handler(item.address, item.value, item.state)
    244          self.cache.add_item(item)
    245          self._deactivate()
    246
    247 -    def error(self, error_data):
    248          """Handle a retrieval error and call apriopriate handler.
    249
    250          Should be called when retrieval fails.
    251
    252          Do nothing when the fetcher is not active any more (after
    253          one of handlers was already called).
    254
    255          :Parameters:
    256              - `error_data`: additional information about the error (e.g. `StanzaError` instance).
    257          :Types:
    258              - `error_data`: fetcher dependant
    259          """
    260          if not self.active:
    261              return
    262          if not self._try_backup_item():
    263              self._error_handler(self.address, error_data)
    264          self.cache.invalidate_object(self.address)
    265          self._deactivate()
    266
    267 -    def timeout(self):
    268          """Handle fetcher timeout and call apriopriate handler.
    269
    270          Is called by the cache object and should _not_ be called by fetcher or
    271          application.
    272
    273          Do nothing when the fetcher is not active any more (after
    274          one of handlers was already called)."""
    275          if not self.active:
    276              return
    277          if not self._try_backup_item():
    278              if self._timeout_handler:
    279                  self._timeout_handler(self.address)
    280              else:
    281                  self._error_handler(self.address, None)
    282          self.cache.invalidate_object(self.address)
    283          self._deactivate()
    284
    285 -    def _try_backup_item(self):
    286          """Check if a backup item is available in cache and call
    287          the item handler if it is.
    288
    289          :return: `True` if backup item was found.
    290          :returntype: `bool`"""
    291          if not self._backup_state:
    292              return False
    293          item = self.cache.get_item(self.address, self._backup_state)
    294          if item:
    295              self._object_handler(item.address, item.value, item.state)
    296              return True
    297          else:
    298              False
    299
    300 -class Cache:
    301      """Caching proxy for object retrieval and caching.
    302
    303      Object factories ("fetchers") are registered in the `Cache` object and used
    304      to e.g. retrieve requested objects from network.  They are called only when
    305      the requested object is not in the cache or is not fresh enough.
    306
    307      A state (freshness level) name may be provided when requesting an object.
    308      When the cached item state is "less fresh" then requested, then new object
    309      will be retrieved.
    310
    311      Following states are defined:
    312
    313        - 'new': always a new object should be retrieved.
    314        - 'fresh': a fresh object (not older than freshness time)
    315        - 'old': object not fresh, but most probably still valid.
    316        - 'stale': object known to be expired.
    317
    318      :Ivariables:
    319          - `default_freshness_period`: default freshness period (in seconds).
    320          - `default_expiration_period`: default expiration period (in seconds).
    321          - `default_purge_period`: default purge period (in seconds). When
    322            0 then items are never purged because of their age.
    323          - `max_items`: maximum number of items to store.
    324          - `_items`: dictionary of stored items.
    325          - `_items_list`: list of stored items with the most suitable for
    326            purging first.
    327          - `_fetcher`: fetcher class for this cache.
    328          - `_active_fetchers`: list of active fetchers sorted by the time of
    329            its expiration time.
    330          - `_lock`: lock for thread safety.
    331      :Types:
    332          - `default_freshness_period`: timedelta
    333          - `default_expiration_period`: timedelta
    334          - `default_purge_period`: timedelta
    335          - `max_items`: `int`
    336          - `_items`: `dict` of (`classobj`, addr) -> `CacheItem`
    337          - `_items_list`: `list` of (`int`, `datetime`, `CacheItem`)
    338          - `_fetcher`: `CacheFetcher` based class
    339          - `_active_fetchers`: `list` of (`int`, `CacheFetcher`)
    340          - `_lock`: `threading.RLock`
    341      """
    342 -    def __init__(self, max_items, default_freshness_period = _hour,
    343              default_expiration_period = 12*_hour, default_purge_period = 24*_hour):
    344          """Initialize a `Cache` object.
    345
    346              :Parameters:
    347                  - `default_freshness_period`: default freshness period (in seconds).
    348                  - `default_expiration_period`: default expiration period (in seconds).
    349                  - `default_purge_period`: default purge period (in seconds). When
    350                    0 then items are never purged because of their age.
    351                  - `max_items`: maximum number of items to store.
    352              :Types:
    353                  - `default_freshness_period`: number
    354                  - `default_expiration_period`: number
    355                  - `default_purge_period`: number
    356                  - `max_items`: number
    357          """
    358          self.default_freshness_period = default_freshness_period
    359          self.default_expiration_period = default_expiration_period
    360          self.default_purge_period = default_purge_period
    361          self.max_items = max_items
    362          self._items = {}
    363          self._items_list = []
    364          self._fetcher = None
    365          self._active_fetchers = []
    366          self._purged = 0
    367          self._lock = threading.RLock()
    368
    369 -    def request_object(self, address, state, object_handler,
    370              error_handler = None, timeout_handler = None,
    371              backup_state = None, timeout = timedelta(minutes=60),
    372              freshness_period = None, expiration_period = None,
    373              purge_period = None):
    374          """Request an object with given address and state not worse than
    375          `state`. The object will be taken from cache if available, and
    376          created/fetched otherwise. The request is asynchronous -- this metod
    377          doesn't return the object directly, but the `object_handler` is called
    378          as soon as the object is available (this may be before `request_object`
    379          returns and may happen in other thread). On error the `error_handler`
    380          will be called, and on timeout -- the `timeout_handler`.
    381
    382          :Parameters:
    383              - `address`: address of the object requested.
    384              - `state`: the worst acceptable object state. When 'new' then always
    385                a new object will be created/fetched. 'stale' will select any
    386                item available in cache.
    387              - `object_handler`: function to be called when object is available.
    388                It will be called with the following arguments: address, object
    389                and its state.
    390              - `error_handler`: function to be called on object retrieval error.
    391                It will be called with two arguments: requested address and
    392                additional error information (fetcher-specific, may be
    393                StanzaError for XMPP objects).  If not given, then the object
    394                handler will be called with object set to `None` and state
    395                "error".
    396              - `timeout_handler`: function to be called on object retrieval
    397                timeout.  It will be called with only one argument: the requested
    398                address. If not given, then the `error_handler` will be called
    399                instead, with error details set to `None`.
    400              - `backup_state`: when set and object in state `state` is not
    401                available in the cache and object retrieval failed then object
    402                with this state will also be looked-up in the cache and provided
    403                if available.
    404              - `timeout`: time interval after which retrieval of the object
    405                should be given up.
    406              - `freshness_period`: time interval after which the item created
    407                should become 'old'.
    408              - `expiration_period`: time interval after which the item created
    409                should become 'stale'.
    410              - `purge_period`: time interval after which the item created
    411                shuld be removed from the cache.
    412          :Types:
    413              - `address`: any hashable
    414              - `state`: "new", "fresh", "old" or "stale"
    415              - `object_handler`: callable(address, value, state)
    416              - `error_handler`: callable(address, error_data)
    417              - `timeout_handler`: callable(address)
    418              - `backup_state`: "new", "fresh", "old" or "stale"
    419              - `timeout`: `timedelta`
    420              - `freshness_period`: `timedelta`
    421              - `expiration_period`: `timedelta`
    422              - `purge_period`: `timedelta`
    423          """
    424          self._lock.acquire()
    425          try:
    426              if state == 'stale':
    427                  state = 'purged'
    428              item = self.get_item(address, state)
    429              if item:
    430                  object_handler(item.address, item.value, item.state)
    431                  return
    432              if not self._fetcher:
    433                  raise TypeError, "No cache fetcher defined"
    434              if not error_handler:
    435                  def default_error_handler(address, _unused):
    436                      "Default error handler."
    437                      return object_handler(address, None, 'error')
    438                  error_handler = default_error_handler
    439              if not timeout_handler:
    440                  def default_timeout_handler(address):
    441                      "Default timeout handler."
    442                      return error_handler(address, None)
    443                  timeout_handler = default_timeout_handler
    444              if freshness_period is None:
    445                  freshness_period = self.default_freshness_period
    446              if expiration_period is None:
    447                  expiration_period = self.default_expiration_period
    448              if purge_period is None:
    449                  purge_period = self.default_purge_period
    450
    451              fetcher = self._fetcher(self, address, freshness_period,
    452                      expiration_period, purge_period, object_handler, error_handler,
    453                      timeout_handler, timeout, backup_state)
    454              fetcher.fetch()
    455              self._active_fetchers.append((fetcher.timeout_time,fetcher))
    456              self._active_fetchers.sort()
    457          finally:
    458              self._lock.release()
    459
    460 -    def invalidate_object(self, address, state = 'stale'):
    461          """Force cache item state change (to 'worse' state only).
    462
    463          :Parameters:
    464              - `state`: the new state requested.
    465          :Types:
    466              - `state`: `str`"""
    467          self._lock.acquire()
    468          try:
    469              item = self.get_item(address)
    470              if item and item.state_value<_state_values[state]:
    471                  item.state=state
    472                  item.update_state()
    473                  self._items_list.sort()
    474          finally:
    475              self._lock.release()
    476
    477 -    def add_item(self, item):
    478          """Add an item to the cache.
    479
    480          Item state is updated before adding it (it will not be 'new' any more).
    481
    482          :Parameters:
    483              - `item`: the item to add.
    484          :Types:
    485              - `item`: `CacheItem`
    486
    487          :return: state of the item after addition.
    488          :returntype: `str`
    489          """
    490          self._lock.acquire()
    491          try:
    492              state = item.update_state()
    493              if state != 'purged':
    494                  if len(self._items_list) >= self.max_items:
    495                      self.purge_items()
    496                  self._items[item.address] = item
    497                  self._items_list.append(item)
    498                  self._items_list.sort()
    499              return item.state
    500          finally:
    501              self._lock.release()
    502
    503 -    def get_item(self, address, state = 'fresh'):
    504          """Get an item from the cache.
    505
    506          :Parameters:
    507              - `address`: its address.
    508              - `state`: the worst state that is acceptable.
    509          :Types:
    510              - `address`: any hashable
    511              - `state`: `str`
    512
    513          :return: the item or `None` if it was not found.
    514          :returntype: `CacheItem`"""
    515          self._lock.acquire()
    516          try:
    517              item = self._items.get(address)
    518              if not item:
    519                  return None
    520              self.update_item(item)
    521              if _state_values[state] >= item.state_value:
    522                  return item
    523              return None
    524          finally:
    525              self._lock.release()
    526
    527 -    def update_item(self, item):
    528          """Update state of an item in the cache.
    529
    530          Update item's state and remove the item from the cache
    531          if its new state is 'purged'
    532
    533          :Parameters:
    534              - `item`: item to update.
    535          :Types:
    536              - `item`: `CacheItem`
    537
    538          :return: new state of the item.
    539          :returntype: `str`"""
    540
    541          self._lock.acquire()
    542          try:
    543              state = item.update_state()
    544              self._items_list.sort()
    545              if item.state == 'purged':
    546                  self._purged += 1
    547                  if self._purged > 0.25*self.max_items:
    548                      self.purge_items()
    549              return state
    550          finally:
    551              self._lock.release()
    552
    553 -    def num_items(self):
    554          """Get the number of items in the cache.
    555
    556          :return: number of items.
    557          :returntype: `int`"""
    558          return len(self._items_list)
    559
    560 -    def purge_items(self):
    561          """Remove purged and overlimit items from the cache.
    562
    563          TODO: optimize somehow.
    564
    565          Leave no more than 75% of `self.max_items` items in the cache."""
    566          self._lock.acquire()
    567          try:
    568              il=self._items_list
    569              num_items = len(il)
    570              need_remove = num_items - int(0.75 * self.max_items)
    571
    572              for _unused in range(need_remove):
    573                  item=il.pop(0)
    574                  try:
    575                      del self._items[item.address]
    576                  except KeyError:
    577                      pass
    578
    579              while il and il[0].update_state()=="purged":
    580                  item=il.pop(0)
    581                  try:
    582                      del self._items[item.address]
    583                  except KeyError:
    584                      pass
    585          finally:
    586              self._lock.release()
    587
    588 -    def tick(self):
    589          """Do the regular cache maintenance.
    590
    591          Must be called from time to time for timeouts and cache old items
    592          purging to work."""
    593          self._lock.acquire()
    594          try:
    595              now = datetime.utcnow()
    596              for t,f in list(self._active_fetchers):
    597                  if t > now:
    598                      break
    599                  f.timeout()
    600              self.purge_items()
    601          finally:
    602              self._lock.release()
    603
    604 -    def remove_fetcher(self, fetcher):
    605          """Remove a running fetcher from the list of active fetchers.
    606
    607          :Parameters:
    608              - `fetcher`: fetcher instance.
    609          :Types:
    610              - `fetcher`: `CacheFetcher`"""
    611          self._lock.acquire()
    612          try:
    613              for t, f in list(self._active_fetchers):
    614                  if f is fetcher:
    615                      self._active_fetchers.remove((t, f))
    616                      f._deactivated()
    617                      return
    618          finally:
    619              self._lock.release()
    620
    621 -    def set_fetcher(self, fetcher_class):
    622          """Set the fetcher class.
    623
    624          :Parameters:
    625              - `fetcher_class`: the fetcher class.
    626          :Types:
    627              - `fetcher_class`: `CacheFetcher` based class
    628          """
    629          self._lock.acquire()
    630          try:
    631              self._fetcher = fetcher_class
    632          finally:
    633              self._lock.release()
    634
    635 -class CacheSuite:
    636      """Caching proxy for object retrieval and caching.
    637
    638      Object factories for other classes are registered in the
    639      `Cache` object and used to e.g. retrieve requested objects from network.
    640      They are called only when the requested object is not in the cache
    641      or is not fresh enough.
    642
    643      Objects are addressed using their class and a class dependant address.
    644      Eg. `pyxmpp.jabber.disco.DiscoInfo` objects are addressed using
    645      (`pyxmpp.jabber.disco.DiscoInfo`,(jid, node)) tuple.
    646
    647      Additionaly a state (freshness level) name may be provided when requesting
    648      an object. When the cached item state is "less fresh" then requested, then
    649      new object will be retrieved.
    650
    651      Following states are defined:
    652
    653        - 'new': always a new object should be retrieved.
    654        - 'fresh': a fresh object (not older than freshness time)
    655        - 'old': object not fresh, but most probably still valid.
    656        - 'stale': object known to be expired.
    657
    658      :Ivariables:
    659          - `default_freshness_period`: default freshness period (in seconds).
    660          - `default_expiration_period`: default expiration period (in seconds).
    661          - `default_purge_period`: default purge period (in seconds). When
    662            0 then items are never purged because of their age.
    663          - `max_items`: maximum number of obejects of one class to store.
    664          - `_caches`: dictionary of per-class caches.
    665          - `_lock`: lock for thread safety.
    666      :Types:
    667          - `default_freshness_period`: timedelta
    668          - `default_expiration_period`: timedelta
    669          - `default_purge_period`: timedelta
    670          - `max_items`: `int`
    671          - `_caches`: `dict` of (`classobj`, addr) -> `Cache`
    672          - `_lock`: `threading.RLock`
    673      """
    674 -    def __init__(self, max_items, default_freshness_period = _hour,
    675              default_expiration_period = 12*_hour, default_purge_period = 24*_hour):
    676          """Initialize a `Cache` object.
    677
    678              :Parameters:
    679                  - `default_freshness_period`: default freshness period (in seconds).
    680                  - `default_expiration_period`: default expiration period (in seconds).
    681                  - `default_purge_period`: default purge period (in seconds). When
    682                    0 then items are never purged because of their age.
    683                  - `max_items`: maximum number of items to store.
    684              :Types:
    685                  - `default_freshness_period`: number
    686                  - `default_expiration_period`: number
    687                  - `default_purge_period`: number
    688                  - `max_items`: number
    689          """
    690          self.default_freshness_period = default_freshness_period
    691          self.default_expiration_period = default_expiration_period
    692          self.default_purge_period = default_purge_period
    693          self.max_items = max_items
    694          self._caches = {}
    695          self._lock = threading.RLock()
    696
    697 -    def request_object(self, object_class, address, state, object_handler,
    698              error_handler = None, timeout_handler = None,
    699              backup_state = None, timeout = None,
    700              freshness_period = None, expiration_period = None, purge_period = None):
    701          """Request an object of given class, with given address and state not
    702          worse than `state`. The object will be taken from cache if available,
    703          and created/fetched otherwise. The request is asynchronous -- this
    704          metod doesn't return the object directly, but the `object_handler` is
    705          called as soon as the object is available (this may be before
    706          `request_object` returns and may happen in other thread). On error the
    707          `error_handler` will be called, and on timeout -- the
    708          `timeout_handler`.
    709
    710          :Parameters:
    711              - `object_class`: class (type) of the object requested.
    712              - `address`: address of the object requested.
    713              - `state`: the worst acceptable object state. When 'new' then always
    714                a new object will be created/fetched. 'stale' will select any
    715                item available in cache.
    716              - `object_handler`: function to be called when object is available.
    717                It will be called with the following arguments: address, object
    718                and its state.
    719              - `error_handler`: function to be called on object retrieval error.
    720                It will be called with two arguments: requested address and
    721                additional error information (fetcher-specific, may be
    722                StanzaError for XMPP objects).  If not given, then the object
    723                handler will be called with object set to `None` and state
    724                "error".
    725              - `timeout_handler`: function to be called on object retrieval
    726                timeout.  It will be called with only one argument: the requested
    727                address. If not given, then the `error_handler` will be called
    728                instead, with error details set to `None`.
    729              - `backup_state`: when set and object in state `state` is not
    730                available in the cache and object retrieval failed then object
    731                with this state will also be looked-up in the cache and provided
    732                if available.
    733              - `timeout`: time interval after which retrieval of the object
    734                should be given up.
    735              - `freshness_period`: time interval after which the item created
    736                should become 'old'.
    737              - `expiration_period`: time interval after which the item created
    738                should become 'stale'.
    739              - `purge_period`: time interval after which the item created
    740                shuld be removed from the cache.
    741          :Types:
    742              - `object_class`: `classobj`
    743              - `address`: any hashable
    744              - `state`: "new", "fresh", "old" or "stale"
    745              - `object_handler`: callable(address, value, state)
    746              - `error_handler`: callable(address, error_data)
    747              - `timeout_handler`: callable(address)
    748              - `backup_state`: "new", "fresh", "old" or "stale"
    749              - `timeout`: `timedelta`
    750              - `freshness_period`: `timedelta`
    751              - `expiration_period`: `timedelta`
    752              - `purge_period`: `timedelta`
    753          """
    754
    755          self._lock.acquire()
    756          try:
    757              if object_class not in self._caches:
    758                  raise TypeError, "No cache for %r" % (object_class,)
    759
    760              self._caches[object_class].request_object(address, state, object_handler,
    761                      error_handler, timeout_handler, backup_state, timeout,
    762                      freshness_period, expiration_period, purge_period)
    763          finally:
    764              self._lock.release()
    765
    766 -    def tick(self):
    767          """Do the regular cache maintenance.
    768
    769          Must be called from time to time for timeouts and cache old items
    770          purging to work."""
    771          self._lock.acquire()
    772          try:
    773              for cache in self._caches.values():
    774                  cache.tick()
    775          finally:
    776              self._lock.release()
    777
    778 -    def register_fetcher(self, object_class, fetcher_class):
    779          """Register a fetcher class for an object class.
    780
    781          :Parameters:
    782              - `object_class`: class to be retrieved by the fetcher.
    783              - `fetcher_class`: the fetcher class.
    784          :Types:
    785              - `object_class`: `classobj`
    786              - `fetcher_class`: `CacheFetcher` based class
    787          """
    788          self._lock.acquire()
    789          try:
    790              cache = self._caches.get(object_class)
    791              if not cache:
    792                  cache = Cache(self.max_items, self.default_freshness_period,
    793                          self.default_expiration_period, self.default_purge_period)
    794                  self._caches[object_class] = cache
    795              cache.set_fetcher(fetcher_class)
    796          finally:
    797              self._lock.release()
    798
    799 -    def unregister_fetcher(self, object_class):
    800          """Unregister a fetcher class for an object class.
    801
    802          :Parameters:
    803              - `object_class`: class retrieved by the fetcher.
    804          :Types:
    805              - `object_class`: `classobj`
    806          """
    807          self._lock.acquire()
    808          try:
    809              cache = self._caches.get(object_class)
    810              if not cache:
    811                  return
    812              cache.set_fetcher(None)
    813          finally:
    814              self._lock.release()
    815
    816  # vi: sts=4 et sw=4
    817