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

Proposed by Vincent Ladeuil
Status: Rejected
Rejected by: Vincent Ladeuil
Proposed branch: lp:~vila/bzr/525571-lock-bazaar-conf-files
Merge into: lp:bzr
Diff against target: 893 lines (+410/-101) (has conflicts)
6 files modified
NEWS (+15/-0)
bzrlib/builtins.py (+27/-14)
bzrlib/config.py (+130/-29)
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
Text conflict in bzrlib/config.py
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+28898@code.launchpad.net

This proposal supersedes a proposal from 2010-06-29.

Description of the change

The precise circumstances leading to the bug remained unclear, so
here it my best guess: somehow one process was able to read a
conf file *while* another process was writing it and failed
because the file was incomplete leading to parse errors.

ConfigObj does:

                h = open(infile, 'rb')
                infile = h.read() or []
                h.close()

to read a config file.

and
            h = open(self.filename, 'wb')
            h.write(output)
            h.close()

to write a config file.

I.e. the read or write can results in several physical IOs that
could explain partial files being read.

Addressing this is done with:

- using an atomic operation to write the config file so readers
  can't get a partial file,

- using a lock to guarantee that a single writer is allowed at any time.

The data model we use for config files is that they are used a
database where the records are the (name, value) pairs.

When a value is modified, the whole config file is written to
disk.

Config objects themselve are rarely cached (if ever) so to access
a value we generally load the whole file.

From there, it makes sense to re-read the whole file before
setting a new value so that other updates are taken into account
(*not* doing so means we will reset the values ignoring the
concurrent updates).

So the sequence to set a new value is:

- take a write lock,
- re-read the config file (to take concurrent updates into
  account),
- set the new value,
- write the config file,
- unlock

The proposed implementation means that all methods that ends up
writing a config file should be decorated with
@needs_write_lock. This is generally set_user_option() and its
variants (notably set_alias() and unset_alias() for
GlobalConfig).

Note that since we use an atomic write we neglect to protect
readers against concurrent writers. This shouldn't be a problem
in practice and I've been yelled at for suggesting it in a
paranoid attempt to cover all cases. The case at point being:

- a reader opens a file,
- starts to read it (unlikely to not complete in a single IO),
- a writer somehow manages to replace the opened file (unlikely since we
  do a rename),
- the OS presents the new content to the reader.

Since I'm not even sure this scenario is valid, I'll wait for
evidence before considering it.

Anyway, machines can crash while a lock is hold so break-lock has
to be updated to handle the config locks. After discussing the
possible implementations with lifeless, we settled on adding a
--conf option that must be specified to break a config lock. The
actual implementation succeeds when asked to break a lock in a
bzrdir where no lock exist, I did the same. We may want to
revisit the behavior for both cases (failing when no lock are
present) but I consider this out of scope for this bug fix.

I didn't enforce the config directory to be '~/.bazaar', it could
as well be '.bzrmeta' or anything.

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

-----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 : Posted in a previous version of this proposal

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 : Posted in a previous version of this proposal

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 : Posted in a previous version of this proposal

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 : Posted in a previous version of this proposal

> 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 : Posted in a previous version of this proposal

> 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...

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

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

...

> Note that since we use an atomic write we neglect to protect
> readers against concurrent writers. This shouldn't be a problem
> in practice and I've been yelled at for suggesting it in a
> paranoid attempt to cover all cases. The case at point being:
>
> - a reader opens a file,
> - starts to read it (unlikely to not complete in a single IO),
> - a writer somehow manages to replace the opened file (unlikely since we
> do a rename),
> - the OS presents the new content to the reader.
>
> Since I'm not even sure this scenario is valid, I'll wait for
> evidence before considering it.

POSIX systems require that the currently open file stays pointing at the
same content (same inode). While the rename replaces the directory
information, the open file handle does not point to a new file. (You
would have to have 'read' be implemented as (open(), seek(), read(),
close(), to get this behavior.)

Windows says that open() creates a lock on the *path*, which means that
you cannot rename a new file over a file which is already open. (So no
atomic rename operation.)

There is probably a CreateFile flag that might allow it (as there is one
that allows you to delete an open file IIRC). But we don't use it ourselves.

I'm about 95% sure that this scenario is invalid.

John
=:->

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

iEYEARECAAYFAkwrrRMACgkQJdeBCYSNAANG5QCeNjIy3IeDz2fFMW0KPHEUp5qv
gdYAoMG9ymYJX32g3tVRWCsIXF5vke8r
=nu81
-----END PGP SIGNATURE-----

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

16:24 < lifeless> vila: --config please, not --conf
16:25 < lifeless> --conf is a strange abbreviation
16:25 < lifeless> c = config.LockableConfig(lambda : location)
16:25 < lifeless> reads really strangely
16:25 < lifeless> perhaps the LockableConfig constructor should be different
16:27 < vila> lifeless: I don't want to break the chain of constructors, I can add a def get_filename if you prefer

When a class provokes ugly code in users, the class is wrong. Perhaps the class could take two keyword constructors, and then only the new code in builtins uses a keyword, and uses the second one to supply a name.

the class can then use it.

The ironic thing here is the first thing the class does is make the filename by calling the constructor.

Please note in the (otherwise excellent) docstring that the reason callers need to lock_write, rather than the innermost writer function, is to ensure that the file is both *read* and *written* in the same lock.

Perhaps that would be best tracked - when doing a read, if the object is not locked, set self._can_write = False, and if it is, set it to True. Then check that as well as whether it is locked, in _write_config.

The file=None to infile=None change seems to just generate spurious noise, and is not done in a backward compatible way.

We are likely going to want to backport this for launchpad, so it really should be a focused patch.

Likewise the test changes that are not related to locking consistency.

Please roll those things back, and I think we'll have a much more focused patch that we can confidently get LP to cherrypick.

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

There's also a number of opaque variable names like 'c', 'p' and 'lc_tests' - while I figured out what they were, they could be a lot clearer.

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

> 16:24 < lifeless> vila: --config please, not --conf
> 16:25 < lifeless> --conf is a strange abbreviation
> 16:25 < lifeless> c = config.LockableConfig(lambda : location)
> 16:25 < lifeless> reads really strangely
> 16:25 < lifeless> perhaps the LockableConfig constructor should be different
> 16:27 < vila> lifeless: I don't want to break the chain of constructors, I can
> add a def get_filename if you prefer
>
>
> When a class provokes ugly code in users, the class is wrong. Perhaps the
> class could take two keyword constructors, and then only the new code in
> builtins uses a keyword, and uses the second one to supply a name.

I.e. __init__ should accept file_name and get_filename and deprecate the later ?

>
> the class can then use it.

Conflicts with making a focused patch. I'll keep that for an updated submission focused on cleanup.

>
> The ironic thing here is the first thing the class does is make the filename
> by calling the constructor.

Yup, it's needed to build the lock directory (I'm sure you got that). The other classes postpone the get_filename() evaluation until it's needed, but given the functions used to provide the names, I don't think postponing is really required (some env variables are involved though, so there may be edge cases).

>
> Please note in the (otherwise excellent) docstring that the reason callers
> need to lock_write, rather than the innermost writer function, is to ensure
> that the file is both *read* and *written* in the same lock.
>

I'll mention that. And yes, it's the real reason for requiring a larger lock scope.

> Perhaps that would be best tracked - when doing a read, if the object is not
> locked, set self._can_write = False, and if it is, set it to True. Then check
> that as well as whether it is locked, in _write_config.

_can_write == is_locked() I see the idea but it won't change the end result nor the feedback we can give.

>
>
> The file=None to infile=None change seems to just generate spurious noise, and
> is not done in a backward compatible way.

It's on a private method and used only by tests (and provide a poor way to build test configs anyway).

>
> We are likely going to want to backport this for launchpad, so it really
> should be a focused patch.

I'll restart from scratch then.

>
> Likewise the test changes that are not related to locking consistency.

Most of them were required to address the flawed assumption that a config object (with content)
can be created without an actual file existing, I had to fix the failing tests.
The duplicated ensure_config_dir_exists() came in the way to.

And I mention that to explain that many cleanups were not for the sake of pure cleaning but driven by problems making the fix harder.

I'll look into an alternative solution to minimise the patch but that would likely end up in either weird code or intrusive changes, we'll see.

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

> 16:24 < lifeless> vila: --config please, not --conf
> 16:25 < lifeless> --conf is a strange abbreviation

:-P

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

Vila, I think I need to expand on this:

>> Perhaps that would be best tracked - when doing a read, if the object is not
>> locked, set self._can_write = False, and if it is, set it to True. Then check
>> that as well as whether it is locked, in _write_config.

> _can_write == is_locked() I see the idea but it won't change the end result nor the feedback we can give.

They are quite different.

If when you *read* you are unlocked, and you permit a *write* without forcing a new read, changes from other processes are discarded.

Here:
conf.read_something()
--client 2 writes to the file
conf.set_something()
--*boom* client 2's changes are lost

What we need is two fold: detection of this case (which setting 'can write' during *read* if it is write locked at the time will permit), and easy api use (which may be less immediate depending on impact).

Alternatively lock_write could possibly trigger a read after locking before returning, which would cause properly lock_write() guarded mutators to work correctly - as they would apply the mutation within the locked context.

In short - concurrency is hard, lets drink beer.

review: Needs Fixing
Revision history for this message
John Szakmeister (jszakmeister) wrote :

On Sun, Jul 4, 2010 at 6:39 PM, Robert Collins
<email address hidden> wrote:
[snip]
> In short - concurrency is hard, lets drink beer.

I think that's the best quote I've seen in a while! :-)

-John

Revision history for this message
Vincent Ladeuil (vila) wrote :
Download full text (3.5 KiB)

>>>>> Robert Collins <email address hidden> writes:

    > Review: Needs Fixing
    > Vila, I think I need to expand on this:

    >>> Perhaps that would be best tracked - when doing a read, if the object is not
    >>> locked, set self._can_write = False, and if it is, set it to True. Then check
    >>> that as well as whether it is locked, in _write_config.

    >> _can_write == is_locked() I see the idea but it won't change the end result nor the feedback we can give.

    > They are quite different.

    > If when you *read* you are unlocked, and you permit a *write*
    > without forcing a new read,

Which this proposal guards against.

    > changes from other processes are discarded.

Yes, that's a delicate problem. This proposal isn't perfect but good
enough to cover most of our use cases.

    > Here:
    > conf.read_something()
    > --client 2 writes to the file
    > conf.set_something()
    > --*boom* client 2's changes are lost

Yup, I added a test for that and saw it fail.

    > What we need is two fold: detection of this case (which setting
    > 'can write' during *read* if it is write locked at the time will
    > permit),

Right, but what will we gain here ? The ability to *not* force the
refreshing read if someone already did it while write locked ?

Yet, it goes in the same direction as implementing a no-op read lock,
which we may want to add too, so, worth considering.

    > and easy api use (which may be less immediate depending on
    > impact).

    > Alternatively lock_write could possibly trigger a read after
    > locking before returning, which would cause properly lock_write()
    > guarded mutators to work correctly - as they would apply the
    > mutation within the locked context.

Inside the lock scope, a write should always happen *after* a refreshing
read or you'll lose changes for variables you're not changing (the
current API change one variable at a time).

Of course you always lose any change to the variable you're changing and
that in itself could be a problem if processes try to use config
variables for synchronization (a really bad idea), like an incremented
counter.

    > In short - concurrency is hard, lets drink beer.

+1

There are various lock schemes with different scopes that can be tried,
but so far, I haven't found a good way to address:

- client1 read 'var'
- client1 starts a "long" process based on 'var'
- client2 set a new value for 'var'
- client1 finished its process with a now outdated value for 'var'

Short of blocking client2 for the duration of client1 process, I can't
see how to address this.

But do we care ?

It's hard to decide without concrete examples, if 'var' is a parent
branch location and the process is pull, chances are the user really
want its pull to finish and changing 'var' to point to a new branch
should just be considered the next time we try to pull (alternate
scenarios includes: abort the pull (and rollback the already fetched
revisions (eeerk)), block the write (i.e. block the whole config file
for the duration of client1 process (omg are you crazy ?))).

Overall, I agree that we should continue thinking about which lock model
we want here, the proposed...

Read more...

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

Marking as WIP pending a discussion during our next sprint on larger issues.
A minimal fix has landed for 2.1 using atomic writes which should be a good enough fix in the mean time.

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

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

...

>
> Overall, I agree that we should continue thinking about which lock model
> we want here, the proposed one is incrementally better and at least
> address the bug better than the minimal one landed for 2.1.
>
> Orthogonal to the lock scope, sharing a config files at the
> wt/branch/repo level could at least avoid reading the config file for
> each variable which could make read locks less painful, but still, I
> don't think it would be enough.

I think defining a scope is reasonable, and just sticking with that.

IMO, configs could be treated just like the rest of the branch data. So
if you '.lock_read()' at the beginning of a process, you read the config
state at that point in time, and then never read the config file again.

When you go to write, you probably need to re-read the file, because a
given text file has more content than what you are likely to be adjusting.

We *could* implement some sort of CAS to make config updates atomic. I
don't think that is worthwhile. (I tried to overwrite parent X with
parent Y, but parent was already Z.)

I agree that you shouldn't try to abuse configs to store incremental
counters, etc.

I honestly don't know what state was getting trashed by concurrent
updates, but I have the feeling that all of the bzr core data doesn't
really care. (Yes, you might get the wrong parent pointer, or submit
pointer, but we don't really store 'live' data in conf files.)

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

iEYEARECAAYFAkwzP7gACgkQJdeBCYSNAANa5QCdFVLPjMHJLyKn7MvR9NdkiAhp
6r8An3u2enhCMDQVbPS/wOmeo5LUO6X3
=Kmd7
-----END PGP SIGNATURE-----

5334. By Vincent Ladeuil

Clarify lock scope.

5335. By Vincent Ladeuil

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

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

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