Proof-of-concept implementation of cache control

diff --git a/trac/env.py b/trac/env.py
--- a/trac/env.py
+++ b/trac/env.py
@@ -196,6 +196,8 @@
             ('setuptools', setuptools.__version__),
             ]
         self._href = self._abs_href = None
+        self._cache_control = {}
+        self._cache_lock = threading.RLock()
 
         from trac.loader import load_components
         plugins_dir = self.config.get('inherit', 'plugins_dir')
@@ -481,6 +483,74 @@
         return self._abs_href
     abs_href = property(_get_abs_href, 'The application URL')
 
+    # Cache control
+
+    def update_cache_control(self):
+        self._cache_lock.acquire()
+        try:
+            db = self.get_db_cnx()
+            cursor = db.cursor()
+            cursor.execute("SELECT key, generation FROM cache_control")
+            for key, gen in cursor.fetchall():
+                previous_gen = self._cache_control.get(key, 0)
+                if abs(previous_gen) < gen: 
+                    self._cache_control[key] = -gen
+                    self.log.debug("Cache '%s' needs update (%d)", key, gen)
+                # otherwise a race happened, a newer generation already made it
+                # into the cache control:
+                #  - either it's positive, meaning the actual cache was 
+                #    already updated to a newer generation, 
+                #  - or it's negative meaning we already target a newer 
+                #    generation. 
+                # In both cases we simply do nothing.
+        finally:
+            self._cache_lock.release()
+
+    def invalidate_cache(self, key):
+        if not isinstance(key, basestring):
+            key = key.__class__.__name__
+        self._cache_lock.acquire()
+        try:
+            db = self.get_db_cnx()
+            cursor = db.cursor()
+            try:
+                gen = 1
+                cursor.execute("INSERT INTO cache_control VALUES (%s,%s)",
+                               (key, gen))
+            except:
+                cursor.execute("UPDATE cache_control SET "
+                               "generation=generation+1 WHERE key=%s ", (key,))
+                cursor.execute("SELECT generation FROM cache_control "
+                               "WHERE key=%s", (key,))
+                cursor.fetchone()
+                for gen, in cursor:
+                    break
+            db.commit()
+            self._cache_control[key] = -gen # negative means invalid
+            self.log.debug("Cache '%s' invalidated (generation %d)", key, gen)
+        finally:
+            self._cache_lock.release()
+
+    def cache_is_valid(self, key):
+        if not isinstance(key, basestring):
+            key = key.__class__.__name__
+        # Not sure it's worth locking here: in case of a a concurrent 
+        # invalidation, even if we use the lock, we risk to miss the 
+        # notification for this request
+        return self._cache_control.get(key, 0) >= 0 # negative means invalid
+
+    def validate_cache(self, key):
+        if not isinstance(key, basestring):
+            key = key.__class__.__name__
+        self._cache_lock.acquire()
+        try:
+            gen = abs(self._cache_control.get(key, 1))
+            self._cache_control[key] = gen
+            self.log.debug("Cache '%s' updated to generation %d", key, gen)
+        finally:
+            self._cache_lock.release()
+
+
 
 class EnvironmentSetup(Component):
     implements(IEnvironmentSetupParticipant)
@@ -583,6 +653,8 @@
                 env = None
             if env is None:
                 env = env_cache.setdefault(env_path, open_environment(env_path))
+            else:
+                env.update_cache_control()
         finally:
             env_cache_lock.release()
     else:
diff --git a/trac/wiki/interwiki.py b/trac/wiki/interwiki.py
--- a/trac/wiki/interwiki.py
+++ b/trac/wiki/interwiki.py
@@ -44,9 +44,9 @@
 
     def reset(self):
         self._interwiki_map = None
-        self.config.touch()
-        # This dictionary maps upper-cased namespaces
-        # to (namespace, prefix, title) values;
+        # The above dictionary maps upper-cased namespaces
+        # to (namespace, prefix, title) values
+        self.env.invalidate_cache(self)
 
     # The component itself behaves as a map
 
@@ -114,10 +114,10 @@
 
     def _get_interwiki_map(self):
         from trac.wiki.model import WikiPage
-        if self._interwiki_map is None:
+        if self._interwiki_map is None or not self.env.cache_is_valid(self):
             self._interwiki_lock.acquire()
             try:
-                if self._interwiki_map is None:
+                if self._interwiki_map is None or not self.env.cache_is_valid(self):
                     self._interwiki_map = {}
                     content = WikiPage(self.env, InterWikiMap._page_name).text
                     in_map = False
@@ -131,9 +131,11 @@
                                     prefix, url, title = m.groups()
                                     url = url.strip()
                                     title = title and title.strip() or prefix
-                                    self[prefix] = (prefix, url, title)
+                                    self._interwiki_map[prefix.upper()] = \
+                                            (prefix, url, title)
                         elif line.startswith('----'):
                             in_map = True
+                    self.env.validate_cache(self)
             finally:
                 self._interwiki_lock.release()
         return self._interwiki_map

