Edgewall Software

TracDev/Proposals/CacheInvalidation: cache-manager-r7992.patch

File cache-manager-r7992.patch, 28.2 KB (added by rblank, 3 years ago)

Updated patch.

  • trac/admin/tests/console-tests.txt

    diff --git a/trac/admin/tests/console-tests.txt b/trac/admin/tests/console-tests.txt
    a b  
    1111attachment export    Export an attachment from a resource to a file or stdout 
    1212attachment list      List attachments of a resource 
    1313attachment remove    Remove an attachment from a resource 
     14cache invalidate     Invalidate one or more caches 
     15cache list           List caches 
    1416component add        Add a new component 
    1517component chown      Change component ownership 
    1618component list       Show available components 
  • new file trac/cache.py

    diff --git a/trac/cache.py b/trac/cache.py
    new file mode 100644
    - +  
     1# -*- coding: utf-8 -*- 
     2# 
     3# Copyright (C) 2009 Edgewall Software 
     4# All rights reserved. 
     5# 
     6# This software is licensed as described in the file COPYING, which 
     7# you should have received as part of this distribution. The terms 
     8# are also available at http://trac.edgewall.com/license.html. 
     9# 
     10# This software consists of voluntary contributions made by many 
     11# individuals. For the exact contribution history, see the revision 
     12# history and logs, available at http://trac.edgewall.org/. 
     13 
     14try: 
     15    import threading 
     16except ImportError: 
     17    import dummy_threading as threading 
     18 
     19from trac.admin import IAdminCommandProvider 
     20from trac.core import Component, implements 
     21from trac.util.compat import partial 
     22from trac.util.text import print_table 
     23from trac.util.translation import _ 
     24 
     25__all__ = ["CacheManager", "cached", "cached_value"] 
     26 
     27 
     28class cached_value(object): 
     29    """Method decorator creating a cached attribute from a data retrieval 
     30    method. 
     31     
     32    Accessing the cached attribute gives back the cached value. The data 
     33    retrieval method will be called as needed by the CacheManager. 
     34    Invalidating the cache for this value is done by `del`eting the attribute. 
     35     
     36    The data retrieval method is called with a single argument `db` containing 
     37    a reference to a database connection. All data retrieval should be done 
     38    through this connection. 
     39     
     40    Note that the cache validity is maintained using a table in the database. 
     41    Most notably, a cache invalidation will trigger a commit, so don't do this 
     42    while another database operation is in progress. 
     43     
     44    If more control over the transaction is needed, see the `cached` decorator. 
     45     
     46    This decorator can only be used within `Component` subclasses. See 
     47    CacheProxy for caching attributes of other objects. 
     48    """ 
     49    def __init__(self, retriever): 
     50        self.retriever = retriever 
     51        self.__doc__ = retriever.__doc__ 
     52         
     53    def __get__(self, instance, owner): 
     54        if instance is None: 
     55            return self 
     56        key = owner.__module__ + '.' + owner.__name__ \ 
     57              + '.' + self.retriever.__name__ 
     58        return CacheManager(instance.env).get(key, 
     59                partial(self.retriever, instance)) 
     60         
     61    def __delete__(self, instance): 
     62        key = instance.__class__.__module__ \ 
     63              + '.' + instance.__class__.__name__ \ 
     64              + '.' + self.retriever.__name__ 
     65        CacheManager(instance.env).invalidate(key) 
     66 
     67 
     68class cached(cached_value): 
     69    """Method decorator creating a cached attribute from a data retrieval 
     70    method. 
     71     
     72    In contrast with cached attributes created by the `cached_value` decorator, 
     73    accessing a cached attribute created with `cached` will not directly give  
     74    back the cached value. Instead, this will return a proxy object with `get` 
     75    and `invalidate` methods, both accepting a `db` connection. After calling 
     76    `invalidate(db)`, doing a `commit` is the responsibility of the caller. 
     77     
     78    This decorator can only be used within `Component` subclasses. See 
     79    CacheProxy for caching attributes of other objects. 
     80    """ 
     81    def __get__(self, instance, owner): 
     82        if instance is None: 
     83            return self 
     84        key = owner.__module__ + '.' + owner.__name__ \ 
     85              + '.' + self.retriever.__name__ 
     86        return CacheProxy(key, partial(self.retriever, instance), 
     87                          instance.env) 
     88 
     89 
     90class CacheProxy(object): 
     91    """Cached attribute proxy. 
     92     
     93    This is the class of the object returned when accessing an attribute 
     94    cached with the `cached` decorator. 
     95     
     96    It can also be instantiated explicitly to cache attributes of 
     97    non-`Component` objects. In this case, the cache identifier key must be 
     98    provided, and the data retrieval function is a normal callable (not an 
     99    unbound method). 
     100    """ 
     101    __slots__ = ["key", "retriever", "env"] 
     102     
     103    def __init__(self, key, retriever, env): 
     104        self.key = key 
     105        self.retriever = retriever 
     106        self.env = env 
     107     
     108    def get(self, db=None): 
     109        return CacheManager(self.env).get(self.key, self.retriever, db) 
     110     
     111    def invalidate(self, db=None): 
     112        CacheManager(self.env).invalidate(self.key, db) 
     113 
     114 
     115class CacheManager(Component): 
     116    """Cache manager component.""" 
     117     
     118    implements(IAdminCommandProvider) 
     119     
     120    def __init__(self): 
     121        self._cache = {} 
     122        self._local = threading.local() 
     123        self._lock = threading.RLock() 
     124     
     125    # Public interface 
     126     
     127    def reset_metadata(self): 
     128        """Reset per-request cache metadata.""" 
     129        try: 
     130            del self._local.meta 
     131            del self._local.cache 
     132        except AttributeError: 
     133            pass 
     134 
     135    def get(self, key, retriever, db=None): 
     136        """Get cached or fresh data for the given key.""" 
     137        # Get cache metadata 
     138        try: 
     139            local_meta = self._local.meta 
     140            local_cache = self._local.cache 
     141        except AttributeError: 
     142            # First cache usage in this request, retrieve cache metadata 
     143            # from the database and make a thread-local copy of the cache 
     144            self.log.debug("Retrieving cache metadata") 
     145            if db is None: 
     146                db = self.env.get_db_cnx() 
     147            cursor = db.cursor() 
     148            cursor.execute("SELECT key, generation FROM cache") 
     149            self._local.meta = local_meta = dict(cursor) 
     150            self._local.cache = local_cache = self._cache.copy() 
     151         
     152        db_generation = local_meta.get(key, -1) 
     153         
     154        # Try the thread-local copy first 
     155        try: 
     156            (data, generation) = local_cache[key] 
     157            if generation == db_generation: 
     158                return data 
     159        except KeyError: 
     160            pass 
     161         
     162        self._lock.acquire() 
     163        try: 
     164            # Get data from the process cache 
     165            try: 
     166                (data, generation) = local_cache[key] = self._cache[key] 
     167                if generation == db_generation: 
     168                    return data 
     169            except KeyError: 
     170                generation = None   # Force retrieval from the database 
     171             
     172            # Check if the process cache has the newest version, as it may 
     173            # have been updated after the metadata retrieval 
     174            if db is None: 
     175                db = self.env.get_db_cnx() 
     176            cursor = db.cursor() 
     177            cursor.execute("SELECT generation FROM cache WHERE key=%s", (key,)) 
     178            row = cursor.fetchone() 
     179            db_generation = row and row[0] or -1 
     180            if db_generation == generation: 
     181                return data 
     182             
     183            # Retrieve data from the database 
     184            self.log.debug("Retrieving data for cache '%s'", key) 
     185            data = retriever(db) 
     186            local_cache[key] = self._cache[key] = (data, db_generation) 
     187            local_meta[key] = db_generation 
     188            return data 
     189        finally: 
     190            self._lock.release() 
     191         
     192    def invalidate(self, key, db=None): 
     193        """Invalidate cached data for the given key.""" 
     194        self.log.debug("Invalidating cache '%s'", key) 
     195        self._lock.acquire() 
     196        try: 
     197            # Invalidate in other processes 
     198            handle_ta = db is None 
     199            if handle_ta: 
     200                db = self.env.get_db_cnx() 
     201            cursor = db.cursor() 
     202             
     203            # The row corresponding to the cache may not exist in the table 
     204            # yet. 
     205            #  - If the row exists, the UPDATE increments the generation, the 
     206            #    SELECT returns a row and we're done. 
     207            #  - If the row doesn't exist, the UPDATE does nothing, but starts 
     208            #    a transaction. The SELECT then returns nothing, and we can 
     209            #    safely INSERT a new row. 
     210            cursor.execute("UPDATE cache SET generation=generation+1 " 
     211                           "WHERE key=%s", (key,)) 
     212            cursor.execute("SELECT generation FROM cache WHERE key=%s", (key,)) 
     213            if not cursor.fetchone(): 
     214                cursor.execute("INSERT INTO cache VALUES (%s, %s)", (key, 0)) 
     215            if handle_ta: 
     216                db.commit() 
     217             
     218            # Invalidate in this process 
     219            self._cache.pop(key, None) 
     220             
     221            # Invalidate in this thread 
     222            try: 
     223                del self._local.cache[key] 
     224            except (AttributeError, KeyError): 
     225                pass 
     226        finally: 
     227            self._lock.release() 
     228 
     229    # IAdminCommandProvider methods 
     230     
     231    def get_admin_commands(self): 
     232        yield ('cache list', '', 
     233               'List caches', 
     234               None, self._do_list) 
     235        yield ('cache invalidate', '<key> [key] [...]', 
     236               'Invalidate one or more caches', 
     237               self._complete_invalidate, self._do_invalidate) 
     238     
     239    def get_cache_keys(self, db=None): 
     240        if db is None: 
     241            db = self.env.get_db_cnx() 
     242        cursor = db.cursor() 
     243        cursor.execute("SELECT key FROM cache") 
     244        return [row[0] for row in cursor] 
     245         
     246    def _complete_invalidate(self, args): 
     247        if len(args) == 1: 
     248            return self.get_cache_keys() 
     249     
     250    def _do_list(self): 
     251        db = self.env.get_db_cnx() 
     252        cursor = db.cursor() 
     253        cursor.execute("SELECT key, generation FROM cache ORDER BY key") 
     254        print_table([(row[0], int(row[1])) for row in cursor], 
     255                    [_('Key'), _('Generation')]) 
     256     
     257    def _do_invalidate(self, *keys): 
     258        db = self.env.get_db_cnx() 
     259        all_keys = set(self.get_cache_keys(db)) 
     260        inval_keys = [] 
     261        for key in keys: 
     262            if key.endswith('*'): 
     263                inval_keys.extend(each for each in all_keys 
     264                                  if each.startswith(key[:-1])) 
     265            else: 
     266                inval_keys.append(key) 
     267        for key in inval_keys: 
     268            self.invalidate(key, db) 
     269        db.commit() 
  • trac/db_default.py

    diff --git a/trac/db_default.py b/trac/db_default.py
    a b  
    1717from trac.db import Table, Column, Index 
    1818 
    1919# Database version identifier. Used for automatic upgrades. 
    20 db_version = 21 
     20db_version = 22 
    2121 
    2222def __mkreports(reports): 
    2323    """Utility function used to create report data in same syntax as the 
     
    5757        Column('authenticated', type='int'), 
    5858        Column('name'), 
    5959        Column('value')], 
     60    Table('cache', key='key')[ 
     61        Column('key'), 
     62        Column('generation')], 
    6063 
    6164    # Attachments 
    6265    Table('attachment', key=('type', 'id', 'filename'))[ 
  • trac/env.py

    diff --git a/trac/env.py b/trac/env.py
    a b  
    2525 
    2626from trac import db_default 
    2727from trac.admin import AdminCommandError, IAdminCommandProvider 
     28from trac.cache import CacheManager 
    2829from trac.config import * 
    2930from trac.core import Component, ComponentManager, implements, Interface, \ 
    3031                      ExtensionPoint, TracError 
     
    583584                env = None 
    584585            if env is None: 
    585586                env = env_cache.setdefault(env_path, open_environment(env_path)) 
     587            else: 
     588                CacheManager(env).reset_metadata() 
    586589        finally: 
    587590            env_cache_lock.release() 
    588591    else: 
  • trac/ticket/api.py

    diff --git a/trac/ticket/api.py b/trac/ticket/api.py
    a b  
    1616 
    1717import re 
    1818from datetime import datetime 
    19 try: 
    20     import threading 
    21 except ImportError: 
    22     import dummy_threading as threading 
    2319 
    2420from genshi.builder import tag 
    2521 
     22from trac.cache import cached, cached_value 
    2623from trac.config import * 
    2724from trac.core import * 
    2825from trac.perm import IPermissionRequestor, PermissionCache, PermissionSystem 
     
    157154        [TracTickets#Assign-toasDrop-DownList Assign-to as Drop-Down List] 
    158155        (''since 0.9'').""") 
    159156 
    160     _fields = None 
    161     _custom_fields = None 
    162  
    163157    def __init__(self): 
    164158        self.log.debug('action controllers for ticket workflow: %r' %  
    165159                [c.__class__.__name__ for c in self.action_controllers]) 
    166         self._fields_lock = threading.RLock() 
    167160 
    168161    # Public API 
    169162 
     
    192185 
    193186    def get_ticket_fields(self): 
    194187        """Returns the list of fields available for tickets.""" 
    195         # This is now cached - as it makes quite a number of things faster, 
    196         # e.g. #6436 
    197         if self._fields is None: 
    198             self._fields_lock.acquire() 
    199             try: 
    200                 if self._fields is None: # double-check (race after 1st check) 
    201                     self._fields = self._get_ticket_fields() 
    202             finally: 
    203                 self._fields_lock.release() 
    204         return [f.copy() for f in self._fields] 
     188        return [f.copy() for f in self.fields.get()] 
    205189 
    206     def reset_ticket_fields(self): 
    207         self._fields_lock.acquire() 
    208         try: 
    209             self._fields = None 
    210             self.config.touch() # brute force approach for now 
    211         finally: 
    212             self._fields_lock.release() 
     190    def reset_ticket_fields(self, db=None): 
     191        """Invalidate ticket field cache.""" 
     192        self.fields.invalidate(db) 
    213193 
    214     def _get_ticket_fields(self): 
     194    @cached 
     195    def fields(self, db): 
     196        """Return the list of fields available for tickets.""" 
    215197        from trac.ticket import model 
    216198 
    217         db = self.env.get_db_cnx() 
    218199        fields = [] 
    219200 
    220201        # Basic text fields 
     
    290271                            'col', 'row', 'format', 'max', 'page', 'verbose'] 
    291272 
    292273    def get_custom_fields(self): 
    293         if self._custom_fields is None: 
    294             self._fields_lock.acquire() 
    295             try: 
    296                 if self._custom_fields is None: # double-check 
    297                     self._custom_fields = self._get_custom_fields() 
    298             finally: 
    299                 self._fields_lock.release() 
    300         return [f.copy() for f in self._custom_fields] 
     274        return [f.copy() for f in self.custom_fields] 
    301275 
    302     def _get_custom_fields(self): 
     276    @cached_value 
     277    def custom_fields(self, db): 
     278        """Return the list of custom ticket fields available for tickets.""" 
    303279        fields = [] 
    304280        config = self.config['ticket-custom'] 
    305281        for name in [option for option, value in config.options() 
  • trac/ticket/model.py

    diff --git a/trac/ticket/model.py b/trac/ticket/model.py
    a b  
    418418                    enum.update(db=db) 
    419419            except ValueError: 
    420420                pass # Ignore cast error for this non-essential operation 
     421        TicketSystem(self.env).reset_ticket_fields(db) 
    421422 
    422423        if handle_ta: 
    423424            db.commit() 
    424425        self.value = self._old_value = None 
    425426        self.name = self._old_name = None 
    426         TicketSystem(self.env).reset_ticket_fields() 
    427427 
    428428    def insert(self, db=None): 
    429429        assert not self.exists, 'Cannot insert existing %s' % self.type 
     
    444444            self.value = int(float(cursor.fetchone()[0])) + 1 
    445445        cursor.execute("INSERT INTO enum (type,name,value) VALUES (%s,%s,%s)", 
    446446                       (self.type, self.name, self.value)) 
     447        TicketSystem(self.env).reset_ticket_fields(db) 
    447448 
    448449        if handle_ta: 
    449450            db.commit() 
    450451        self._old_name = self.name 
    451452        self._old_value = self.value 
    452         TicketSystem(self.env).reset_ticket_fields() 
    453453 
    454454    def update(self, db=None): 
    455455        assert self.exists, 'Cannot update non-existent %s' % self.type 
     
    471471            cursor.execute("UPDATE ticket SET %s=%%s WHERE %s=%%s" % 
    472472                           (self.ticket_col, self.ticket_col), 
    473473                           (self.name, self._old_name)) 
     474        TicketSystem(self.env).reset_ticket_fields(db) 
    474475 
    475476        if handle_ta: 
    476477            db.commit() 
    477478        self._old_name = self.name 
    478479        self._old_value = self.value 
    479         TicketSystem(self.env).reset_ticket_fields() 
    480480 
    481481    @classmethod 
    482482    def select(cls, env, db=None): 
     
    561561        cursor.execute("DELETE FROM component WHERE name=%s", (self.name,)) 
    562562 
    563563        self.name = self._old_name = None 
     564        TicketSystem(self.env).reset_ticket_fields(db) 
    564565 
    565566        if handle_ta: 
    566567            db.commit() 
    567         TicketSystem(self.env).reset_ticket_fields() 
    568568 
    569569    def insert(self, db=None): 
    570570        assert not self.exists, 'Cannot insert existing component' 
     
    581581        cursor.execute("INSERT INTO component (name,owner,description) " 
    582582                       "VALUES (%s,%s,%s)", 
    583583                       (self.name, self.owner, self.description)) 
     584        TicketSystem(self.env).reset_ticket_fields(db) 
    584585 
    585586        if handle_ta: 
    586587            db.commit() 
    587         TicketSystem(self.env).reset_ticket_fields() 
    588588 
    589589    def update(self, db=None): 
    590590        assert self.exists, 'Cannot update non-existent component' 
     
    607607            cursor.execute("UPDATE ticket SET component=%s WHERE component=%s", 
    608608                           (self.name, self._old_name)) 
    609609            self._old_name = self.name 
     610        TicketSystem(self.env).reset_ticket_fields(db) 
    610611 
    611612        if handle_ta: 
    612613            db.commit() 
    613         TicketSystem(self.env).reset_ticket_fields() 
    614614 
    615615    @classmethod 
    616616    def select(cls, env, db=None): 
     
    688688            ticket['milestone'] = retarget_to 
    689689            ticket.save_changes(author, 'Milestone %s deleted' % self.name, 
    690690                                now, db=db) 
     691        TicketSystem(self.env).reset_ticket_fields(db) 
    691692 
    692693        if handle_ta: 
    693694            db.commit() 
    694         TicketSystem(self.env).reset_ticket_fields() 
    695695 
    696696    def insert(self, db=None): 
    697697        assert self.name, 'Cannot create milestone with no name' 
     
    708708                       "VALUES (%s,%s,%s,%s)", 
    709709                       (self.name, to_timestamp(self.due), to_timestamp(self.completed), 
    710710                        self.description)) 
     711        TicketSystem(self.env).reset_ticket_fields(db) 
    711712 
    712713        if handle_ta: 
    713714            db.commit() 
    714         TicketSystem(self.env).reset_ticket_fields() 
    715715 
    716716    def update(self, db=None): 
    717717        assert self.name, 'Cannot update milestone with no name' 
     
    734734        cursor.execute("UPDATE ticket SET milestone=%s WHERE milestone=%s", 
    735735                       (self.name, self._old_name)) 
    736736        self._old_name = self.name 
     737        TicketSystem(self.env).reset_ticket_fields(db) 
    737738 
    738739        if handle_ta: 
    739740            db.commit() 
    740         TicketSystem(self.env).reset_ticket_fields() 
    741741 
    742742    @classmethod 
    743743    def select(cls, env, include_completed=True, db=None): 
     
    814814        cursor.execute("DELETE FROM version WHERE name=%s", (self.name,)) 
    815815 
    816816        self.name = self._old_name = None 
     817        TicketSystem(self.env).reset_ticket_fields(db) 
    817818 
    818819        if handle_ta: 
    819820            db.commit() 
    820         TicketSystem(self.env).reset_ticket_fields() 
    821821 
    822822    def insert(self, db=None): 
    823823        assert not self.exists, 'Cannot insert existing version' 
     
    834834        cursor.execute("INSERT INTO version (name,time,description) " 
    835835                       "VALUES (%s,%s,%s)", 
    836836                       (self.name, to_timestamp(self.time), self.description)) 
     837        TicketSystem(self.env).reset_ticket_fields(db) 
    837838 
    838839        if handle_ta: 
    839840            db.commit() 
    840         TicketSystem(self.env).reset_ticket_fields() 
    841841 
    842842    def update(self, db=None): 
    843843        assert self.exists, 'Cannot update non-existent version' 
     
    860860            cursor.execute("UPDATE ticket SET version=%s WHERE version=%s", 
    861861                           (self.name, self._old_name)) 
    862862            self._old_name = self.name 
     863        TicketSystem(self.env).reset_ticket_fields(db) 
    863864 
    864865        if handle_ta: 
    865866            db.commit() 
    866         TicketSystem(self.env).reset_ticket_fields() 
    867867 
    868868    @classmethod 
    869869    def select(cls, env, db=None): 
  • new file trac/upgrades/db22.py

    diff --git a/trac/upgrades/db22.py b/trac/upgrades/db22.py
    new file mode 100644
    - +  
     1from trac.db import Table, Column, DatabaseManager 
     2 
     3def do_upgrade(env, ver, cursor): 
     4    """Add the cache table.""" 
     5    table = Table('cache', key='key')[ 
     6        Column('key'), 
     7        Column('generation') 
     8    ] 
     9    db_connector, _ = DatabaseManager(env)._get_connector() 
     10    for stmt in db_connector.to_sql(table): 
     11        cursor.execute(stmt) 
  • trac/wiki/api.py

    diff --git a/trac/wiki/api.py b/trac/wiki/api.py
    a b  
    1616# Author: Jonas Borgström <jonas@edgewall.com> 
    1717#         Christopher Lenz <cmlenz@gmx.de> 
    1818 
    19 try: 
    20     import threading 
    21 except ImportError: 
    22     import dummy_threading as threading 
    23 import time 
    2419import urllib 
    2520import re 
    2621from StringIO import StringIO 
    2722 
    2823from genshi.builder import tag 
    2924 
     25from trac.cache import cached_value 
    3026from trac.config import BoolOption 
    3127from trac.core import * 
    3228from trac.resource import IResourceManager 
     
    164160    macro_providers = ExtensionPoint(IWikiMacroProvider) 
    165161    syntax_providers = ExtensionPoint(IWikiSyntaxProvider) 
    166162 
    167     INDEX_UPDATE_INTERVAL = 5 # seconds 
    168  
    169163    ignore_missing_pages = BoolOption('wiki', 'ignore_missing_pages', 'false', 
    170164        """Enable/disable highlighting CamelCase links to missing pages 
    171165        (''since 0.9'').""") 
     
    182176        For public sites where anonymous users can edit the wiki it is 
    183177        recommended to leave this option disabled (which is the default).""") 
    184178 
    185     def __init__(self): 
    186         self._index = None 
    187         self._last_index_update = 0 
    188         self._index_lock = threading.RLock() 
    189  
    190     def _update_index(self): 
    191         self._index_lock.acquire() 
    192         try: 
    193             now = time.time() 
    194             if now > self._last_index_update + WikiSystem.INDEX_UPDATE_INTERVAL: 
    195                 self.log.debug('Updating wiki page index') 
    196                 db = self.env.get_db_cnx() 
    197                 cursor = db.cursor() 
    198                 cursor.execute("SELECT DISTINCT name FROM wiki") 
    199                 self._index = {} 
    200                 for (name,) in cursor: 
    201                     self._index[name] = True 
    202                 self._last_index_update = now 
    203         finally: 
    204             self._index_lock.release() 
     179    @cached_value 
     180    def pages(self, db): 
     181        """Return the names of all existing wiki pages.""" 
     182        cursor = db.cursor() 
     183        cursor.execute("SELECT DISTINCT name FROM wiki") 
     184        return [name for (name,) in cursor] 
    205185 
    206186    # Public API 
    207187 
     
    211191        If the `prefix` parameter is given, only names that start with that 
    212192        prefix are included. 
    213193        """ 
    214         self._update_index() 
    215         # Note: use of keys() is intentional since iterkeys() is prone to 
    216         # errors with concurrent modification 
    217         for page in self._index.keys(): 
     194        for page in self.pages: 
    218195            if not prefix or page.startswith(prefix): 
    219196                yield page 
    220197 
    221198    def has_page(self, pagename): 
    222199        """Whether a page with the specified name exists.""" 
    223         self._update_index() 
    224         return self._index.has_key(pagename.rstrip('/')) 
     200        return pagename.rstrip('/') in self.pages 
    225201 
    226202    # IWikiChangeListener methods 
    227203 
    228204    def wiki_page_added(self, page): 
    229205        if not self.has_page(page.name): 
    230             self.log.debug('Adding page %s to index' % page.name) 
    231             self._index[page.name] = True 
     206            del self.pages 
    232207 
    233208    def wiki_page_changed(self, page, version, t, comment, author, ipnr): 
    234209        pass 
    235210 
    236211    def wiki_page_deleted(self, page): 
    237212        if self.has_page(page.name): 
    238             self.log.debug('Removing page %s from index' % page.name) 
    239             del self._index[page.name] 
     213            del self.pages 
    240214 
    241215    def wiki_page_version_deleted(self, page): 
    242216        pass 
  • trac/wiki/interwiki.py

    diff --git a/trac/wiki/interwiki.py b/trac/wiki/interwiki.py
    a b  
    1515# Author: Christian Boos <cboos@neuf.fr> 
    1616 
    1717import re 
    18 try: 
    19     import threading 
    20 except ImportError: 
    21     import dummy_threading as threading 
    2218 
    2319from genshi.builder import tag 
    2420 
     21from trac.cache import cached_value 
    2522from trac.core import * 
    2623from trac.wiki.formatter import Formatter 
    2724from trac.wiki.parser import WikiParser 
     
    3734    _interwiki_re = re.compile(r"(%s)[ \t]+([^ \t]+)(?:[ \t]+#(.*))?" % 
    3835                               WikiParser.LINK_SCHEME, re.UNICODE) 
    3936    _argspec_re = re.compile(r"\$\d") 
    40     _interwiki_map = None 
    4137 
    42     def __init__(self): 
    43         self._interwiki_lock = threading.RLock() 
    44  
    45     def reset(self): 
    46         self._interwiki_map = None 
    47         self.config.touch() 
    48         # This dictionary maps upper-cased namespaces 
    49         # to (namespace, prefix, title) values; 
    50  
    51     # The component itself behaves as a map 
     38    # The component itself behaves as a read-only map 
    5239 
    5340    def __contains__(self, ns): 
    5441        return ns.upper() in self.interwiki_map 
     
    5643    def __getitem__(self, ns): 
    5744        return self.interwiki_map[ns.upper()] 
    5845 
    59     def __setitem__(self, ns, value): 
    60         self.interwiki_map[ns.upper()] = value 
    61  
    6246    def keys(self): 
    6347        return self.interwiki_map.keys() 
    6448 
     
    10286 
    10387    def wiki_page_changed(self, page, version, t, comment, author, ipnr): 
    10488        if page.name == InterWikiMap._page_name: 
    105             self.reset() 
     89            del self.interwiki_map 
    10690 
    10791    def wiki_page_deleted(self, page): 
    10892        if page.name == InterWikiMap._page_name: 
    109             self.reset() 
     93            del self.interwiki_map 
    11094 
    11195    def wiki_page_version_deleted(self, page): 
    11296        if page.name == InterWikiMap._page_name: 
    113             self.reset() 
     97            del self.interwiki_map 
    11498 
    115     def _get_interwiki_map(self): 
     99    @cached_value 
     100    def interwiki_map(self, db): 
     101        """Map from upper-cased namespaces to (namespace, prefix, title)  
     102        values. 
     103        """ 
    116104        from trac.wiki.model import WikiPage 
    117         if self._interwiki_map is None: 
    118             self._interwiki_lock.acquire() 
    119             try: 
    120                 if self._interwiki_map is None: 
    121                     self._interwiki_map = {} 
    122                     content = WikiPage(self.env, InterWikiMap._page_name).text 
     105        map = {} 
     106        content = WikiPage(self.env, InterWikiMap._page_name, db=db).text 
     107        in_map = False 
     108        for line in content.split('\n'): 
     109            if in_map: 
     110                if line.startswith('----'): 
    123111                    in_map = False 
    124                     for line in content.split('\n'): 
    125                         if in_map: 
    126                             if line.startswith('----'): 
    127                                 in_map = False 
    128                             else: 
    129                                 m = re.match(InterWikiMap._interwiki_re, line) 
    130                                 if m: 
    131                                     prefix, url, title = m.groups() 
    132                                     url = url.strip() 
    133                                     title = title and title.strip() or prefix 
    134                                     self[prefix] = (prefix, url, title) 
    135                         elif line.startswith('----'): 
    136                             in_map = True 
    137             finally: 
    138                 self._interwiki_lock.release() 
    139         return self._interwiki_map 
    140     interwiki_map = property(_get_interwiki_map) 
     112                else: 
     113                    m = re.match(InterWikiMap._interwiki_re, line) 
     114                    if m: 
     115                        prefix, url, title = m.groups() 
     116                        url = url.strip() 
     117                        title = title and title.strip() or prefix 
     118                        map[prefix.upper()] = (prefix, url, title) 
     119            elif line.startswith('----'): 
     120                in_map = True 
     121        return map 
    141122 
    142123    # IWikiMacroProvider methods 
    143124