Package pyxmpp :: Module cache |
[hide private]

# Source Code for Module pyxmpp.cache

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