Merge lp:~mkanat/loggerhead/synchronize-lru_cache into lp:loggerhead

Proposed by Max Kanat-Alexander
Status: Superseded
Proposed branch: lp:~mkanat/loggerhead/synchronize-lru_cache
Merge into: lp:loggerhead
Diff against target: 39 lines (+13/-2)
1 file modified
loggerhead/history.py (+13/-2)
To merge this branch: bzr merge lp:~mkanat/loggerhead/synchronize-lru_cache
Reviewer Review Type Date Requested Status
Max Kanat-Alexander (community) Needs Fixing
Matt Nordhoff Approve
Review via email: mp+23977@code.launchpad.net

This proposal has been superseded by a proposal from 2010-05-04.

Description of the change

Fixes Bug #420738 by synchronizing all our access to lru_cache, which is not thread-safe.

In testing, I don't see any significant performance degradation under access from many different users accessing several different branches.

To post a comment you must log in.
Revision history for this message
Ian Clatworthy (ian-clatworthy) wrote :

Max,

Well done on tracking down the problem and thanks for the patch. Aren't threading problems fun? :-)

My main concern with the proposed change is that I'd prefer the Cache object to have the intelligence to Do The Right Thing wrt locking, rather than it being left up to the client to guard access to it. As it stands, we're deciding to use an LRU cache in apps/branch.py and paying the consequences through increased code complexity several layers deeper. What do you think about having a decorator class that provides the synchronized access? E.g. changing the cache creation line to be something like ...

  graph_cache = ThreadSafeCache(bzrlib.lru_cache.LRUCache(10))

and introducing ThreadSafeCache() as the layer with the RLock?

OTOH, this bug is critical and perhaps we ought to just land it as is, particularly as John is looking to rework the caching architecture Real Soon Now anyhow.

Any second opinions?

Revision history for this message
Max Kanat-Alexander (mkanat) wrote :

> Well done on tracking down the problem and thanks for the patch. Aren't
> threading problems fun? :-)

  Fun indeed. :-)

> What do you think
> about having a decorator class that provides the synchronized access? E.g.
> changing the cache creation line to be something like ...
>
> graph_cache = ThreadSafeCache(bzrlib.lru_cache.LRUCache(10))
>
> and introducing ThreadSafeCache() as the layer with the RLock?

  I thought about that, and the problem is that it would depend to some degree on implementation details of lru_cache--if the client started using it in different ways, or if the lru_cache implementation changed in some ways, we'd have to start adding locks in different places.

  John has already said that he's not going to make the LRUCache itself thread-safe (see the bug), and the only accesses we make into to it are via this interface, so I think the simplest and most forward-compatible solution is to put the locking in our client code.

410. By Matt Nordhoff

Fix infinite recursion when unpickling Containers.

Unpickle tries to access a few attributes (__getstate__, etc.) before filling the __dict__, so __getattr__ went into a loop trying to find self._properties.

It's no longer possible to use properties that start with an underscore, but that's a bit icky anyway.

411. By Matt Nordhoff

Don't let properties that start with _ be created in the first place.

Revision history for this message
Matt Nordhoff (mnordhoff) wrote :

Sounds fine to me, and I've been running it for a few days without problems.

review: Approve
412. By Michael Hudson-Doyle

merge ~mkanat/loggerhead/synchronize-lru_cache

413. By John A Meinel

Cache the Branch reverse-tag dict, since Branch does not.

Revision history for this message
John A Meinel (jameinel) wrote :

I should comment here a little bit. I'm a bit surprised that this actually handles what we want it to.

Specifically, RevInfoMemoryCache is created for each History object created, and a new one is created for each HTTP request (if I sorted out my info correctly.)

As such, *each* HTTP thread ends up with its own lock around the cache. And you don't end up doing much of any cross-thread protection, which was the whole point.

I could probably be convinced to add a potential locking primitive to LRUCache. I wanted to avoid the overhead, but we can just default to None, and allow callers to set a Lock object that we'll respect. Something that says "if self._lock is not None: self._lock.acquire()".

I think that __getitem__ can be protected after the "if node is mru:" check. If two threads are accessing different items concurrently, there is no clear-cut MRU, and at worst the one that thinks it is already 'most recent' will just be 1 step behind.

If this *does* seem to fix the problem, my guess is that it only does so by playing tricks with threads and the GIL, and causing sync points. (aka, by accident)

Revision history for this message
John A Meinel (jameinel) wrote :

Check out: lp:///~jameinel/bzr/lru_locking

It still needs tests, and possibly 'live-fire' testing, hammering the cache structure with lots of threads. However, I'm fairly sure that I've gotten a minimal locked section (we lock as little as possible), while still maintaining correctness.

LRUCache.add() is not specifically thread-safe if you have cleanups and are adding different values for the same key. We might want to consider putting a big lock around all of add() for that case, but callers could also do that...

The main reason I poked at it was because you can't get 'fine-grained' locking around the LRU double-linked-list unless it is implemented in the cache itself.

414. By Matt Nordhoff

Set Cache-Control and Expires headers on static pages. (John Arbash Meinel)

Revision history for this message
Max Kanat-Alexander (mkanat) wrote :

Okay, John is right--the bug is not fixed. I just ran into it on bzr.mozilla.org again.

For loggerhead, a quick workaround is to make the lock global instead of a per-instance item.

We do have cleanups, and I suppose it's possible for us to be updating an existing branch with a new cache, right? So I think we'll need this lock in loggerhead anyway.

review: Needs Fixing
415. By Max Kanat-Alexander

jam rightly pointed out that the RevInfoMemoryCache lock needs to be global

Unmerged revisions

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'loggerhead/history.py'
--- loggerhead/history.py 2010-04-14 17:54:32 +0000
+++ loggerhead/history.py 2010-04-23 02:53:12 +0000
@@ -168,6 +168,9 @@
168168
169 def __init__(self, cache):169 def __init__(self, cache):
170 self._cache = cache170 self._cache = cache
171 # lru_cache is not thread-safe, so we need to lock all accesses.
172 # It is even modified when doing a get() on it.
173 self._lock = threading.RLock()
171174
172 def get(self, key, revid):175 def get(self, key, revid):
173 """Return the data associated with `key`, subject to a revid check.176 """Return the data associated with `key`, subject to a revid check.
@@ -175,7 +178,11 @@
175 If a value was stored under `key`, with the same revid, return it.178 If a value was stored under `key`, with the same revid, return it.
176 Otherwise return None.179 Otherwise return None.
177 """180 """
178 cached = self._cache.get(key)181 self._lock.acquire()
182 try:
183 cached = self._cache.get(key)
184 finally:
185 self._lock.release()
179 if cached is None:186 if cached is None:
180 return None187 return None
181 stored_revid, data = cached188 stored_revid, data = cached
@@ -187,7 +194,11 @@
187 def set(self, key, revid, data):194 def set(self, key, revid, data):
188 """Store `data` under `key`, to be checked against `revid` on get().195 """Store `data` under `key`, to be checked against `revid` on get().
189 """196 """
190 self._cache[key] = (revid, data)197 self._lock.acquire()
198 try:
199 self._cache[key] = (revid, data)
200 finally:
201 self._lock.release()
191202
192# Used to store locks that prevent multiple threads from building a 203# Used to store locks that prevent multiple threads from building a
193# revision graph for the same branch at the same time, because that can204# revision graph for the same branch at the same time, because that can

Subscribers

People subscribed via source and target branches