Merge lp:~lifeless/bzr/bug-514090 into lp:bzr/2.1

Proposed by Robert Collins
Status: Merged
Merged at revision: not available
Proposed branch: lp:~lifeless/bzr/bug-514090
Merge into: lp:bzr/2.1
Diff against target: 173 lines (+59/-20)
3 files modified
NEWS (+18/-0)
bzrlib/chk_map.py (+35/-14)
bzrlib/tests/test_chk_map.py (+6/-6)
To merge this branch: bzr merge lp:~lifeless/bzr/bug-514090
Reviewer Review Type Date Requested Status
Martin Pool Approve
Review via email: mp+18263@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Robert Collins (lifeless) wrote :

Yo. Thread race fixness FTW.

Revision history for this message
Martin Pool (mbp) wrote :

Makes sense as an emergency fix.

It would be nice to hang this off a specific object scope to be safer, or to rely on the app serializing access to a Repository.

review: Approve
Revision history for this message
Martin Packman (gz) wrote :

A small note, there's been a long-standing bug in both mod_python and mod_wsgi where threading.local() objects only last for a single request:
<http://code.google.com/p/modwsgi/issues/detail?id=120>

I presume however this doesn't affect launchpad, and would at worst just make the cache useless.

Revision history for this message
Robert Collins (lifeless) wrote :

we run with paste.deploy. Do you know if this affects that?

mod_wsgi being affected may cause this to hurt smart-server-over-http
users, but OTOH the cache shouldn't be very important between requests
anyway, as we try very hard to do all the heavy lifting in a single
smart verb.

Thanks a lot for mentioning this!

-Rob

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'NEWS'
2--- NEWS 2010-01-29 09:13:30 +0000
3+++ NEWS 2010-01-29 15:23:14 +0000
4@@ -5,6 +5,24 @@
5 .. contents:: List of Releases
6 :depth: 1
7
8+bzr 2.1.0 (not released yet)
9+############################
10+
11+:Codename:
12+:2.1.0:
13+
14+Bug Fixes
15+*********
16+
17+* Using the ``bzrlib.chk_map`` module from within multiple threads at the
18+ same time was broken due to race conditions with a module level page
19+ cache. This shows up as a KeyError in the ``bzrlib.lru_cache`` code with
20+ ``bzrlib.chk_map`` in the backtrace, and can be triggered without using
21+ the same high level objects such as ``bzrlib.repository.Repository``
22+ from different threads. chk_map now uses a thread local cache which may
23+ increase memory pressure on processes using threads.
24+ (Robert Collins, John Arbash Meinel, #514090)
25+
26 bzr 2.1.0rc2
27 ############
28
29
30=== modified file 'bzrlib/chk_map.py'
31--- bzrlib/chk_map.py 2009-11-02 17:27:52 +0000
32+++ bzrlib/chk_map.py 2010-01-29 15:23:14 +0000
33@@ -38,6 +38,7 @@
34 """
35
36 import heapq
37+import threading
38
39 from bzrlib import lazy_import
40 lazy_import.lazy_import(globals(), """
41@@ -59,11 +60,31 @@
42 # If each line is 50 bytes, and you have 255 internal pages, with 255-way fan
43 # out, it takes 3.1MB to cache the layer.
44 _PAGE_CACHE_SIZE = 4*1024*1024
45-# We are caching bytes so len(value) is perfectly accurate
46-_page_cache = lru_cache.LRUSizeCache(_PAGE_CACHE_SIZE)
47+# Per thread caches for 2 reasons:
48+# - in the server we may be serving very different content, so we get less
49+# cache thrashing.
50+# - we avoid locking on every cache lookup.
51+_thread_caches = threading.local()
52+# The page cache.
53+_thread_caches.page_cache = None
54+
55+def _get_cache():
56+ """Get the per-thread page cache.
57+
58+ We need a function to do this because in a new thread the _thread_caches
59+ threading.local object does not have the cache initialized yet.
60+ """
61+ page_cache = getattr(_thread_caches, 'page_cache', None)
62+ if page_cache is None:
63+ # We are caching bytes so len(value) is perfectly accurate
64+ page_cache = lru_cache.LRUSizeCache(_PAGE_CACHE_SIZE)
65+ _thread_caches.page_cache = page_cache
66+ return page_cache
67+
68
69 def clear_cache():
70- _page_cache.clear()
71+ _get_cache().clear()
72+
73
74 # If a ChildNode falls below this many bytes, we check for a remap
75 _INTERESTING_NEW_SIZE = 50
76@@ -161,11 +182,11 @@
77
78 def _read_bytes(self, key):
79 try:
80- return _page_cache[key]
81+ return _get_cache()[key]
82 except KeyError:
83 stream = self._store.get_record_stream([key], 'unordered', True)
84 bytes = stream.next().get_bytes_as('fulltext')
85- _page_cache[key] = bytes
86+ _get_cache()[key] = bytes
87 return bytes
88
89 def _dump_tree(self, include_keys=False):
90@@ -901,7 +922,7 @@
91 bytes = ''.join(lines)
92 if len(bytes) != self._current_size():
93 raise AssertionError('Invalid _current_size')
94- _page_cache.add(self._key, bytes)
95+ _get_cache().add(self._key, bytes)
96 return [self._key]
97
98 def refs(self):
99@@ -1143,7 +1164,7 @@
100 found_keys = set()
101 for key in keys:
102 try:
103- bytes = _page_cache[key]
104+ bytes = _get_cache()[key]
105 except KeyError:
106 continue
107 else:
108@@ -1174,7 +1195,7 @@
109 prefix, node_key_filter = keys[record.key]
110 node_and_filters.append((node, node_key_filter))
111 self._items[prefix] = node
112- _page_cache.add(record.key, bytes)
113+ _get_cache().add(record.key, bytes)
114 for info in node_and_filters:
115 yield info
116
117@@ -1300,7 +1321,7 @@
118 lines.append(serialised[prefix_len:])
119 sha1, _, _ = store.add_lines((None,), (), lines)
120 self._key = StaticTuple("sha1:" + sha1,).intern()
121- _page_cache.add(self._key, ''.join(lines))
122+ _get_cache().add(self._key, ''.join(lines))
123 yield self._key
124
125 def _search_key(self, key):
126@@ -1489,11 +1510,11 @@
127 self._state = None
128
129 def _read_nodes_from_store(self, keys):
130- # We chose not to use _page_cache, because we think in terms of records
131- # to be yielded. Also, we expect to touch each page only 1 time during
132- # this code. (We may want to evaluate saving the raw bytes into the
133- # page cache, which would allow a working tree update after the fetch
134- # to not have to read the bytes again.)
135+ # We chose not to use _get_cache(), because we think in
136+ # terms of records to be yielded. Also, we expect to touch each page
137+ # only 1 time during this code. (We may want to evaluate saving the
138+ # raw bytes into the page cache, which would allow a working tree
139+ # update after the fetch to not have to read the bytes again.)
140 as_st = StaticTuple.from_sequence
141 stream = self._store.get_record_stream(keys, 'unordered', True)
142 for record in stream:
143
144=== modified file 'bzrlib/tests/test_chk_map.py'
145--- bzrlib/tests/test_chk_map.py 2009-10-21 20:53:21 +0000
146+++ bzrlib/tests/test_chk_map.py 2010-01-29 15:23:14 +0000
147@@ -905,7 +905,7 @@
148 # Unmapping the new node will check the existing nodes to see if they
149 # would fit.
150 # Clear the page cache so we ensure we have to read all the children
151- chk_map._page_cache.clear()
152+ chk_map.clear_cache()
153 chkmap.unmap(('aad',))
154 self.assertIsInstance(chkmap._root_node._items['aaa'], LeafNode)
155 self.assertIsInstance(chkmap._root_node._items['aab'], LeafNode)
156@@ -945,12 +945,12 @@
157 # Now clear the page cache, and only include 2 of the children in the
158 # cache
159 aab_key = chkmap._root_node._items['aab']
160- aab_bytes = chk_map._page_cache[aab_key]
161+ aab_bytes = chk_map._get_cache()[aab_key]
162 aac_key = chkmap._root_node._items['aac']
163- aac_bytes = chk_map._page_cache[aac_key]
164- chk_map._page_cache.clear()
165- chk_map._page_cache[aab_key] = aab_bytes
166- chk_map._page_cache[aac_key] = aac_bytes
167+ aac_bytes = chk_map._get_cache()[aac_key]
168+ chk_map.clear_cache()
169+ chk_map._get_cache()[aab_key] = aab_bytes
170+ chk_map._get_cache()[aac_key] = aac_bytes
171
172 # Unmapping the new node will check the nodes from the page cache
173 # first, and not have to read in 'aaa'

Subscribers

People subscribed via source and target branches