Merge lp:~vila/bzr/525571-lock-bazaar-conf-files into lp:bzr

Proposed by Vincent Ladeuil
Status: Superseded
Proposed branch: lp:~vila/bzr/525571-lock-bazaar-conf-files
Merge into: lp:bzr
Diff against target: 779 lines (+360/-87) (has conflicts)
6 files modified
NEWS (+15/-0)
bzrlib/builtins.py (+20/-7)
bzrlib/config.py (+87/-22)
bzrlib/tests/blackbox/test_break_lock.py (+44/-20)
bzrlib/tests/test_commands.py (+1/-1)
bzrlib/tests/test_config.py (+193/-37)
Text conflict in NEWS
To merge this branch: bzr merge lp:~vila/bzr/525571-lock-bazaar-conf-files
Reviewer Review Type Date Requested Status
Robert Collins (community) Needs Fixing
Review via email: mp+28764@code.launchpad.net

This proposal has been superseded by a proposal from 2010-06-30.

Description of the change

This implements a write lock on LocationConfig and GlobalConfig and add support for them in break-lock
as required for bug #525571.

There is no user nor dev visible change but I'll welcome feedback from plugin authors.

There is a new bzrlib.config.LockableConfig that plugins authors using config files in ~/.bazaar may want to inherit (as LocationConfig and GlobalConfig do now).

I had to do some cleanup in the tests as modifying the model made quite a few of them fail
(congrats to test authors: failing tests are good tests ! :)

So I made a bit more cleanup than strictly necessary (during failure analysis),
my apologies to the reviewers.

To post a comment you must log in.
Revision history for this message
John A Meinel (jameinel) wrote :

-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1

Vincent Ladeuil wrote:
> Vincent Ladeuil has proposed merging lp:~vila/bzr/525571-lock-bazaar-conf-files into lp:bzr.
>
> Requested reviews:
> bzr-core (bzr-core)
> Related bugs:
> #525571 No locking when updating files in ~/.bazaar
> https://bugs.launchpad.net/bugs/525571
>
>
> This implements a write lock on LocationConfig and GlobalConfig and add support for them in break-lock
> as required for bug #525571.
>
> There is no user nor dev visible change but I'll welcome feedback from plugin authors.
>
> There is a new bzrlib.config.LockableConfig that plugins authors using config files in ~/.bazaar may want to inherit (as LocationConfig and GlobalConfig do now).
>
> I had to do some cleanup in the tests as modifying the model made quite a few of them fail
> (congrats to test authors: failing tests are good tests ! :)
>
> So I made a bit more cleanup than strictly necessary (during failure analysis),
> my apologies to the reviewers.
>

As a comment, without having really read the code thoroughly.

How does this handle stuff like 2 branches locking concurrently
locations.conf. I don't know how often we do it internally, though.

I think lots of filesystem locks on the bazaar directory could adversely
affect performance on Windows. IME locking isn't too expensive if you do
it 1 or 2 times. But if you lock and unlock on every attribute that gets
set, then it probably starts to be an issue.

On a Windows host:
  $ TIMEIT -s "b = Branch.open('.')" "b.lock_write(); b.unlock()"
  10.5msec

On an Ubuntu VM on the same machine:
  $ TIMEIT -s "b = Branch.open('.')" "b.lock_write(); b.unlock()"
  1.55msec

John
=:->
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.9 (Cygwin)
Comment: Using GnuPG with Mozilla - http://enigmail.mozdev.org/

iEYEARECAAYFAkwqMNkACgkQJdeBCYSNAAO7WACfYZOte5LfqA4Ro4J6U/3ZA2Cf
ZhkAoLy3d0+tQjcgx047AYI0sJMiZlfY
=mMJj
-----END PGP SIGNATURE-----

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

07:04 -!- guilhembi
[~<email address hidden>] has quit
[Quit: Client exiting]
07:11 < lifeless> vila:
07:11 < lifeless> + __doc__ = """\
07:11 < lifeless> + Break a dead lock on a repository, branch,
working directory or config file.
07:11 < lifeless> I'd prefer to see that spelt as
07:11 < lifeless> __doc__ = \
07:11 < lifeless> """Break...
07:12 < lifeless> because having to actually think about what you were
escaping hurt my brain

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

Meta: This seems like a case where two threads would be nice:
a) fix the tests that are currently a bit gratuitous.
b) make the behaviour change

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

Ok, actual review stuff:
 the docstring layout is wrong, please nuke the \.

We should check for branches first, not config files, because branch locks are the common case and break-lock doesn't need to be slow.

This change is suspicous:

152 def _write_config_file(self):
153 - f = file(self._get_filename(), "wb")
154 + fname = self._get_filename()
155 + conf_dir = os.path.dirname(fname)
156 + ensure_config_dir_exists(conf_dir)
157 + f = file(fname, "wb")
158 try:
159 - osutils.copy_ownership_from_path(f.name)
160 + osutils.copy_ownership_from_path(fname)
161 self._get_parser().write(f)
162 finally:
163 f.close()
164

It appears to be adding a new stat/mkdir check, at the wrong layer.

missing VWS:

172 + """
173 + lock_name = 'lock'

Ditto here:

181 + def lock_write(self, token=None):
182 + if self.lock is None:
183 + ensure_config_dir_exists(self.dir)
184 + self._lock = lockdir.LockDir(self.transport, self.lock_name)
185 + self._lock.lock_write(token)
186 + return lock.LogicalLockResult(self.unlock)

If the dir doesn't exist, something is wrong - we really shouldn't have gotten this far without it, should we?

Docstrings!!!

I'll review the tests after the core is good.

Uh, you raise NoLockDir, but use it to mean 'A config directory that is not locked' which is very odd. Please consider something that means what you need. Also see my ordering suggestion which may help you not to need this at all.

review: Needs Fixing
Revision history for this message
Vincent Ladeuil (vila) wrote :

> Ok, actual review stuff:
> the docstring layout is wrong, please nuke the \.
>
> We should check for branches first, not config files, because branch locks are
> the common case and break-lock doesn't need to be slow.

break_lock() for wt, branch, repo gives no indication about whether it fails or succeeds.
Trying the conf files first was the easiest.

Regarding performance, I think we don't care at all, the user is supposed to first check that
the lock is not hold by a running process (or someone else) which requires seconds in the best case
or occur long after the lock has been created.

>
> This change is suspicous:
>
> 152 def _write_config_file(self):
> 153 - f = file(self._get_filename(), "wb")
> 154 + fname = self._get_filename()
> 155 + conf_dir = os.path.dirname(fname)
> 156 + ensure_config_dir_exists(conf_dir)
> 157 + f = file(fname, "wb")
> 158 try:
> 159 - osutils.copy_ownership_from_path(f.name)
> 160 + osutils.copy_ownership_from_path(fname)
> 161 self._get_parser().write(f)
> 162 finally:
> 163 f.close()
> 164
>
> It appears to be adding a new stat/mkdir check, at the wrong layer.

No adding there, code duplication removal instead, ensure_config_dir_exists() was called anyway.

>
> missing VWS:
>
> 172 + """
> 173 + lock_name = 'lock'

Fixed.
>
>
>
> Ditto here:
>
> 181 + def lock_write(self, token=None):
> 182 + if self.lock is None:
> 183 + ensure_config_dir_exists(self.dir)
> 184 + self._lock = lockdir.LockDir(self.transport,
> self.lock_name)
> 185 + self._lock.lock_write(token)
> 186 + return lock.LogicalLockResult(self.unlock)
>
> If the dir doesn't exist, something is wrong - we really shouldn't have gotten
> this far without it, should we?

When the config file didn't exist, the config dir needs to be created.

> Docstrings!!!

A bit terse but I will add some.

> Uh, you raise NoLockDir, but use it to mean 'A config directory that is not
> locked' which is very odd.
> Please consider something that means what you need.

I need something that means: "Oh, I wanted to break a lock there but there is no lock dir there,
surely I can't break a lock in that case". I fail to see the oddness :-/

Revision history for this message
Vincent Ladeuil (vila) wrote :

> As a comment, without having really read the code thoroughly.
>
> How does this handle stuff like 2 branches locking concurrently
> locations.conf. I don't know how often we do it internally, though.

TestLockableConfig.test_writes_are_serialized

>
> I think lots of filesystem locks on the bazaar directory could adversely
> affect performance on Windows. IME locking isn't too expensive if you do
> it 1 or 2 times. But if you lock and unlock on every attribute that gets
> set, then it probably starts to be an issue.

The actual code is not correct, it allows concurrent writers. If higher levels do too many updates of config variables it's a problem in the higher levels, we could imagine holding the updates until the last one, but
this can't addressed here.

Correctness comes before performance, there are many things we can do to address performance but it's way out of scope for this proposal IMHO.

>
> On a Windows host:
> $ TIMEIT -s "b = Branch.open('.')" "b.lock_write(); b.unlock()"
> 10.5msec
>
> On an Ubuntu VM on the same machine:
> $ TIMEIT -s "b = Branch.open('.')" "b.lock_write(); b.unlock()"
> 1.55msec

Thanks, good data point, but still, I haven't seen code doing massive updates of config variables either...

5327. By Vincent Ladeuil

Revert the lock scope to a sane value.

* bzrlib/tests/test_config.py:
(TestLockableConfig.test_writes_are_serialized)
(TestLockableConfig.test_read_while_writing): Fix the fallouts.

* bzrlib/config.py:
(LockableConfig): Wrong idea, the lock needs to be taken arond the
whole value update call, reducing the lock scope to
_write_config_file exclude the config file re-read.
(GlobalConfig.set_user_option, GlobalConfig.set_alias)
(GlobalConfig.unset_alias, LocationConfig.set_user_option): These
methods needs to be decorated with needs_write_lock to enforce the
design constraints (lock, re-read config, set new value, write
config, unlock).

5328. By Vincent Ladeuil

Fix wrong bug number and clarify NEWS entries.

5329. By Vincent Ladeuil

Fix docstring.

5330. By Vincent Ladeuil

Further clarify NEWS entry.

5331. By Vincent Ladeuil

Add a test to help LockableConfig daughter classes identify methods that needs to be decorated.

5332. By Vincent Ladeuil

Implement the --conf option for break-lock as per lifeless suggestion.

* bzrlib/errors.py:
(NoLockDir): Useless, deleted.

* bzrlib/config.py:
(LockableConfig.unlock): NoLockDir is useless, break_lock()
silenty succeeds if the directory doesn't exist.

* bzrlib/tests/blackbox/test_break_lock.py:
Tweak the tests.

* bzrlib/builtins.py:
(cmd_break_lock): Fix docstring, add a --conf option to deal with
config files.

5333. By Vincent Ladeuil

Final cleanup.

5334. By Vincent Ladeuil

Clarify lock scope.

5335. By Vincent Ladeuil

Use --config instead of --conf for break-lock.

Unmerged revisions

5335. By Vincent Ladeuil

Use --config instead of --conf for break-lock.

5334. By Vincent Ladeuil

Clarify lock scope.

5333. By Vincent Ladeuil

Final cleanup.

5332. By Vincent Ladeuil

Implement the --conf option for break-lock as per lifeless suggestion.

* bzrlib/errors.py:
(NoLockDir): Useless, deleted.

* bzrlib/config.py:
(LockableConfig.unlock): NoLockDir is useless, break_lock()
silenty succeeds if the directory doesn't exist.

* bzrlib/tests/blackbox/test_break_lock.py:
Tweak the tests.

* bzrlib/builtins.py:
(cmd_break_lock): Fix docstring, add a --conf option to deal with
config files.

5331. By Vincent Ladeuil

Add a test to help LockableConfig daughter classes identify methods that needs to be decorated.

5330. By Vincent Ladeuil

Further clarify NEWS entry.

5329. By Vincent Ladeuil

Fix docstring.

5328. By Vincent Ladeuil

Fix wrong bug number and clarify NEWS entries.

5327. By Vincent Ladeuil

Revert the lock scope to a sane value.

* bzrlib/tests/test_config.py:
(TestLockableConfig.test_writes_are_serialized)
(TestLockableConfig.test_read_while_writing): Fix the fallouts.

* bzrlib/config.py:
(LockableConfig): Wrong idea, the lock needs to be taken arond the
whole value update call, reducing the lock scope to
_write_config_file exclude the config file re-read.
(GlobalConfig.set_user_option, GlobalConfig.set_alias)
(GlobalConfig.unset_alias, LocationConfig.set_user_option): These
methods needs to be decorated with needs_write_lock to enforce the
design constraints (lock, re-read config, set new value, write
config, unlock).

5326. By Vincent Ladeuil

Add some comments.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'NEWS'
2--- NEWS 2010-06-28 21:45:10 +0000
3+++ NEWS 2010-06-30 14:21:34 +0000
4@@ -36,6 +36,15 @@
5 New Features
6 ************
7
8+* ``bzr break-lock`` now defines a --config parameter that is required
9+ to break locks on config files. (Vincent Ladeuil, #525571)
10+
11+* ``bzrlib.config.LockableConfig`` is a base class for config files that
12+ needs to be protected against multiple writers. All methods that
13+ change a configuration variable value must be decorated with
14+ @needs_write_lock (set_option() for example).
15+ (Vincent Ladeuil, #525571)
16+
17 * Support ``--directory`` option for a number of additional commands:
18 conflicts, merge-directive, missing, resolve, shelve, switch,
19 unshelve, whoami. (Martin von Gagern, #527878)
20@@ -62,12 +71,18 @@
21 or pull location in locations.conf or branch.conf.
22 (Gordon Tyler, #534787)
23
24+<<<<<<< TREE
25 * ``BzrDir.find_branches`` should ignore branches with missing repositories.
26 (Marius Kruger, Robert Collins)
27
28 * ``BzrDir.find_bzrdirs`` should ignore dirs that raises PermissionDenied.
29 (Marius Kruger, Robert Collins)
30
31+=======
32+* Configuration files in ``${BZR_HOME}`` are now protected
33+ against concurrent writers. (Vincent Ladeuil, #525571)
34+
35+>>>>>>> MERGE-SOURCE
36 * Ensure that wrong path specifications in ``BZR_PLUGINS_AT`` display
37 proper error messages. (Vincent Ladeuil, #591215)
38
39
40=== modified file 'bzrlib/builtins.py'
41--- bzrlib/builtins.py 2010-06-23 08:19:28 +0000
42+++ bzrlib/builtins.py 2010-06-30 14:21:34 +0000
43@@ -4830,7 +4830,10 @@
44
45
46 class cmd_break_lock(Command):
47- __doc__ = """Break a dead lock on a repository, branch or working directory.
48+ __doc__ = """Break a dead lock.
49+
50+ This command breaks a lock on a repository, branch, working directory or
51+ config file.
52
53 CAUTION: Locks should only be broken when you are sure that the process
54 holding the lock has been stopped.
55@@ -4841,17 +4844,27 @@
56 :Examples:
57 bzr break-lock
58 bzr break-lock bzr+ssh://example.com/bzr/foo
59+ bzr break-lock --conf ~/.bazaar
60 """
61+
62 takes_args = ['location?']
63+ takes_options = [
64+ Option('conf',
65+ help='LOCATION is a directory containing configuration files.'),
66+ ]
67
68- def run(self, location=None, show=False):
69+ def run(self, location=None, conf=False):
70 if location is None:
71 location = u'.'
72- control, relpath = bzrdir.BzrDir.open_containing(location)
73- try:
74- control.break_lock()
75- except NotImplementedError:
76- pass
77+ if conf:
78+ c = config.LockableConfig(lambda : location)
79+ c.break_lock()
80+ else:
81+ control, relpath = bzrdir.BzrDir.open_containing(location)
82+ try:
83+ control.break_lock()
84+ except NotImplementedError:
85+ pass
86
87
88 class cmd_wait_until_signalled(Command):
89
90=== modified file 'bzrlib/config.py'
91--- bzrlib/config.py 2010-06-02 04:50:35 +0000
92+++ bzrlib/config.py 2010-06-30 14:21:34 +0000
93@@ -62,9 +62,11 @@
94 up=pull
95 """
96
97+from cStringIO import StringIO
98 import os
99 import sys
100
101+from bzrlib.decorators import needs_write_lock
102 from bzrlib.lazy_import import lazy_import
103 lazy_import(globals(), """
104 import errno
105@@ -76,11 +78,14 @@
106 from bzrlib import (
107 debug,
108 errors,
109+ lock,
110+ lockdir,
111 mail_client,
112 osutils,
113 registry,
114 symbol_versioning,
115 trace,
116+ transport,
117 ui,
118 urlutils,
119 win32utils,
120@@ -356,18 +361,30 @@
121 self._get_filename = get_filename
122 self._parser = None
123
124- def _get_parser(self, file=None):
125+ def _get_parser(self, infile=None):
126 if self._parser is not None:
127 return self._parser
128- if file is None:
129- input = self._get_filename()
130- else:
131- input = file
132+ # Do we have a file name for the config file
133+ if self._get_filename is None:
134+ fname = None
135+ else:
136+ fname = self._get_filename()
137+ # Do we have a content for the config file ?
138+ if infile is None:
139+ fname_or_content = fname
140+ else:
141+ fname_or_content = infile
142+ # Build the ConfigObj
143+ p = None
144 try:
145- self._parser = ConfigObj(input, encoding='utf-8')
146+ p = ConfigObj(fname_or_content, encoding='utf-8')
147 except configobj.ConfigObjError, e:
148 raise errors.ParseConfigError(e.errors, e.config.filename)
149- return self._parser
150+ if p is not None and fname is not None:
151+ # Make sure p.reload() will use the right file name
152+ p.filename = fname
153+ self._parser = p
154+ return p
155
156 def _get_matching_sections(self):
157 """Return an ordered list of (section_name, extra_path) pairs.
158@@ -478,27 +495,76 @@
159 return self.get_user_option('nickname')
160
161 def _write_config_file(self):
162- f = file(self._get_filename(), "wb")
163+ fname = self._get_filename()
164+ conf_dir = os.path.dirname(fname)
165+ ensure_config_dir_exists(conf_dir)
166+ f = file(fname, "wb")
167 try:
168- osutils.copy_ownership_from_path(f.name)
169+ osutils.copy_ownership_from_path(fname)
170 self._get_parser().write(f)
171 finally:
172 f.close()
173
174
175-class GlobalConfig(IniBasedConfig):
176+class LockableConfig(IniBasedConfig):
177+ """A configuration needing explicit locking for access.
178+
179+ If several processes try to write the config file, the accesses need to be
180+ serialized.
181+ """
182+
183+ lock_name = 'lock'
184+
185+ def __init__(self, get_filename):
186+ super(LockableConfig, self).__init__(get_filename)
187+ self.dir = osutils.dirname(osutils.safe_unicode(self._get_filename()))
188+ self.transport = transport.get_transport(self.dir)
189+ self._lock = None
190+
191+ def lock_write(self, token=None):
192+ if self._lock is None:
193+ ensure_config_dir_exists(self.dir)
194+ self._lock = lockdir.LockDir(self.transport, self.lock_name)
195+ self._lock.lock_write(token)
196+ return lock.LogicalLockResult(self.unlock)
197+
198+ def unlock(self):
199+ self._lock.unlock()
200+
201+ def break_lock(self):
202+ if self._lock is None:
203+ self._lock = lockdir.LockDir(self.transport, self.lock_name)
204+ self._lock.break_lock()
205+
206+ def _write_config_file(self):
207+ if self._lock is None or not self._lock.is_held:
208+ # NB: if the following exception is raised it probably means a
209+ # missing @needs_write_lock decorator on one of the callers.
210+ raise errors.ObjectNotLocked(self)
211+ fname = self._get_filename()
212+ f = StringIO()
213+ p = self._get_parser()
214+ p.write(f)
215+ self.transport.put_bytes(os.path.basename(fname), f.getvalue())
216+ # Make sure p.reload() will use the right file name
217+ p.filename = fname
218+ osutils.copy_ownership_from_path(fname)
219+
220+
221+class GlobalConfig(LockableConfig):
222 """The configuration that should be used for a specific location."""
223
224- def get_editor(self):
225- return self._get_user_option('editor')
226-
227 def __init__(self):
228 super(GlobalConfig, self).__init__(config_filename)
229
230+ @needs_write_lock
231 def set_user_option(self, option, value):
232 """Save option and its value in the configuration."""
233 self._set_option(option, value, 'DEFAULT')
234
235+ def get_editor(self):
236+ return self._get_user_option('editor')
237+
238 def get_aliases(self):
239 """Return the aliases section."""
240 if 'ALIASES' in self._get_parser():
241@@ -506,10 +572,12 @@
242 else:
243 return {}
244
245+ @needs_write_lock
246 def set_alias(self, alias_name, alias_command):
247 """Save the alias in the configuration."""
248 self._set_option(alias_name, alias_command, 'ALIASES')
249
250+ @needs_write_lock
251 def unset_alias(self, alias_name):
252 """Unset an existing alias."""
253 aliases = self._get_parser().get('ALIASES')
254@@ -519,15 +587,13 @@
255 self._write_config_file()
256
257 def _set_option(self, option, value, section):
258- # FIXME: RBC 20051029 This should refresh the parser and also take a
259- # file lock on bazaar.conf.
260- conf_dir = os.path.dirname(self._get_filename())
261- ensure_config_dir_exists(conf_dir)
262+ if self._parser is not None:
263+ self._parser.reload()
264 self._get_parser().setdefault(section, {})[option] = value
265 self._write_config_file()
266
267
268-class LocationConfig(IniBasedConfig):
269+class LocationConfig(LockableConfig):
270 """A configuration object that gives the policy for a location."""
271
272 def __init__(self, location):
273@@ -643,6 +709,7 @@
274 if policy_key in self._get_parser()[section]:
275 del self._get_parser()[section][policy_key]
276
277+ @needs_write_lock
278 def set_user_option(self, option, value, store=STORE_LOCATION):
279 """Save option and its value in the configuration."""
280 if store not in [STORE_LOCATION,
281@@ -650,10 +717,8 @@
282 STORE_LOCATION_APPENDPATH]:
283 raise ValueError('bad storage policy %r for %r' %
284 (store, option))
285- # FIXME: RBC 20051029 This should refresh the parser and also take a
286- # file lock on locations.conf.
287- conf_dir = os.path.dirname(self._get_filename())
288- ensure_config_dir_exists(conf_dir)
289+ if self._parser is not None:
290+ self._parser.reload()
291 location = self.location
292 if location.endswith('/'):
293 location = location[:-1]
294
295=== modified file 'bzrlib/tests/blackbox/test_break_lock.py'
296--- bzrlib/tests/blackbox/test_break_lock.py 2010-06-11 07:32:12 +0000
297+++ bzrlib/tests/blackbox/test_break_lock.py 2010-06-30 14:21:34 +0000
298@@ -18,17 +18,18 @@
299
300 import os
301
302-import bzrlib
303 from bzrlib import (
304+ branch,
305+ bzrdir,
306+ config,
307 errors,
308 lockdir,
309+ osutils,
310+ tests,
311 )
312-from bzrlib.branch import Branch
313-from bzrlib.bzrdir import BzrDir
314-from bzrlib.tests import TestCaseWithTransport
315-
316-
317-class TestBreakLock(TestCaseWithTransport):
318+
319+
320+class TestBreakLock(tests.TestCaseWithTransport):
321
322 # General principal for break-lock: All the elements that might be locked
323 # by a bzr operation on PATH, are candidates that break-lock may unlock.
324@@ -52,14 +53,14 @@
325 'repo/',
326 'repo/branch/',
327 'checkout/'])
328- bzrlib.bzrdir.BzrDir.create('master-repo').create_repository()
329- self.master_branch = bzrlib.bzrdir.BzrDir.create_branch_convenience(
330+ bzrdir.BzrDir.create('master-repo').create_repository()
331+ self.master_branch = bzrdir.BzrDir.create_branch_convenience(
332 'master-repo/master-branch')
333- bzrlib.bzrdir.BzrDir.create('repo').create_repository()
334- local_branch = bzrlib.bzrdir.BzrDir.create_branch_convenience('repo/branch')
335+ bzrdir.BzrDir.create('repo').create_repository()
336+ local_branch = bzrdir.BzrDir.create_branch_convenience('repo/branch')
337 local_branch.bind(self.master_branch)
338- checkoutdir = bzrlib.bzrdir.BzrDir.create('checkout')
339- bzrlib.branch.BranchReferenceFormat().initialize(
340+ checkoutdir = bzrdir.BzrDir.create('checkout')
341+ branch.BranchReferenceFormat().initialize(
342 checkoutdir, target_branch=local_branch)
343 self.wt = checkoutdir.create_workingtree()
344
345@@ -73,7 +74,7 @@
346 # however, we dont test breaking the working tree because we
347 # cannot accurately do so right now: the dirstate lock is held
348 # by an os lock, and we need to spawn a separate process to lock it
349- # thne kill -9 it.
350+ # then kill -9 it.
351 # sketch of test:
352 # lock most of the dir:
353 self.wt.branch.lock_write()
354@@ -82,22 +83,45 @@
355 # we need 5 yes's - wt, branch, repo, bound branch, bound repo.
356 self.run_bzr('break-lock checkout', stdin="y\ny\ny\ny\n")
357 # a new tree instance should be lockable
358- branch = bzrlib.branch.Branch.open('checkout')
359- branch.lock_write()
360- branch.unlock()
361+ b = branch.Branch.open('checkout')
362+ b.lock_write()
363+ b.unlock()
364 # and a new instance of the master branch
365- mb = branch.get_master_branch()
366+ mb = b.get_master_branch()
367 mb.lock_write()
368 mb.unlock()
369 self.assertRaises(errors.LockBroken, self.wt.unlock)
370 self.assertRaises(errors.LockBroken, self.master_branch.unlock)
371
372
373-class TestBreakLockOldBranch(TestCaseWithTransport):
374+class TestBreakLockOldBranch(tests.TestCaseWithTransport):
375
376 def test_break_lock_format_5_bzrdir(self):
377 # break lock on a format 5 bzrdir should just return
378- self.make_branch_and_tree('foo', format=bzrlib.bzrdir.BzrDirFormat5())
379+ self.make_branch_and_tree('foo', format=bzrdir.BzrDirFormat5())
380 out, err = self.run_bzr('break-lock foo')
381 self.assertEqual('', out)
382 self.assertEqual('', err)
383+
384+
385+class TestConfigBreakLock(tests.TestCaseWithTransport):
386+
387+ def create_pending_lock(self):
388+ def config_file_name():
389+ return './my.conf'
390+ self.build_tree_contents([(config_file_name(), '[DEFAULT]\none=1\n')])
391+ conf = config.LockableConfig(config_file_name)
392+ conf.lock_write()
393+ return conf
394+
395+ def test_create_pending_lock(self):
396+ conf = self.create_pending_lock()
397+ self.addCleanup(conf.unlock)
398+ self.assertTrue(conf._lock.is_held)
399+
400+ def test_break_lock(self):
401+ conf = self.create_pending_lock()
402+ self.run_bzr('break-lock --conf %s'
403+ % osutils.dirname(conf._get_filename()),
404+ stdin="y\n")
405+ self.assertRaises(errors.LockBroken, conf.unlock)
406
407=== modified file 'bzrlib/tests/test_commands.py'
408--- bzrlib/tests/test_commands.py 2010-05-27 21:16:48 +0000
409+++ bzrlib/tests/test_commands.py 2010-06-30 14:21:34 +0000
410@@ -97,7 +97,7 @@
411 def _get_config(self, config_text):
412 my_config = config.GlobalConfig()
413 config_file = StringIO(config_text.encode('utf-8'))
414- my_config._parser = my_config._get_parser(file=config_file)
415+ my_config._parser = my_config._get_parser(infile=config_file)
416 return my_config
417
418 def test_simple(self):
419
420=== modified file 'bzrlib/tests/test_config.py'
421--- bzrlib/tests/test_config.py 2010-04-23 07:15:23 +0000
422+++ bzrlib/tests/test_config.py 2010-06-30 14:21:34 +0000
423@@ -19,6 +19,7 @@
424 from cStringIO import StringIO
425 import os
426 import sys
427+import threading
428
429 #import bzrlib specific imports here
430 from bzrlib import (
431@@ -38,6 +39,32 @@
432 from bzrlib.util.configobj import configobj
433
434
435+def lockable_config_scenarios():
436+ return [
437+ ('global',
438+ {'config_file_name': config.config_filename,
439+ 'config_class': config.GlobalConfig,
440+ 'config_args': [],
441+ 'config_section': 'DEFAULT'}),
442+ ('locations',
443+ {'config_file_name': config.locations_config_filename,
444+ 'config_class': config.LocationConfig,
445+ 'config_args': ['.'],
446+ 'config_section': '.'}),]
447+
448+
449+def load_tests(standard_tests, module, loader):
450+ suite = loader.suiteClass()
451+
452+ lc_tests, remaining_tests = tests.split_suite_by_condition(
453+ standard_tests, tests.condition_isinstance((
454+ TestLockableConfig,
455+ )))
456+ tests.multiply_tests(lc_tests, lockable_config_scenarios(), suite)
457+ suite.addTest(remaining_tests)
458+ return suite
459+
460+
461 sample_long_alias="log -r-15..-1 --line"
462 sample_config_text = u"""
463 [DEFAULT]
464@@ -129,6 +156,9 @@
465 self._calls.append(('keys',))
466 return []
467
468+ def reload(self):
469+ self._calls.append(('reload',))
470+
471 def write(self, arg):
472 self._calls.append(('write',))
473
474@@ -371,7 +401,7 @@
475
476 def make_config_parser(self, s):
477 conf = config.IniBasedConfig(None)
478- parser = conf._get_parser(file=StringIO(s.encode('utf-8')))
479+ parser = conf._get_parser(infile=StringIO(s.encode('utf-8')))
480 return conf, parser
481
482
483@@ -384,16 +414,144 @@
484 config_file = StringIO(sample_config_text.encode('utf-8'))
485 my_config = config.IniBasedConfig(None)
486 self.failUnless(
487- isinstance(my_config._get_parser(file=config_file),
488+ isinstance(my_config._get_parser(infile=config_file),
489 configobj.ConfigObj))
490
491 def test_cached(self):
492 config_file = StringIO(sample_config_text.encode('utf-8'))
493 my_config = config.IniBasedConfig(None)
494- parser = my_config._get_parser(file=config_file)
495+ parser = my_config._get_parser(infile=config_file)
496 self.failUnless(my_config._get_parser() is parser)
497
498
499+class TestLockableConfig(tests.TestCaseInTempDir):
500+
501+ # Set by load_tests
502+ config_file_name = None
503+ config_class = None
504+ config_args = None
505+ config_section = None
506+
507+ def setUp(self):
508+ super(TestLockableConfig, self).setUp()
509+ config.ensure_config_dir_exists()
510+ text = '[%s]\none=1\ntwo=2\n' % (self.config_section,)
511+ self.build_tree_contents([(self.config_file_name(), text)])
512+
513+ def create_config(self):
514+ return self.config_class(*self.config_args)
515+
516+ def test_simple_read_access(self):
517+ c = self.create_config()
518+ self.assertEquals('1', c.get_user_option('one'))
519+
520+ def test_simple_write_access(self):
521+ c = self.create_config()
522+ c.set_user_option('one', 'one')
523+ self.assertEquals('one', c.get_user_option('one'))
524+
525+ def test_unlocked_config(self):
526+ c = self.create_config()
527+ self.assertRaises(errors.ObjectNotLocked, c._write_config_file)
528+
529+ def test_listen_to_the_last_speaker(self):
530+ c1 = self.create_config()
531+ c2 = self.create_config()
532+ c1.set_user_option('one', 'ONE')
533+ c2.set_user_option('two', 'TWO')
534+ self.assertEquals('ONE', c1.get_user_option('one'))
535+ self.assertEquals('TWO', c2.get_user_option('two'))
536+ # The second update respect the first one
537+ self.assertEquals('ONE', c2.get_user_option('one'))
538+
539+ def test_last_speaker_wins(self):
540+ # If the same config is not shared, the same variable modified twice
541+ # can only see a single result.
542+ c1 = self.create_config()
543+ c2 = self.create_config()
544+ c1.set_user_option('one', 'c1')
545+ c2.set_user_option('one', 'c2')
546+ self.assertEquals('c2', c2.get_user_option('one'))
547+ # The first modification is still available until another refresh
548+ # occur
549+ self.assertEquals('c1', c1.get_user_option('one'))
550+ c1.set_user_option('two', 'done')
551+ self.assertEquals('c2', c1.get_user_option('one'))
552+
553+ def test_writes_are_serialized(self):
554+ c1 = self.create_config()
555+ c2 = self.create_config()
556+
557+ # We spawn a thread that will pause *during* the write
558+ before_writing = threading.Event()
559+ after_writing = threading.Event()
560+ writing_done = threading.Event()
561+ c1_orig = c1._write_config_file
562+ def c1_write_config_file():
563+ before_writing.set()
564+ c1_orig()
565+ # The lock is held we wait for the main thread to decide when to
566+ # continue
567+ after_writing.wait()
568+ c1._write_config_file = c1_write_config_file
569+ def c1_set_option():
570+ c1.set_user_option('one', 'c1')
571+ writing_done.set()
572+ t1 = threading.Thread(target=c1_set_option)
573+ # Collect the thread after the test
574+ self.addCleanup(t1.join)
575+ # Be ready to unblock the thread if the test goes wrong
576+ self.addCleanup(after_writing.set)
577+ t1.start()
578+ before_writing.wait()
579+ self.assertTrue(c1._lock.is_held)
580+ self.assertRaises(errors.LockContention,
581+ c2.set_user_option, 'one', 'c2')
582+ self.assertEquals('c1', c1.get_user_option('one'))
583+ # Let the lock be released
584+ after_writing.set()
585+ writing_done.wait()
586+ c2.set_user_option('one', 'c2')
587+ self.assertEquals('c2', c2.get_user_option('one'))
588+
589+ def test_read_while_writing(self):
590+ c1 = self.create_config()
591+ # We spawn a thread that will pause *during* the write
592+ ready_to_write = threading.Event()
593+ do_writing = threading.Event()
594+ writing_done = threading.Event()
595+ c1_orig = c1._write_config_file
596+ def c1_write_config_file():
597+ ready_to_write.set()
598+ # The lock is held we wait for the main thread to decide when to
599+ # continue
600+ do_writing.wait()
601+ c1_orig()
602+ writing_done.set()
603+ c1._write_config_file = c1_write_config_file
604+ def c1_set_option():
605+ c1.set_user_option('one', 'c1')
606+ t1 = threading.Thread(target=c1_set_option)
607+ # Collect the thread after the test
608+ self.addCleanup(t1.join)
609+ # Be ready to unblock the thread if the test goes wrong
610+ self.addCleanup(do_writing.set)
611+ t1.start()
612+ # Ensure the thread is ready to write
613+ ready_to_write.wait()
614+ self.assertTrue(c1._lock.is_held)
615+ self.assertEquals('c1', c1.get_user_option('one'))
616+ # If we read during the write, we get the old value
617+ c2 = self.create_config()
618+ self.assertEquals('1', c2.get_user_option('one'))
619+ # Let the writing occur and ensure it occurred
620+ do_writing.set()
621+ writing_done.wait()
622+ # Now we get the updated value
623+ c3 = self.create_config()
624+ self.assertEquals('c1', c3.get_user_option('one'))
625+
626+
627 class TestGetUserOptionAs(TestIniConfig):
628
629 def test_get_user_option_as_bool(self):
630@@ -583,26 +741,26 @@
631 def test_user_id(self):
632 config_file = StringIO(sample_config_text.encode('utf-8'))
633 my_config = config.GlobalConfig()
634- my_config._parser = my_config._get_parser(file=config_file)
635+ my_config._parser = my_config._get_parser(infile=config_file)
636 self.assertEqual(u"Erik B\u00e5gfors <erik@bagfors.nu>",
637 my_config._get_user_id())
638
639 def test_absent_user_id(self):
640 config_file = StringIO("")
641 my_config = config.GlobalConfig()
642- my_config._parser = my_config._get_parser(file=config_file)
643+ my_config._parser = my_config._get_parser(infile=config_file)
644 self.assertEqual(None, my_config._get_user_id())
645
646 def test_configured_editor(self):
647 config_file = StringIO(sample_config_text.encode('utf-8'))
648 my_config = config.GlobalConfig()
649- my_config._parser = my_config._get_parser(file=config_file)
650+ my_config._parser = my_config._get_parser(infile=config_file)
651 self.assertEqual("vim", my_config.get_editor())
652
653 def test_signatures_always(self):
654 config_file = StringIO(sample_always_signatures)
655 my_config = config.GlobalConfig()
656- my_config._parser = my_config._get_parser(file=config_file)
657+ my_config._parser = my_config._get_parser(infile=config_file)
658 self.assertEqual(config.CHECK_NEVER,
659 my_config.signature_checking())
660 self.assertEqual(config.SIGN_ALWAYS,
661@@ -612,7 +770,7 @@
662 def test_signatures_if_possible(self):
663 config_file = StringIO(sample_maybe_signatures)
664 my_config = config.GlobalConfig()
665- my_config._parser = my_config._get_parser(file=config_file)
666+ my_config._parser = my_config._get_parser(infile=config_file)
667 self.assertEqual(config.CHECK_NEVER,
668 my_config.signature_checking())
669 self.assertEqual(config.SIGN_WHEN_REQUIRED,
670@@ -622,7 +780,7 @@
671 def test_signatures_ignore(self):
672 config_file = StringIO(sample_ignore_signatures)
673 my_config = config.GlobalConfig()
674- my_config._parser = my_config._get_parser(file=config_file)
675+ my_config._parser = my_config._get_parser(infile=config_file)
676 self.assertEqual(config.CHECK_ALWAYS,
677 my_config.signature_checking())
678 self.assertEqual(config.SIGN_NEVER,
679@@ -632,7 +790,7 @@
680 def _get_sample_config(self):
681 config_file = StringIO(sample_config_text.encode('utf-8'))
682 my_config = config.GlobalConfig()
683- my_config._parser = my_config._get_parser(file=config_file)
684+ my_config._parser = my_config._get_parser(infile=config_file)
685 return my_config
686
687 def test_gpg_signing_command(self):
688@@ -643,7 +801,7 @@
689 def _get_empty_config(self):
690 config_file = StringIO("")
691 my_config = config.GlobalConfig()
692- my_config._parser = my_config._get_parser(file=config_file)
693+ my_config._parser = my_config._get_parser(infile=config_file)
694 return my_config
695
696 def test_gpg_signing_command_unset(self):
697@@ -745,10 +903,11 @@
698 [('__init__', config.locations_config_filename(),
699 'utf-8')])
700 config.ensure_config_dir_exists()
701- #os.mkdir(config.config_dir())
702 f = file(config.branches_config_filename(), 'wb')
703- f.write('')
704- f.close()
705+ try:
706+ f.write('')
707+ finally:
708+ f.close()
709 oldparserclass = config.ConfigObj
710 config.ConfigObj = InstrumentedConfigObj
711 try:
712@@ -995,10 +1154,15 @@
713 else:
714 global_file = StringIO(global_config.encode('utf-8'))
715 branches_file = StringIO(sample_branches_text.encode('utf-8'))
716+ # Make sure the locations config can be reloaded
717+ config.ensure_config_dir_exists()
718+ f = file(config.locations_config_filename(), 'wb')
719+ try:
720+ f.write(branches_file.getvalue())
721+ finally:
722+ f.close
723 self.my_config = config.BranchConfig(FakeBranch(location))
724- # Force location config to use specified file
725 self.my_location_config = self.my_config._get_location_config()
726- self.my_location_config._get_parser(branches_file)
727 # Force global config to use specified file
728 self.my_config._get_global_config()._get_parser(global_file)
729
730@@ -1007,25 +1171,14 @@
731 record = InstrumentedConfigObj("foo")
732 self.my_location_config._parser = record
733
734- real_mkdir = os.mkdir
735- self.created = False
736- def checked_mkdir(path, mode=0777):
737- self.log('making directory: %s', path)
738- real_mkdir(path, mode)
739- self.created = True
740-
741- os.mkdir = checked_mkdir
742- try:
743- self.callDeprecated(['The recurse option is deprecated as of '
744- '0.14. The section "/a/c" has been '
745- 'converted to use policies.'],
746- self.my_config.set_user_option,
747- 'foo', 'bar', store=config.STORE_LOCATION)
748- finally:
749- os.mkdir = real_mkdir
750-
751- self.failUnless(self.created, 'Failed to create ~/.bazaar')
752- self.assertEqual([('__contains__', '/a/c'),
753+ self.callDeprecated(['The recurse option is deprecated as of '
754+ '0.14. The section "/a/c" has been '
755+ 'converted to use policies.'],
756+ self.my_config.set_user_option,
757+ 'foo', 'bar', store=config.STORE_LOCATION)
758+
759+ self.assertEqual([('reload',),
760+ ('__contains__', '/a/c'),
761 ('__contains__', '/a/c/'),
762 ('__setitem__', '/a/c', {}),
763 ('__getitem__', '/a/c'),
764@@ -1083,10 +1236,13 @@
765 if global_config is not None:
766 global_file = StringIO(global_config.encode('utf-8'))
767 my_config._get_global_config()._get_parser(global_file)
768- self.my_location_config = my_config._get_location_config()
769+ lconf = my_config._get_location_config()
770 if location_config is not None:
771 location_file = StringIO(location_config.encode('utf-8'))
772- self.my_location_config._get_parser(location_file)
773+ lconf._get_parser(location_file)
774+ # Make sure the config can be reloaded
775+ lconf._parser.filename = config.locations_config_filename()
776+ self.my_location_config = lconf
777 if branch_data_config is not None:
778 my_config.branch.control_files.files['branch.conf'] = \
779 branch_data_config