Hi there,

this is a revised version of the patch posted here:
http://svn.haxx.se/dev/archive-2010-04/0106.shtml

The improvements:

* allow for arbitrary (but preferably very small) number
 of pinned entries per cache
* support pinned entries from multiple threads
 (i.e. don't use hold the mutex across API calls)
* added atomic set&pin operation
* check for pin leaks before the pool cleanup

An svn_wc__call_with_write_lock-like approach would
work for scenarios like the packed_offset_cache one in
this patch. But it is much less obvious when the function
to execute may or may not create the locked object in
the first place. Therefore, I decided against that option.

-- Stefan^2.

[[[

Allow for cache entries to be access directly, i.e. without
prior copying. Use that to fix FSFS offset lookup runtime
from O(N) to amortized O(1).

* subversion/include/private/svn_cache.h
 (svn_cache__create_memcache, svn_cache__set):
 update commentary.
 (svn_cache__get_pinned, svn_cache__set_pinned,
 svn_cache__unpin): declare new functions

* subversion/libsvn_fs_fs/fs_fs.c
 (get_packed_offset): use new interface functions.

* subversion/libsvn_subr/cache-inprocess.c
 (inprocess_cache_t): add head of pinned entry list
 (cache_page, cache_entry): add pin counters and
 pinned entry list
 (allocate_page, pin_entry): new utility functions
 (erase_page): freeing pages with pinned entries is fatal.
 (inprocess_cache_set_locked): core implementation
 of inprocess_cache_set without using the mutex;
 don't evict nor modify pinned entries
 (inprocess_cache_set): reimplement using
 inprocess_cache_set_locked
 (inprocess_cache_get_pinned, inprocess_cache_set_pinned,
 inprocess_cache_unpin): implement
 (inprocess_cache_vtable): add new entries
 (check_cache_before_cleanup, svn_cache_create_inprocess):
 check for leftover pins before clearing up the cache.

* subversion/libsvn_subr/cache-memcache.c
 (memcache_get_pinned, memcache_set_pinned,
 memcache_get_unpin): implement as "not supported"

* subversion/libsvn_subr/cache.c
 (svn_cache__get_pinned, svn_cache__set_pinned,
 svn_cache__get_unpin): implement

* subversion/libsvn_subr/cache.h
 (svn_cache__vtable_t): extend vtable

patch by stefanfuhrmann < at > alice-dsl.de
]]]
Index: subversion/include/private/svn_cache.h
===================================================================
--- subversion/include/private/svn_cache.h      (revision 937673)
+++ subversion/include/private/svn_cache.h      (working copy)
@@ -144,7 +144,8 @@
  *
  * These caches are always thread safe.
  *
- * These caches do not support svn_cache__iter.
+ * These caches do not support svn_cache__iter, svn_cache__get_pinned,
+ * svn_cache__set_pinned and svn_cache__unpin.
  *
  * If Subversion was not built with apr_memcache support, always
  * raises SVN_ERR_NO_APR_MEMCACHE.
@@ -215,6 +216,9 @@
  * the cache's copy of the previous value may not be immediately
  * cleared); it is only guaranteed to not leak for caches created with
  * @a items_per_page equal to 1.
+ *
+ * It is illegal to modify a pinned entry. Attempts to do so will trigger 
+ * assertions.
  */
 svn_error_t *
 svn_cache__set(svn_cache__t *cache,
@@ -248,6 +252,67 @@
                 svn_iter_apr_hash_cb_t func,
                 void *baton,
                 apr_pool_t *pool);
+
+/**
+ * Returns a reference to a value indexed by @a key from @a cache into 
+ * @a *value, setting @a *found to TRUE iff it is in the cache and FALSE 
+ * if it is not found. If necessary, temporary objects will be allocated
+ * in @a pool.
+ *
+ * Unlike svn_cache__get, this function does not create a copy of that 
+ * value but returns a reference to the cached value. The reference remains 
+ * valid until svn_cache__unpin is called.
+ *
+ * Every value returned by this function must be released explicitly by
+ * calling svn_cache__unpin. It is suggested that the calling code evaluates
+ * the returned @a *value immediately and unpins it a.s.a.p.
+ *
+ * It is not legal to modify a pinned entry or to destroy the cache
+ * as long as it contains pinned entries. Failure to comply with this
+ * will trigger assertions.
+ *
+ * svn_cache__get_pinned is not supported by all cache implementations; 
+ * see the svn_cache__create_* function for details.
+ */
+svn_error_t *
+svn_cache__get_pinned(void **value,
+                      svn_boolean_t *found,
+                      const svn_cache__t *cache,
+                      const void *key,
+                      apr_pool_t *pool);
+
+/**
+ * Atomic combination of svn_cache__set and svn_cache__get_pinned.
+ * The pinned reference is returned in @a *pinned_value.
+ *
+ * It is not legal to modify a pinned entry or to destroy the cache
+ * as long as it contains pinned entries. Failure to comply with this
+ * will trigger assertions.
+ *
+ * svn_cache__set_pinned is not supported by all cache implementations; 
+ * see the svn_cache__create_* function for details.
+ */
+svn_error_t *
+svn_cache__set_pinned(svn_cache__t *cache,
+                      const void *key,
+                      void *value,
+                      void **pinned_value,
+                      apr_pool_t *pool);
+
+/**
+ * Unpin a @a value reference from @a cache previously returned by 
+ * svn_cache__get_pinned or svn_cache__set_pinned. The function will 
+ * return an error if the reference has not been pinned before. If 
+ * necessary, temporary objects will be allocated in @a pool.
+ *
+ * svn_cache__unpin is not supported by all cache implementations; 
+ * see the svn_cache__create_* function for details.
+ */
+svn_error_t *
+svn_cache__unpin(const void *value,
+                 const svn_cache__t *cache,
+                 apr_pool_t *pool);
+
 /** @} */
 
 
Index: subversion/include/svn_error_codes.h
===================================================================
--- subversion/include/svn_error_codes.h        (revision 937673)
+++ subversion/include/svn_error_codes.h        (working copy)
@@ -1311,6 +1311,11 @@
              SVN_ERR_MISC_CATEGORY_START + 32,
              "Unsupported schema found in SQLite db")
 
+  /** @since New in 1.7. */
+  SVN_ERRDEF(SVN_ERR_INPROCCACHE_OVERFLOW,
+             SVN_ERR_MISC_CATEGORY_START + 33,
+             "In-process cache overflow due to pinned entries")
+
   /* command-line client errors */
 
   SVN_ERRDEF(SVN_ERR_CL_ARG_PARSING_ERROR,
Index: subversion/libsvn_fs_fs/fs_fs.c
===================================================================
--- subversion/libsvn_fs_fs/fs_fs.c     (revision 937673)
+++ subversion/libsvn_fs_fs/fs_fs.c     (working copy)
@@ -1828,14 +1828,14 @@
   apr_pool_t *iterpool;
 
   shard = rev / ffd->max_files_per_dir;
-  SVN_ERR(svn_cache__get((void **) &manifest, &is_cached,
-                         ffd->packed_offset_cache, &shard, pool));
+  SVN_ERR(svn_cache__get_pinned((void **) &manifest, &is_cached,
+                                ffd->packed_offset_cache, &shard, pool));
 
   if (is_cached)
     {
       *rev_offset = APR_ARRAY_IDX(manifest, rev % ffd->max_files_per_dir,
                                   apr_off_t);
-      return SVN_NO_ERROR;
+      return svn_cache__unpin((void*)manifest, ffd->packed_offset_cache, pool);
     }
 
   /* Open the manifest file. */
Index: subversion/libsvn_subr/cache-inprocess.c
===================================================================
--- subversion/libsvn_subr/cache-inprocess.c    (revision 937673)
+++ subversion/libsvn_subr/cache-inprocess.c    (working copy)
@@ -66,6 +66,14 @@
    */
   apr_pool_t *cache_pool;
 
+  /* If NULL, no item has been pined.
+   * The pinned entries for a double-linked listed. 
+   * While this is efficient for very low numbers of entries,
+   * we need to switch to some hash once this feature should
+   * get used more extensively.
+   */
+  struct cache_entry *first_pinned_entry;
+
 #if APR_HAS_THREADS
   /* A lock for intra-process synchronization to the cache, or NULL if
    * the cache's creator doesn't feel the cache needs to be
@@ -89,6 +97,9 @@
   /* A singly linked list of the entries on this page; used to remove
    * them from the cache's HASH before reusing the page. */
   struct cache_entry *first_entry;
+
+  /* Number of pinned entries in this page. */
+  apr_size_t pin_count;
 };
 
 /* An cache entry. */
@@ -102,6 +113,16 @@
 
   /* Next entry on the page. */
   struct cache_entry *next_entry;
+
+  /* Next entry that got pinned. NULL, if pin_count == 0 or end of the chain*/
+  struct cache_entry *next_pinned;
+
+  /* Previous element in the list of pinned entries.
+  /* NULL, if pin_count == 0 or head of the list. */
+  struct cache_entry *previous_pinned;
+
+  /* Number of times this entry got pinned. */
+  apr_size_t pin_count;
 };
 
 
@@ -243,6 +264,8 @@
 {
   struct cache_entry *e;
 
+  SVN_ERR_ASSERT_NO_RETURN(page->pin_count == 0);
+
   remove_page_from_list(page);
 
   for (e = page->first_entry;
@@ -262,21 +285,38 @@
   cache->partial_page_number_filled = 0;
 }
 
+/* Create and initialize a fresh cache page.
+ * Don't update the global page counter. */
+static void 
+allocate_page(inprocess_cache_t *cache)
+{
+    cache->partial_page = apr_pcalloc(cache->cache_pool,
+                            sizeof(*(cache->partial_page)));
 
+    cache->partial_page->page_pool = svn_pool_create(cache->cache_pool);
+    cache->partial_page->pin_count = 0;
+    cache->partial_page_number_filled = 0;
+}
+
+/* Similar to inprocess_cache_set but assumes the cache
+ * to be already locked (and won't unlock it, either).
+ * Attempts to modify a pinned entry trigger an assertion.
+ * Returns the entry that contains the value that has been set. */
 static svn_error_t *
-inprocess_cache_set(void *cache_void,
-                    const void *key,
-                    void *value,
-                    apr_pool_t *pool)
+inprocess_cache_set_locked(inprocess_cache_t *cache,
+                           const void *key,
+                           void *value,
+                           struct cache_entry **entry,
+                           apr_pool_t *pool)
 {
-  inprocess_cache_t *cache = cache_void;
-  struct cache_entry *existing_entry;
-  svn_error_t *err = SVN_NO_ERROR;
+  struct cache_entry *existing_entry =
+      apr_hash_get(cache->hash, key, cache->klen);
 
-  SVN_ERR(lock_cache(cache));
+  /* You cannot update a pinned entry because others will
+   * probably reference the old entry. */
+  if (existing_entry)
+      SVN_ERR_ASSERT(existing_entry->pin_count == 0);
 
-  existing_entry = apr_hash_get(cache->hash, key, cache->klen);
-
   /* Is it already here, but we can do the one-item-per-page
    * optimization? */
   if (existing_entry && cache->items_per_page == 1)
@@ -302,19 +342,18 @@
       struct cache_page *page = existing_entry->page;
 
       move_page_to_front(cache, page);
-      err = duplicate_value(&(existing_entry->value), cache,
-                            value, page->page_pool);
-      goto cleanup;
+      SVN_ERR(duplicate_value(&(existing_entry->value), cache,
+                              value, page->page_pool));
+
+      *entry = existing_entry;
+      return SVN_NO_ERROR;
     }
 
   /* Do we not have a partial page to put it on, but we are allowed to
    * allocate more? */
   if (cache->partial_page == NULL && cache->unallocated_pages > 0)
     {
-      cache->partial_page = apr_pcalloc(cache->cache_pool,
-                                        sizeof(*(cache->partial_page)));
-      cache->partial_page->page_pool = svn_pool_create(cache->cache_pool);
-      cache->partial_page_number_filled = 0;
+      allocate_page(cache);
       (cache->unallocated_pages)--;
     }
 
@@ -323,12 +362,39 @@
    * count? */
   if (cache->partial_page == NULL)
     {
-      struct cache_page *oldest_page = cache->sentinel->prev;
+      struct cache_page *victim_page;
 
-      SVN_ERR_ASSERT(oldest_page != cache->sentinel);
+      /* Look for a page with no pinned items on it. 
+       * Start at the oldest page. */
+      for (victim_page = cache->sentinel->prev;
+           victim_page != cache->sentinel;
+           victim_page = victim_page->prev)
+        {
+          if (victim_page->pin_count == 0)
+            {
+              /* Erase the page and put it in cache->partial_page. */
+              erase_page(cache, victim_page);
+              break;
+            }
+        }
+    }
 
-      /* Erase the page and put it in cache->partial_page. */
-      erase_page(cache, oldest_page);
+  /* Cache overflow due to pinned items on every page?
+   * Tough luck. We don't have any chance other than to 
+   * the original allocation limit. */
+  if (cache->partial_page == NULL)
+    {
+      /* You pinned too many entries. Fix that! */
+      svn_error_t *svn_warning = 
+          svn_error_create(SVN_ERR_INPROCCACHE_OVERFLOW, 
+                           NULL,
+                           _("Cache overflow. Too many entries pinned."));
+
+      svn_handle_warning2(stderr, svn_warning, "svn-debug: ");
+      svn_error_clear (svn_warning);
+
+      /* Well, until then we need to carry one ... */
+      allocate_page(cache);
     }
 
   SVN_ERR_ASSERT(cache->partial_page != NULL);
@@ -340,16 +406,19 @@
 
     /* Copy the key and value into the page's pool.  */
     new_entry->key = duplicate_key(cache, key, page->page_pool);
-    err = duplicate_value(&(new_entry->value), cache, value,
-                          page->page_pool);
-    if (err)
-      goto cleanup;
+    SVN_ERR(duplicate_value(&(new_entry->value), cache, value,
+                            page->page_pool));
 
     /* Add the entry to the page's list. */
     new_entry->page = page;
     new_entry->next_entry = page->first_entry;
     page->first_entry = new_entry;
 
+    /* Initialized pinning info */
+    new_entry->next_pinned = NULL;
+    new_entry->previous_pinned = NULL;
+    new_entry->pin_count = 0;
+
     /* Add the entry to the hash, using the *entry's* copy of the
      * key. */
     apr_hash_set(cache->hash, new_entry->key, cache->klen, new_entry);
@@ -363,10 +432,28 @@
         insert_page(cache, page);
         cache->partial_page = NULL;
       }
+
+    /* Return result*/
+    *entry = new_entry;
+    return SVN_NO_ERROR;
   }
+}
 
- cleanup:
-  return unlock_cache(cache, err);
+/* Synchronize access to the cache and then set the value.
+ * We don't care about the entry being used. */
+static svn_error_t *
+inprocess_cache_set(void *cache_void,
+                    const void *key,
+                    void *value,
+                    apr_pool_t *pool)
+{
+  inprocess_cache_t *cache = cache_void;
+  struct cache_entry *existing_entry;
+
+  SVN_ERR(lock_cache(cache));
+  return unlock_cache(cache, 
+                      inprocess_cache_set_locked(cache, key, value, 
+                                                 &existing_entry, pool));
 }
 
 /* Baton type for svn_cache__iter. */
@@ -406,15 +493,162 @@
   return unlock_cache(cache,
                       svn_iter_apr_hash(completed, cache->hash, iter_cb, &b,
                                         pool));
+}
 
+/* All the counter & pointer update jazz required to pin the
+ * given entry. */
+static void
+pin_entry(inprocess_cache_t *cache,
+          struct cache_entry *entry)
+{
+  /* Entries that have already been pinned just need 
+   * their reference counts to be bumped. */
+  if (entry->pin_count == 0)
+    {
+      /* The entry has not been pinned, yet.
+       * We put it at the head of the pinned entry list. */
+      if (cache->first_pinned_entry != NULL)
+        {
+          entry->next_pinned = cache->first_pinned_entry;
+          cache->first_pinned_entry->previous_pinned = entry;
+        }
+
+      cache->first_pinned_entry = entry;
+    }
+
+  ++entry->pin_count;
+  ++entry->page->pin_count;
 }
 
+/* Very similar to inprocess_cache_get but will return a pinned
+ * reference to the value instead of a copy. */
+static svn_error_t *
+inprocess_cache_get_pinned(void **value_p,
+                           svn_boolean_t *found,
+                           void *cache_void,
+                           const void *key,
+                           apr_pool_t *pool)
+{
+  inprocess_cache_t *cache = cache_void;
+  struct cache_entry *entry;
+
+  SVN_ERR(lock_cache(cache));
+
+  entry = apr_hash_get(cache->hash, key, cache->klen);
+  if (! entry)
+    {
+      *found = FALSE;
+      return unlock_cache(cache, SVN_NO_ERROR);
+    }
+
+  move_page_to_front(cache, entry->page);
+  pin_entry(cache, entry);
+
+  *found = TRUE;
+  *value_p = entry->value;
+
+  return unlock_cache(cache, SVN_NO_ERROR);
+}
+
+/* Similar to svn_cache__set but will also return a pinned
+ * reference to the cached value that has been set. */
+static svn_error_t *
+inprocess_cache_set_pinned(void *cache_void,
+                           const void *key,
+                           void *value,
+                           void **pinned_value,
+                           apr_pool_t *pool)
+{
+  inprocess_cache_t *cache = cache_void;
+  struct cache_entry *existing_entry = NULL;
+  svn_error_t *err = SVN_NO_ERROR;
+
+  SVN_ERR(lock_cache(cache));
+  err = inprocess_cache_set_locked(cache, key, value, 
+                                   &existing_entry, pool);
+  if (err == SVN_NO_ERROR)
+    {
+      SVN_ERR_ASSERT(existing_entry != NULL);
+      pin_entry(cache, existing_entry);
+
+      *pinned_value = existing_entry->value;
+    }
+
+  return unlock_cache(cache, err);
+}
+
+/* Linearly probe chain of pinned entries for one matching value_p
+ * and unpin it. If there is no such entry, return an error.
+ * This implementation is slow for larger numbers of pinned entries. */
+static svn_error_t *
+inprocess_cache_unpin(const void *value_p,
+                      void *cache_void,
+                      apr_pool_t *pool)
+{
+  inprocess_cache_t *cache = cache_void;
+  struct cache_entry *entry;
+
+  SVN_ERR(lock_cache(cache));
+
+  /* Crawl through the list of  pinned entries until
+   * we found the one to unpin. */
+  for (entry = cache->first_pinned_entry;
+       entry != NULL;
+       entry = entry->next_pinned)
+    {
+       if (entry->value == value_p)
+         {
+           --entry->page->pin_count;
+           if (--entry->pin_count == 0)
+             {
+               if (cache->first_pinned_entry == entry)
+                   cache->first_pinned_entry = entry->next_pinned;
+
+               if (entry->previous_pinned != NULL)
+                 {
+                   entry->previous_pinned->next_pinned = entry->next_pinned;
+                   entry->previous_pinned = NULL;
+                 }
+
+               if (entry->next_pinned != NULL)
+                 {
+                   entry->next_pinned->previous_pinned = 
entry->previous_pinned;
+                   entry->next_pinned = NULL;
+                 }
+             }
+
+           return unlock_cache(cache, SVN_NO_ERROR);
+         }
+    }
+
+  /* Wasn't found! */
+  return unlock_cache(cache, svn_error_create(SVN_ERR_INCORRECT_PARAMS, NULL,
+                            _("Object has not been pinned")));
+}
+
 static svn_cache__vtable_t inprocess_cache_vtable = {
   inprocess_cache_get,
   inprocess_cache_set,
-  inprocess_cache_iter
+  inprocess_cache_iter,
+  inprocess_cache_get_pinned,
+  inprocess_cache_set_pinned,
+  inprocess_cache_unpin
 };
 
+/* Pre-cleanup pool hook to check for any remaining pinned entries
+ * before destroying the pool. */
+static apr_status_t 
+check_cache_before_cleanup(void *cache_void)
+{
+  inprocess_cache_t *cache = cache_void;
+
+  /* Pin leaks are very dangerous: 
+   * someone might access the memory after de-allocation. */
+  SVN_ERR_ASSERT_NO_RETURN(cache->first_pinned_entry == NULL);
+
+  return APR_SUCCESS;
+}
+
 svn_error_t *
 svn_cache__create_inprocess(svn_cache__t **cache_p,
                             svn_cache__dup_func_t dup_func,
@@ -443,6 +677,9 @@
   /* The sentinel doesn't need a pool.  (We're happy to crash if we
    * accidentally try to treat it like a real page.) */
 
+  cache->first_pinned_entry = NULL;
+  apr_pool_pre_cleanup_register(pool, cache, check_cache_before_cleanup);
+
 #if APR_HAS_THREADS
   if (thread_safe)
     {
Index: subversion/libsvn_subr/cache-memcache.c
===================================================================
--- subversion/libsvn_subr/cache-memcache.c     (revision 937673)
+++ subversion/libsvn_subr/cache-memcache.c     (working copy)
@@ -222,10 +222,44 @@
                           _("Can't iterate a memcached cache"));
 }
 
+static svn_error_t *
+memcache_get_pinned(void **value_p,
+                    svn_boolean_t *found,
+                    void *cache_void,
+                    const void *key,
+                    apr_pool_t *pool)
+{
+  return svn_error_create(SVN_ERR_UNSUPPORTED_FEATURE, NULL,
+                          _("Can't pin a memcached object"));
+}
+
+static svn_error_t *
+memcache_set_pinned(svn_cache__t *cache,
+                    const void *key,
+                    void *value,
+                    void **pinned_value,
+                    apr_pool_t *pool)
+{
+  return svn_error_create(SVN_ERR_UNSUPPORTED_FEATURE, NULL,
+                          _("Can't pin a memcached object"));
+}
+
+static svn_error_t *
+memcache_unpin(const void *value_p,
+               void *cache_void,
+               apr_pool_t *pool)
+{
+  return svn_error_create(SVN_ERR_UNSUPPORTED_FEATURE, NULL,
+                          _("Can't unpin a memcached object"));
+}
+
 static svn_cache__vtable_t memcache_vtable = {
   memcache_get,
   memcache_set,
-  memcache_iter
+  memcache_iter,
+  memcache_get_pinned,
+  memcache_set_pinned,
+  memcache_unpin,
 };
 
 svn_error_t *
Index: subversion/libsvn_subr/cache.c
===================================================================
--- subversion/libsvn_subr/cache.c      (revision 937673)
+++ subversion/libsvn_subr/cache.c      (working copy)
@@ -96,3 +96,50 @@
                                pool);
 }
 
+svn_error_t *
+svn_cache__get_pinned(void **value_p,
+                      svn_boolean_t *found,
+                      const svn_cache__t *cache,
+                      const void *key,
+                      apr_pool_t *pool)
+{
+  /* In case any errors happen and are quelched, make sure we start
+     out with FOUND set to false. */
+  *found = FALSE;
+  return handle_error(cache,
+                      (cache->vtable->get_pinned)(value_p,
+                                                  found,
+                                                  cache->cache_internal,
+                                                  key,
+                                                  pool),
+                      pool);
+}
+
+svn_error_t *
+svn_cache__set_pinned(svn_cache__t *cache,
+                      const void *key,
+                      void *value,
+                      void **pinned_value,
+                      apr_pool_t *pool)
+{
+  return handle_error(cache,
+                      (cache->vtable->set_pinned)(cache->cache_internal,
+                                                  key,
+                                                  value,
+                                                  pinned_value,
+                                                  pool),
+                      pool);
+}
+
+svn_error_t *
+svn_cache__unpin(const void *value_p,
+                 const svn_cache__t *cache,
+                 apr_pool_t *pool)
+{
+  return handle_error(cache,
+                      (cache->vtable->unpin)(value_p,
+                                             cache->cache_internal,
+                                             pool),
+                      pool);
+}
+
Index: subversion/libsvn_subr/cache.h
===================================================================
--- subversion/libsvn_subr/cache.h      (revision 937673)
+++ subversion/libsvn_subr/cache.h      (working copy)
@@ -47,6 +47,22 @@
                        svn_iter_apr_hash_cb_t func,
                        void *baton,
                        apr_pool_t *pool);
+
+  svn_error_t *(*get_pinned)(void **value,
+                             svn_boolean_t *found,
+                             void *cache_implementation,
+                             const void *key,
+                             apr_pool_t *pool);
+
+  svn_error_t *(*set_pinned)(void *cache_implementation,
+                             const void *key,
+                             void *value,
+                             void **pinned_value,
+                             apr_pool_t *pool);
+
+  svn_error_t *(*unpin)(const void *value,
+                        void *cache_implementation,
+                        apr_pool_t *pool);
 } svn_cache__vtable_t;
 
 struct svn_cache__t {

Reply via email to