Merge lp:~spiv/bzr/cleanup-hof into lp:bzr
- cleanup-hof
- Merge into bzr.dev
Status: | Rejected |
---|---|
Rejected by: | Andrew Bennetts |
Proposed branch: | lp:~spiv/bzr/cleanup-hof |
Merge into: | lp:bzr |
Diff against target: |
798 lines 9 files modified
bzrlib/cleanup.py (+172/-0) bzrlib/decorators.py (+28/-0) bzrlib/lockable_files.py (+2/-2) bzrlib/lockdir.py (+2/-0) bzrlib/progress.py (+3/-0) bzrlib/remote.py (+3/-1) bzrlib/revisiontree.py (+2/-0) bzrlib/tests/__init__.py (+182/-0) bzrlib/tests/test_cleanup.py (+259/-0) |
To merge this branch: | bzr merge lp:~spiv/bzr/cleanup-hof |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
bzr-core | Pending | ||
Review via email: mp+12318@code.launchpad.net |
Commit message
Description of the change
Andrew Bennetts (spiv) wrote : | # |
Martin Pool (mbp) wrote : | # |
2009/9/24 Andrew Bennetts <email address hidden>:
> [Trying an RFC for a branch by cross-posting to both the merge proposal on
> Launchpad, and the bazaar@ list.]
>
> Hi all,
>
> Here's the problem. In this example, Python gives no way for final_func to know
> if try_func raised an error or not:
>
> try:
> try_func()
> finally:
> final_func()
>
> Not even by sys._getframe hacks, AFAICT. I can go into some gory details about
> why not, but more important is what to do about it.
>
> It's problematic because, in general, we don't want an error from final_func to
> override an error from try_func. This means that sometimes users get errors
> about, say, abort_write_group failing, when what originally went wrong was
> “connection lost”. The various TooManyConcurre
> common symptom of this.
>
> This brings me to <https:/
> <https:/
>
> Bug 429747 proposes a higher-
> least give us a common place to control policy about log vs. raise, etc. That
> branch implements that, as a function called run_cleanup (and some variations).
> It lives in a new module, bzrlib.cleanups. I have tried to give the new module
> and its contents fairly reasonable docstrings, and clear tests.
>
> The downside of run_cleanup is that it will suppress some exceptions that should
> be propagated, but that seems to be unavoidable given the limitations of Python.
> (And note that developers can always use -Dcleanup to get UI warnings about
> failures in run_cleanup).
>
> The branch also implements something called do_with_cleanups, which can actually
> be pretty robust and correct, at the cost of making simple callsites a bit
> uglier. I have a follow-on patch that adjusts bzrlib.commit to use this, which
> would fix some outstanding bugs.
>
> So, here's the RFC: I think the policy should be:
>
> * use run_cleanup if a cleanup failure is likely to be unimportant to a user
> (or at least, the chance of it being important is < the chance of the main
> func raising an error that should not be obscured).
> * consider changing the cleanup function itself to never raise errors, e.g.
> abort_write_
> Again, only applicable if failures during cleanup are "uninteresting".
> * use run_cleanups if you are running multiple cleanups; it takes care of
> running them all even if an exception happens.
> * if you want very correct behaviour, use do_with_cleanups. This tends to make
> simple callsites a bit uglier, but in already complex code (like commit) it
> may be no worse.
>
> If agreed, I can add this to HACKING.txt.
(I haven't read the diff yet, but we did talk about it ...)
I was originally suggesting that we should handle this by putting all
cleanup-type code through a single bottleneck. I think you're on to
something in saying (if you are) that we should instead put it through
common policies for different types of cleanup.
My sense is that this is more about the type of cleanup than the place
...
Vincent Ladeuil (vila) wrote : | # |
>>>>> "Andrew" == Andrew Bennetts <email address hidden> writes:
Andrew> [Trying an RFC for a branch by cross-posting to both the merge proposal on
Andrew> Launchpad, and the bazaar@ list.]
Andrew> Hi all,
Andrew> Here's the problem. In this example, Python gives no way for final_func to know
Andrew> if try_func raised an error or not:
Andrew> try:
Andrew> try_func()
Andrew> finally:
Andrew> final_func()
try:
no_error = False
try_func()
no_error = True
finally:
final_
Andrew Bennetts (spiv) wrote : | # |
Martin Pool wrote:
> 2009/9/24 Andrew Bennetts <email address hidden>:
[...]
>
> (I haven't read the diff yet, but we did talk about it ...)
>
> I was originally suggesting that we should handle this by putting all
> cleanup-type code through a single bottleneck. I think you're on to
> something in saying (if you are) that we should instead put it through
> common policies for different types of cleanup.
Yes, I think that's what I'm getting at (the picture has been emerging
pretty gradually!).
> My sense is that this is more about the type of cleanup than the place
> it's being called from, so I'd expect that normally we will be
> changing the functions called at cleanup, not the code with the
> try/finally block. Of course if there is code with multiply nested
> try/finally blocks and we can make it cleaner by changing to a
> higher-
Right, that's a good example where a h-o-f can make the code cleaner, not
just more robust.
> I think we can distinguish different types of severity of failure.
> For instance, if we fail to unlock a branch, that's worth telling the
> user about, but it's bad to give them such a large or severe message
> that it obscures their view of what actually went wrong, as often
> seems to happen in bugs.
>
> One blue-sky option here would be to indicate success or failure by
> return code: unlock (say) returns True if it actually unlocked, False
> if it failed. Then code that really cares can see, but the default
> will be not to interrupt processing. This gives you a way to have
> failures that are 'slightly interesting'.
I don't particularly like checking return values. I think it might be nice
to be able to say "foo.unlock(
on_error=
the default, and it's also questionable about whether we really want to
burden all unlock implementations with this, but it's an interesting
strawman API.
My thinking here is that *usually* an unlock failure is probably not
something to report to the user, but there are times when we definitely want
errors to propagate directly (e.g. an explicit invocation of break-lock, and
probably during the test suite).
So there's a mix of inputs here. I'm having trouble articulating this
clearly, but roughly I feel that what's desired in any individual case may
depend on a combination of:
- the cleanup operation itself;
- what the callsite intends (some places may explicitly expect and suppress
particular errors, so emitting something to the UI instead of raising
something the callsite can catch may be a bad thing);
- any global policy, like debug flags;
- and maybe also the specific error (e.g. KeyboardInterrupt vs. connection
lost vs. an assertion about unfinished progress bars)?
The challenge seems to be balancing these desires while keeping things
reasonable clean and simple.
> It seems to me the most common requests are:
>
> * TooManyConcurre
> operation being interrupted either because of a client-side bug or
> error, or a connection dropping
> * Unlock failing because ...
Andrew Bennetts (spiv) wrote : | # |
Vincent Ladeuil wrote:
[...]
> try:
> no_error = False
> try_func()
> no_error = True
> finally:
> final_func(
Ugh! And of course then you need to change final_func to reliably never
raise errors depending on that flag... and then there are cases with multiple
cleanups...
But yes, changing the try/finally somehow is the only option (using
do_with_cleanups from my patch is another variation on this). It's a
shame because it means the obvious spelling is not the correct one :(
-Andrew.
Martin Pool (mbp) wrote : | # |
> Vincent Ladeuil wrote:
> [...]
> > try:
> > no_error = False
> > try_func()
> > no_error = True
> > finally:
> > final_func(
>
> Ugh! And of course then you need to change final_func to reliably never
> raise errors depending on that flag... and then there are cases with multiple
> cleanups...
Not to mention the possibility of UnboundLocalErrors - unlikely in this simple case, but still possible if you just happen to get interrupted at the right time.
But Vincent's suggestion does point out something interesting, which is that the "has an error occurred" or "have things finished correctly" is often already available in the program state without needing a specific variable to be added for it. In the case we're primarily looking at here: if unlock runs and the medium(
We can decompose this into:
How do we detect when we're in a knock-on error case?
What do we do differently?
The assumption is that we want to suppress errors only when they would be knock-on errors. I think we do want to squelch them in that case, but perhaps for some of these errors, like failure to unlock, they don't need to be propagated as errors at any time (outside of testing etc).
Martin Pool (mbp) wrote : | # |
2009/9/24 Andrew Bennetts <email address hidden>:
> Martin Pool wrote:
>> 2009/9/24 Andrew Bennetts <email address hidden>:
> [...]
>>
>> (I haven't read the diff yet, but we did talk about it ...)
>>
>> I was originally suggesting that we should handle this by putting all
>> cleanup-type code through a single bottleneck. I think you're on to
>> something in saying (if you are) that we should instead put it through
>> common policies for different types of cleanup.
>
> Yes, I think that's what I'm getting at (the picture has been emerging
> pretty gradually!).
>
>> My sense is that this is more about the type of cleanup than the place
>> it's being called from, so I'd expect that normally we will be
>> changing the functions called at cleanup, not the code with the
>> try/finally block. Of course if there is code with multiply nested
>> try/finally blocks and we can make it cleaner by changing to a
>> higher-
>
> Right, that's a good example where a h-o-f can make the code cleaner, not
> just more robust.
>
>> I think we can distinguish different types of severity of failure.
>> For instance, if we fail to unlock a branch, that's worth telling the
>> user about, but it's bad to give them such a large or severe message
>> that it obscures their view of what actually went wrong, as often
>> seems to happen in bugs.
>>
>> One blue-sky option here would be to indicate success or failure by
>> return code: unlock (say) returns True if it actually unlocked, False
>> if it failed. Then code that really cares can see, but the default
>> will be not to interrupt processing. This gives you a way to have
>> failures that are 'slightly interesting'.
>
> I don't particularly like checking return values.
OK, but why? Obviously in general in Python one should be using
exceptions rather than return code as the normal convention, but are
there any other drawbacks? The other main reason is that return codes
can easily be ignored but that is (perhaps) just what we want here.
One problem with return codes is that, if you return a boolean, you
don't get any explanation of what went wrong. We could potentially
return an Exception object, but that might be getting too weird.
> I think it might be nice
> to be able to say "foo.unlock(
> on_error=
> the default, and it's also questionable about whether we really want to
> burden all unlock implementations with this, but it's an interesting
> strawman API.
That seems ok, at least assuming that you expect different callers to
prefer to get errors, warnings, or nothing. But aside from tests
specifically for locking, I'm not sure that they will.
> My thinking here is that *usually* an unlock failure is probably not
> something to report to the user, but there are times when we definitely want
> errors to propagate directly (e.g. an explicit invocation of break-lock, and
> probably during the test suite).
I don't think break-lock calls unlock, it calls break_lock, which
presumably would always error.
> So there's a mix of inputs here. I'm having troubl...
Stephen Turnbull (stephen-xemacs) wrote : | # |
Andrew Bennetts writes:
> Here's the problem. In this example, Python gives no way for
> final_func to know if try_func raised an error or not:
>
> try:
> try_func()
> finally:
> final_func()
In 2.6, you can do
try:
errflag = "Impossible is nothing."
try_func()
except:
errflag = "Uh oh ..."
finally:
final_
In earlier Pythae you must do the ugly but equivalent
try:
errflag = "Impossible is nothing."
try:
try_func()
except:
errflag = "Uh oh ..."
finally:
final_
I believe Guido acknowledged not allowing the more compact notation in
the first place to be excessive caution, but it's not impossible to
distinguish exceptions raised by try_func from those raised by
final_func.
So what's the problem? You want to do this without fixing all the
broken try ... finally blocks?
> Not even by sys._getframe hacks, AFAICT. I can go into some gory
> details about why not,
Inquiring minds want to know....
Martin Pool (mbp) wrote : | # |
2009/9/24 Stephen J. Turnbull <email address hidden>:
> I believe Guido acknowledged not allowing the more compact notation in
> the first place to be excessive caution, but it's not impossible to
> distinguish exceptions raised by try_func from those raised by
> final_func.
>
> So what's the problem? You want to do this without fixing all the
> broken try ... finally blocks?
It's not _impossible_, we just don't want to add boilerplate to
everything that uses a try/finally block. There are many more of them
than there are distinct types of cleanup to run.
--
Martin <http://
Andrew Bennetts (spiv) wrote : | # |
Martin Pool wrote:
> 2009/9/24 Stephen J. Turnbull <email address hidden>:
>
> > I believe Guido acknowledged not allowing the more compact notation in
> > the first place to be excessive caution, but it's not impossible to
> > distinguish exceptions raised by try_func from those raised by
> > final_func.
> >
> > So what's the problem? You want to do this without fixing all the
> > broken try ... finally blocks?
>
> It's not _impossible_, we just don't want to add boilerplate to
> everything that uses a try/finally block. There are many more of them
> than there are distinct types of cleanup to run.
Especially when the boilerplate involved is just complex enough that it
would be error-prone.
e.g. your proposed boilerplate appears to be missing a raise in the except
block... (unless you also suggest boilerplate in every final_func to start with
saving sys.exc_info() and then finally reraise it!)
-Andrew.
Robert Collins (lifeless) wrote : | # |
We actually want something a lot closer to 'with', I suspect.
The problem is that we support python 2.4
-Rob
Andrew Bennetts (spiv) wrote : | # |
Martin Pool wrote:
[...]
> > I don't particularly like checking return values.
>
> OK, but why? Obviously in general in Python one should be using
> exceptions rather than return code as the normal convention, but are
> there any other drawbacks? The other main reason is that return codes
> can easily be ignored but that is (perhaps) just what we want here.
It's just the “easy to forget to check them” issue that concerns me.
> One problem with return codes is that, if you return a boolean, you
> don't get any explanation of what went wrong. We could potentially
> return an Exception object, but that might be getting too weird.
>
> > I think it might be nice
> > to be able to say "foo.unlock(
> > on_error=
> > the default, and it's also questionable about whether we really want to
> > burden all unlock implementations with this, but it's an interesting
> > strawman API.
>
> That seems ok, at least assuming that you expect different callers to
> prefer to get errors, warnings, or nothing. But aside from tests
> specifically for locking, I'm not sure that they will.
Yeah, I'm not sure exactly how featureful we need to be. It could well be a
YAGNI, although testability is important.
> > My thinking here is that *usually* an unlock failure is probably not
> > something to report to the user, but there are times when we definitely want
> > errors to propagate directly (e.g. an explicit invocation of break-lock, and
> > probably during the test suite).
>
> I don't think break-lock calls unlock, it calls break_lock, which
> presumably would always error.
Oh, right, good point. I feel like there's probably other examples, but
perhaps not...
> > So there's a mix of inputs here. I'm having trouble articulating this
> > clearly, but roughly I feel that what's desired in any individual case may
> > depend on a combination of:
> >
> > - the cleanup operation itself;
> > - what the callsite intends (some places may explicitly expect and suppress
> > particular errors, so emitting something to the UI instead of raising
> > something the callsite can catch may be a bad thing);
> > - any global policy, like debug flags;
> > - and maybe also the specific error (e.g. KeyboardInterrupt vs. connection
> > lost vs. an assertion about unfinished progress bars)?
> >
> > The challenge seems to be balancing these desires while keeping things
> > reasonable clean and simple.
>
> My idea was that we should try, at least in a branch, just changing
> the policy, and see what happens in several dimensions: how that
> changes the look & feel of the code as you change it, what tests fail
> if any and how easy & clean it is to fix them, and what positive or
> negative effects this has on users. Then we'll have more data on the
> balance.
Ok, trying it in practice makes good sense. I might aim for trying to
update (or at least look at) every try-finally in bzrdir.py, repository.py
and branch.py and see how it goes.
[...]
> I think that policy sounds ok, though maybe we need different
> functions for important and unimportant cleanups. I suggest taking
> the im...
Martin Pool (mbp) wrote : | # |
2009/9/24 Andrew Bennetts <email address hidden>:
> Martin Pool wrote:
>> 2009/9/24 Stephen J. Turnbull <email address hidden>:
>>
>> > I believe Guido acknowledged not allowing the more compact notation in
>> > the first place to be excessive caution, but it's not impossible to
>> > distinguish exceptions raised by try_func from those raised by
>> > final_func.
>> >
>> > So what's the problem? You want to do this without fixing all the
>> > broken try ... finally blocks?
>>
>> It's not _impossible_, we just don't want to add boilerplate to
>> everything that uses a try/finally block. There are many more of them
>> than there are distinct types of cleanup to run.
>
> Especially when the boilerplate involved is just complex enough that it
> would be error-prone.
>
> e.g. your proposed boilerplate appears to be missing a raise in the except
> block... (unless you also suggest boilerplate in every final_func to start with
> saving sys.exc_info() and then finally reraise it!)
This is the place where Stephen's supposed to say something about Lisp
being a great improvement on many languages that came after it... ;-)
--
Martin <http://
Martin Pool (mbp) wrote : | # |
2009/9/24 Andrew Bennetts <email address hidden>:
>> My idea was that we should try, at least in a branch, just changing
>> the policy, and see what happens in several dimensions: how that
>> changes the look & feel of the code as you change it, what tests fail
>> if any and how easy & clean it is to fix them, and what positive or
>> negative effects this has on users. Then we'll have more data on the
>> balance.
>
> Ok, trying it in practice makes good sense. I might aim for trying to
> update (or at least look at) every try-finally in bzrdir.py, repository.py
> and branch.py and see how it goes.
I'm kind of hoping you won't actually need to change them though, only
the code they call.
> [...]
>> I think that policy sounds ok, though maybe we need different
>> functions for important and unimportant cleanups. I suggest taking
>> the implementation all the way through with say unlock so that we can
>> see how it actually behaves.
>
> That sounds like a good experiment, although I dread the effort to actually
> update all the implementations of unlock...
>
> (By “all the way through” you mean extending the API of unlock, right?)
By "all the way through" I mean to the point where you can
interactively test (somehow, maybe by killing your network connection)
creating a situation where unlock would run after an error, and
observe that it looks better than it does with the current code. Not
all the way 'across' every case at first.
--
Martin <http://
Stephen Turnbull (stephen-xemacs) wrote : | # |
Andrew Bennetts writes:
> e.g. your proposed boilerplate appears to be missing a raise in the
> except block...
That's not proposed boilerplate; that's proof of concept that
communication *is* possible.
That said, I don't think reraising from the try is appropriate; that
puts you in the same boat you are worried about where you can't
distinguish between an exception that occurs in try_func() and one
that occurs in final_func().
If I were writing code where there are a lot of try-finally blocks,
but only a few versions of final_func(), what I would probably
actually do is stuff the actual exception into errflag, and let each
final_func() reraise it, deal with it, or ignore it as appropriate to
that final_func().
Agreed, this is still ugly and somewhat error prone with except-less
try-finally blocks, but the semantics and syntax of a finally clause
that handles exceptions are less than transparent to me. Should the
exception be available to finally if already handled? What is the
syntax for binding the exception to an identifier in the finally
clause? Should exceptions (re)raised in an except clause be treated
differently from exceptions that were raised from the try clause?
Andrew Bennetts (spiv) wrote : | # |
Martin Pool wrote:
[...]
> OK, so +1 from me, but the proof will be in how it would handle the
> bugs reported as TooMany*. It might be worth grepping for them and
> looking at their call stacks.
So, I've spent some time looking at the TooMany* bug reports. Here's a summary
of causes:
* WorkingTree.pull has error -> then runs unlock in finally
* Branch.push has error -> then run unlock in finally
* Repo.fetch has error during stream -> then run unlock in finally
(e.g. due to buggy stream from old server sending cross-format to 2a)
* commit has an error -> then runs a bunch of unlocks in a finally
So those are all a case of unlock doomed to failure by prior error, then
obscures that prior error.
There is one other case, though:
* WorkingTree.commit invokes -> BzrBranch5.
BzrDir.open -> do_catching_
with TooMany* error.
This one doesn't appear to be a cleanup error, but some sort of failure about
charging onwards after an error, when probably we shouldn't? I'm not sure
exactly what's going on there, but I suspect a bug in the way we find formats in
BzrDir.open. Anyway, this one is not a bug during cleanup AFAICT.
So, it appears we would get excellent mileage out of changing unlock on the core
objects (Branch, WorkingTree, etc) to suppress at least most errors, although
probably we still want to allow LockNotHeld and maybe LockBroken to raise, not
just warn?
Martin Pool (mbp) wrote : | # |
2009/9/25 Andrew Bennetts <email address hidden>:
> Martin Pool wrote:
> [...]
>> OK, so +1 from me, but the proof will be in how it would handle the
>> bugs reported as TooMany*. It might be worth grepping for them and
>> looking at their call stacks.
>
> So, I've spent some time looking at the TooMany* bug reports. Here's a summary
> of causes:
>
> * WorkingTree.pull has error -> then runs unlock in finally
> * Branch.push has error -> then run unlock in finally
> * Repo.fetch has error during stream -> then run unlock in finally
> (e.g. due to buggy stream from old server sending cross-format to 2a)
> * commit has an error -> then runs a bunch of unlocks in a finally
>
> So those are all a case of unlock doomed to failure by prior error, then
> obscures that prior error.
OK
> There is one other case, though:
>
> * WorkingTree.commit invokes -> BzrBranch5.
> BzrDir.open -> do_catching_
> with TooMany* error.
>
> This one doesn't appear to be a cleanup error, but some sort of failure about
> charging onwards after an error, when probably we shouldn't? I'm not sure
> exactly what's going on there, but I suspect a bug in the way we find formats in
> BzrDir.open. Anyway, this one is not a bug during cleanup AFAICT.
>
> So, it appears we would get excellent mileage out of changing unlock on the core
> objects (Branch, WorkingTree, etc) to suppress at least most errors, although
> probably we still want to allow LockNotHeld and maybe LockBroken to raise, not
> just warn?
That sounds good. We could even consider not treating LockNotHeld etc
differently to start with, people are unlikely to try to catch them.
--
Martin <http://
Andrew Bennetts (spiv) wrote : | # |
Martin Pool wrote:
[...]
> > So, it appears we would get excellent mileage out of changing unlock on the core
> > objects (Branch, WorkingTree, etc) to suppress at least most errors, although
> > probably we still want to allow LockNotHeld and maybe LockBroken to raise, not
> > just warn?
>
> That sounds good. We could even consider not treating LockNotHeld etc
> differently to start with, people are unlikely to try to catch them.
I'm pretty sure tests do, though.
I'm currently experimenting with a decorator that looks like this:
@only_
def unlock(self):
And so far it's looking promising, although there's still a little bit of test
suite fall out...
- 4675. By Andrew Bennetts
-
Add some experimental decorators: @only_raises(..) and @cleanup_method.
John A Meinel (jameinel) wrote : | # |
So what is the status of this submission? Is it superseded by your other work?
- 4676. By Andrew Bennetts
-
Change test_unlock_
in_write_ group to expect a log_exception_ quietly rather than a raise. - 4677. By Andrew Bennetts
-
Suppress most errors from Branch.unlock too.
- 4678. By Andrew Bennetts
-
Merge lp:bzr.
- 4679. By Andrew Bennetts
-
Merge robust-
cleanup- in-commit, but ignore its changes (which just drop some features added in this branch.)
Unmerged revisions
- 4679. By Andrew Bennetts
-
Merge robust-
cleanup- in-commit, but ignore its changes (which just drop some features added in this branch.)
Preview Diff
1 | === added file 'bzrlib/cleanup.py' |
2 | --- bzrlib/cleanup.py 1970-01-01 00:00:00 +0000 |
3 | +++ bzrlib/cleanup.py 2009-09-25 02:11:12 +0000 |
4 | @@ -0,0 +1,172 @@ |
5 | +# Copyright (C) 2009 Canonical Ltd |
6 | +# |
7 | +# This program is free software; you can redistribute it and/or modify |
8 | +# it under the terms of the GNU General Public License as published by |
9 | +# the Free Software Foundation; either version 2 of the License, or |
10 | +# (at your option) any later version. |
11 | +# |
12 | +# This program is distributed in the hope that it will be useful, |
13 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
14 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
15 | +# GNU General Public License for more details. |
16 | +# |
17 | +# You should have received a copy of the GNU General Public License |
18 | +# along with this program; if not, write to the Free Software |
19 | +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA |
20 | + |
21 | +"""Helpers for managing cleanup functions and the errors they might raise. |
22 | + |
23 | +Generally, code that wants to perform some cleanup at the end of an action will |
24 | +look like this:: |
25 | + |
26 | + from bzrlib.cleanups import run_cleanup |
27 | + try: |
28 | + do_something() |
29 | + finally: |
30 | + run_cleanup(cleanup_something) |
31 | + |
32 | +Any errors from `cleanup_something` will be logged, but not raised. |
33 | +Importantly, any errors from do_something will be propagated. |
34 | + |
35 | +There is also convenience function for running multiple, independent cleanups |
36 | +in sequence: run_cleanups. e.g.:: |
37 | + |
38 | + try: |
39 | + do_something() |
40 | + finally: |
41 | + run_cleanups([cleanup_func_a, cleanup_func_b], ...) |
42 | + |
43 | +Developers can use the `-Dcleanup` debug flag to cause cleanup errors to be |
44 | +reported in the UI as well as logged. |
45 | + |
46 | +Note the tradeoff that run_cleanup/run_cleanups makes: errors from |
47 | +`do_something` will not be obscured by errors from `cleanup_something`, but |
48 | +errors from `cleanup_something` will never reach the user, even if there is no |
49 | +error from `do_something`. So run_cleanup is good to use when a failure of |
50 | +internal housekeeping (e.g. failure to finish a progress bar) is unimportant to |
51 | +a user. |
52 | + |
53 | +If you want to be certain that the first, and only the first, error is raised, |
54 | +then use:: |
55 | + |
56 | + do_with_cleanups(do_something, cleanups) |
57 | + |
58 | +This is more inconvenient (because you need to make every try block a |
59 | +function), but will ensure that the first error encountered is the one raised, |
60 | +while also ensuring all cleanups are run. |
61 | +""" |
62 | + |
63 | + |
64 | +import sys |
65 | +from bzrlib import ( |
66 | + debug, |
67 | + trace, |
68 | + ) |
69 | + |
70 | +def _log_cleanup_error(exc): |
71 | + trace.mutter('Cleanup failed:') |
72 | + trace.log_exception_quietly() |
73 | + if 'cleanup' in debug.debug_flags: |
74 | + trace.warning('bzr: warning: Cleanup failed: %s', exc) |
75 | + |
76 | + |
77 | +def run_cleanup(func, *args, **kwargs): |
78 | + """Run func(*args, **kwargs), logging but not propagating any error it |
79 | + raises. |
80 | + |
81 | + :returns: True if func raised no errors, else False. |
82 | + """ |
83 | + try: |
84 | + func(*args, **kwargs) |
85 | + except KeyboardInterrupt: |
86 | + raise |
87 | + except Exception, exc: |
88 | + _log_cleanup_error(exc) |
89 | + return False |
90 | + return True |
91 | + |
92 | + |
93 | +def run_cleanup_reporting_errors(func, *args, **kwargs): |
94 | + try: |
95 | + func(*args, **kwargs) |
96 | + except KeyboardInterrupt: |
97 | + raise |
98 | + except Exception, exc: |
99 | + trace.mutter('Cleanup failed:') |
100 | + trace.log_exception_quietly() |
101 | + trace.warning('Cleanup failed: %s', exc) |
102 | + return False |
103 | + return True |
104 | + |
105 | + |
106 | +def run_cleanups(funcs, on_error='log'): |
107 | + """Run a series of cleanup functions. |
108 | + |
109 | + :param errors: One of 'log', 'warn first', 'warn all' |
110 | + """ |
111 | + seen_error = False |
112 | + for func in funcs: |
113 | + if on_error == 'log' or (on_error == 'warn first' and seen_error): |
114 | + seen_error |= run_cleanup(func) |
115 | + else: |
116 | + seen_error |= run_cleanup_reporting_errors(func) |
117 | + |
118 | + |
119 | +def do_with_cleanups(func, cleanup_funcs): |
120 | + """Run `func`, then call all the cleanup_funcs. |
121 | + |
122 | + All the cleanup_funcs are guaranteed to be run. The first exception raised |
123 | + by func or any of the cleanup_funcs is the one that will be propagted by |
124 | + this function (subsequent errors are caught and logged). |
125 | + |
126 | + Conceptually similar to:: |
127 | + |
128 | + try: |
129 | + return func() |
130 | + finally: |
131 | + for cleanup in cleanup_funcs: |
132 | + cleanup() |
133 | + |
134 | + It avoids several problems with using try/finally directly: |
135 | + * an exception from func will not be obscured by a subsequent exception |
136 | + from a cleanup. |
137 | + * an exception from a cleanup will not prevent other cleanups from |
138 | + running (but the first exception encountered is still the one |
139 | + propagated). |
140 | + |
141 | + Unike `run_cleanup`, `do_with_cleanups` can propagate an exception from a |
142 | + cleanup, but only if there is no exception from func. |
143 | + """ |
144 | + # As correct as Python 2.4 allows. |
145 | + try: |
146 | + result = func() |
147 | + except: |
148 | + # We have an exception from func already, so suppress cleanup errors. |
149 | + run_cleanups(cleanup_funcs) |
150 | + raise |
151 | + else: |
152 | + # No exception from func, so allow the first exception from |
153 | + # cleanup_funcs to propagate if one occurs (but only after running all |
154 | + # of them). |
155 | + exc_info = None |
156 | + for cleanup in cleanup_funcs: |
157 | + # XXX: Hmm, if KeyboardInterrupt arrives at exactly this line, we |
158 | + # won't run all cleanups... perhaps we should temporarily install a |
159 | + # SIGINT handler? |
160 | + if exc_info is None: |
161 | + try: |
162 | + cleanup() |
163 | + except: |
164 | + # This is the first cleanup to fail, so remember its |
165 | + # details. |
166 | + exc_info = sys.exc_info() |
167 | + else: |
168 | + # We already have an exception to propagate, so log any errors |
169 | + # but don't propagate them. |
170 | + run_cleanup(cleanup) |
171 | + if exc_info is not None: |
172 | + raise exc_info[0], exc_info[1], exc_info[2] |
173 | + # No error, so we can return the result |
174 | + return result |
175 | + |
176 | + |
177 | |
178 | === modified file 'bzrlib/decorators.py' |
179 | --- bzrlib/decorators.py 2009-03-23 14:59:43 +0000 |
180 | +++ bzrlib/decorators.py 2009-09-25 02:11:12 +0000 |
181 | @@ -24,6 +24,9 @@ |
182 | |
183 | import sys |
184 | |
185 | +from bzrlib.cleanup import run_cleanup |
186 | +from bzrlib import trace |
187 | + |
188 | |
189 | def _get_parameters(func): |
190 | """Recreate the parameters for a function using introspection. |
191 | @@ -204,6 +207,31 @@ |
192 | return write_locked |
193 | |
194 | |
195 | +def cleanup_method(unbound): |
196 | + """Decorate unbound... """ |
197 | + def cleanup_wrapper(*args, **kwargs): |
198 | + run_cleanup(unbound, *args, **kwargs) |
199 | + cleanup_wrapper.__doc__ = unbound.__doc__ |
200 | + cleanup_wrapper.__name__ = unbound.__name__ |
201 | + return cleanup_wrapper |
202 | + |
203 | + |
204 | +def only_raises(*errors): |
205 | + def decorator(unbound): |
206 | + def wrapped(*args, **kwargs): |
207 | + try: |
208 | + return unbound(*args, **kwargs) |
209 | + except errors: |
210 | + raise |
211 | + except: |
212 | + trace.mutter('Error suppressed by only_raises:') |
213 | + trace.log_exception_quietly() |
214 | + wrapped.__doc__ = unbound.__doc__ |
215 | + wrapped.__name__ = unbound.__name__ |
216 | + return wrapped |
217 | + return decorator |
218 | + |
219 | + |
220 | # Default is more functionality, 'bzr' the commandline will request fast |
221 | # versions. |
222 | needs_read_lock = _pretty_needs_read_lock |
223 | |
224 | === modified file 'bzrlib/lockable_files.py' |
225 | --- bzrlib/lockable_files.py 2009-07-27 05:39:01 +0000 |
226 | +++ bzrlib/lockable_files.py 2009-09-25 02:11:12 +0000 |
227 | @@ -32,8 +32,7 @@ |
228 | """) |
229 | |
230 | from bzrlib.decorators import ( |
231 | - needs_read_lock, |
232 | - needs_write_lock, |
233 | + only_raises, |
234 | ) |
235 | from bzrlib.symbol_versioning import ( |
236 | deprecated_in, |
237 | @@ -221,6 +220,7 @@ |
238 | """Setup a write transaction.""" |
239 | self._set_transaction(transactions.WriteTransaction()) |
240 | |
241 | + @only_raises(errors.LockNotHeld, errors.LockBroken) |
242 | def unlock(self): |
243 | if not self._lock_mode: |
244 | return lock.cant_unlock_not_held(self) |
245 | |
246 | === modified file 'bzrlib/lockdir.py' |
247 | --- bzrlib/lockdir.py 2009-07-27 05:24:02 +0000 |
248 | +++ bzrlib/lockdir.py 2009-09-25 02:11:12 +0000 |
249 | @@ -112,6 +112,7 @@ |
250 | lock, |
251 | ) |
252 | import bzrlib.config |
253 | +from bzrlib.decorators import only_raises |
254 | from bzrlib.errors import ( |
255 | DirectoryNotEmpty, |
256 | FileExists, |
257 | @@ -286,6 +287,7 @@ |
258 | info_bytes) |
259 | return tmpname |
260 | |
261 | + @only_raises(LockNotHeld, LockBroken) |
262 | def unlock(self): |
263 | """Release a held lock |
264 | """ |
265 | |
266 | === modified file 'bzrlib/progress.py' |
267 | --- bzrlib/progress.py 2009-09-24 04:55:10 +0000 |
268 | +++ bzrlib/progress.py 2009-09-25 02:11:12 +0000 |
269 | @@ -30,6 +30,7 @@ |
270 | from bzrlib import ( |
271 | errors, |
272 | ) |
273 | +from bzrlib.decorators import cleanup_method |
274 | from bzrlib.trace import mutter |
275 | from bzrlib.symbol_versioning import ( |
276 | deprecated_function, |
277 | @@ -130,6 +131,7 @@ |
278 | def tick(self): |
279 | self.update(self.msg) |
280 | |
281 | + @cleanup_method |
282 | def finished(self): |
283 | if self.progress_view: |
284 | self.progress_view.task_finished(self) |
285 | @@ -247,6 +249,7 @@ |
286 | # next update should not throttle |
287 | self.last_update = now - self.MIN_PAUSE - 1 |
288 | |
289 | + @cleanup_method |
290 | def finished(self): |
291 | """Return this bar to its progress stack.""" |
292 | self.clear() |
293 | |
294 | === modified file 'bzrlib/remote.py' |
295 | --- bzrlib/remote.py 2009-09-24 05:31:23 +0000 |
296 | +++ bzrlib/remote.py 2009-09-25 02:11:12 +0000 |
297 | @@ -33,7 +33,7 @@ |
298 | ) |
299 | from bzrlib.branch import BranchReferenceFormat |
300 | from bzrlib.bzrdir import BzrDir, RemoteBzrDirFormat |
301 | -from bzrlib.decorators import needs_read_lock, needs_write_lock |
302 | +from bzrlib.decorators import needs_read_lock, needs_write_lock, only_raises |
303 | from bzrlib.errors import ( |
304 | NoSuchRevision, |
305 | SmartProtocolError, |
306 | @@ -1082,6 +1082,7 @@ |
307 | else: |
308 | raise errors.UnexpectedSmartServerResponse(response) |
309 | |
310 | + @only_raises(errors.LockNotHeld, errors.LockBroken) |
311 | def unlock(self): |
312 | if not self._lock_count: |
313 | return lock.cant_unlock_not_held(self) |
314 | @@ -2382,6 +2383,7 @@ |
315 | return |
316 | raise errors.UnexpectedSmartServerResponse(response) |
317 | |
318 | + @only_raises(errors.LockNotHeld, errors.LockBroken) |
319 | def unlock(self): |
320 | try: |
321 | self._lock_count -= 1 |
322 | |
323 | === modified file 'bzrlib/revisiontree.py' |
324 | --- bzrlib/revisiontree.py 2009-08-28 05:00:33 +0000 |
325 | +++ bzrlib/revisiontree.py 2009-09-25 02:11:12 +0000 |
326 | @@ -25,6 +25,7 @@ |
327 | symbol_versioning, |
328 | tree, |
329 | ) |
330 | +from bzrlib.decorators import cleanup_method |
331 | |
332 | |
333 | class RevisionTree(tree.Tree): |
334 | @@ -180,6 +181,7 @@ |
335 | return '<%s instance at %x, rev_id=%r>' % ( |
336 | self.__class__.__name__, id(self), self._revision_id) |
337 | |
338 | + @cleanup_method |
339 | def unlock(self): |
340 | self._repository.unlock() |
341 | |
342 | |
343 | === modified file 'bzrlib/tests/__init__.py' |
344 | --- bzrlib/tests/__init__.py 2009-09-24 04:54:19 +0000 |
345 | +++ bzrlib/tests/__init__.py 2009-09-25 02:11:12 +0000 |
346 | @@ -3854,6 +3854,188 @@ |
347 | This function can be replaced if you need to change the default test |
348 | suite on a global basis, but it is not encouraged. |
349 | """ |
350 | +<<<<<<< TREE |
351 | +======= |
352 | + testmod_names = [ |
353 | + 'bzrlib.doc', |
354 | + 'bzrlib.tests.blackbox', |
355 | + 'bzrlib.tests.commands', |
356 | + 'bzrlib.tests.per_branch', |
357 | + 'bzrlib.tests.per_bzrdir', |
358 | + 'bzrlib.tests.per_interrepository', |
359 | + 'bzrlib.tests.per_intertree', |
360 | + 'bzrlib.tests.per_inventory', |
361 | + 'bzrlib.tests.per_interbranch', |
362 | + 'bzrlib.tests.per_lock', |
363 | + 'bzrlib.tests.per_transport', |
364 | + 'bzrlib.tests.per_tree', |
365 | + 'bzrlib.tests.per_pack_repository', |
366 | + 'bzrlib.tests.per_repository', |
367 | + 'bzrlib.tests.per_repository_chk', |
368 | + 'bzrlib.tests.per_repository_reference', |
369 | + 'bzrlib.tests.per_versionedfile', |
370 | + 'bzrlib.tests.per_workingtree', |
371 | + 'bzrlib.tests.test__annotator', |
372 | + 'bzrlib.tests.test__chk_map', |
373 | + 'bzrlib.tests.test__dirstate_helpers', |
374 | + 'bzrlib.tests.test__groupcompress', |
375 | + 'bzrlib.tests.test__known_graph', |
376 | + 'bzrlib.tests.test__rio', |
377 | + 'bzrlib.tests.test__walkdirs_win32', |
378 | + 'bzrlib.tests.test_ancestry', |
379 | + 'bzrlib.tests.test_annotate', |
380 | + 'bzrlib.tests.test_api', |
381 | + 'bzrlib.tests.test_atomicfile', |
382 | + 'bzrlib.tests.test_bad_files', |
383 | + 'bzrlib.tests.test_bencode', |
384 | + 'bzrlib.tests.test_bisect_multi', |
385 | + 'bzrlib.tests.test_branch', |
386 | + 'bzrlib.tests.test_branchbuilder', |
387 | + 'bzrlib.tests.test_btree_index', |
388 | + 'bzrlib.tests.test_bugtracker', |
389 | + 'bzrlib.tests.test_bundle', |
390 | + 'bzrlib.tests.test_bzrdir', |
391 | + 'bzrlib.tests.test__chunks_to_lines', |
392 | + 'bzrlib.tests.test_cache_utf8', |
393 | + 'bzrlib.tests.test_chk_map', |
394 | + 'bzrlib.tests.test_chk_serializer', |
395 | + 'bzrlib.tests.test_chunk_writer', |
396 | + 'bzrlib.tests.test_clean_tree', |
397 | + 'bzrlib.tests.test_cleanup', |
398 | + 'bzrlib.tests.test_commands', |
399 | + 'bzrlib.tests.test_commit', |
400 | + 'bzrlib.tests.test_commit_merge', |
401 | + 'bzrlib.tests.test_config', |
402 | + 'bzrlib.tests.test_conflicts', |
403 | + 'bzrlib.tests.test_counted_lock', |
404 | + 'bzrlib.tests.test_crash', |
405 | + 'bzrlib.tests.test_decorators', |
406 | + 'bzrlib.tests.test_delta', |
407 | + 'bzrlib.tests.test_debug', |
408 | + 'bzrlib.tests.test_deprecated_graph', |
409 | + 'bzrlib.tests.test_diff', |
410 | + 'bzrlib.tests.test_directory_service', |
411 | + 'bzrlib.tests.test_dirstate', |
412 | + 'bzrlib.tests.test_email_message', |
413 | + 'bzrlib.tests.test_eol_filters', |
414 | + 'bzrlib.tests.test_errors', |
415 | + 'bzrlib.tests.test_export', |
416 | + 'bzrlib.tests.test_extract', |
417 | + 'bzrlib.tests.test_fetch', |
418 | + 'bzrlib.tests.test_fifo_cache', |
419 | + 'bzrlib.tests.test_filters', |
420 | + 'bzrlib.tests.test_ftp_transport', |
421 | + 'bzrlib.tests.test_foreign', |
422 | + 'bzrlib.tests.test_generate_docs', |
423 | + 'bzrlib.tests.test_generate_ids', |
424 | + 'bzrlib.tests.test_globbing', |
425 | + 'bzrlib.tests.test_gpg', |
426 | + 'bzrlib.tests.test_graph', |
427 | + 'bzrlib.tests.test_groupcompress', |
428 | + 'bzrlib.tests.test_hashcache', |
429 | + 'bzrlib.tests.test_help', |
430 | + 'bzrlib.tests.test_hooks', |
431 | + 'bzrlib.tests.test_http', |
432 | + 'bzrlib.tests.test_http_response', |
433 | + 'bzrlib.tests.test_https_ca_bundle', |
434 | + 'bzrlib.tests.test_identitymap', |
435 | + 'bzrlib.tests.test_ignores', |
436 | + 'bzrlib.tests.test_index', |
437 | + 'bzrlib.tests.test_info', |
438 | + 'bzrlib.tests.test_inv', |
439 | + 'bzrlib.tests.test_inventory_delta', |
440 | + 'bzrlib.tests.test_knit', |
441 | + 'bzrlib.tests.test_lazy_import', |
442 | + 'bzrlib.tests.test_lazy_regex', |
443 | + 'bzrlib.tests.test_lock', |
444 | + 'bzrlib.tests.test_lockable_files', |
445 | + 'bzrlib.tests.test_lockdir', |
446 | + 'bzrlib.tests.test_log', |
447 | + 'bzrlib.tests.test_lru_cache', |
448 | + 'bzrlib.tests.test_lsprof', |
449 | + 'bzrlib.tests.test_mail_client', |
450 | + 'bzrlib.tests.test_memorytree', |
451 | + 'bzrlib.tests.test_merge', |
452 | + 'bzrlib.tests.test_merge3', |
453 | + 'bzrlib.tests.test_merge_core', |
454 | + 'bzrlib.tests.test_merge_directive', |
455 | + 'bzrlib.tests.test_missing', |
456 | + 'bzrlib.tests.test_msgeditor', |
457 | + 'bzrlib.tests.test_multiparent', |
458 | + 'bzrlib.tests.test_mutabletree', |
459 | + 'bzrlib.tests.test_nonascii', |
460 | + 'bzrlib.tests.test_options', |
461 | + 'bzrlib.tests.test_osutils', |
462 | + 'bzrlib.tests.test_osutils_encodings', |
463 | + 'bzrlib.tests.test_pack', |
464 | + 'bzrlib.tests.test_patch', |
465 | + 'bzrlib.tests.test_patches', |
466 | + 'bzrlib.tests.test_permissions', |
467 | + 'bzrlib.tests.test_plugins', |
468 | + 'bzrlib.tests.test_progress', |
469 | + 'bzrlib.tests.test_read_bundle', |
470 | + 'bzrlib.tests.test_reconcile', |
471 | + 'bzrlib.tests.test_reconfigure', |
472 | + 'bzrlib.tests.test_registry', |
473 | + 'bzrlib.tests.test_remote', |
474 | + 'bzrlib.tests.test_rename_map', |
475 | + 'bzrlib.tests.test_repository', |
476 | + 'bzrlib.tests.test_revert', |
477 | + 'bzrlib.tests.test_revision', |
478 | + 'bzrlib.tests.test_revisionspec', |
479 | + 'bzrlib.tests.test_revisiontree', |
480 | + 'bzrlib.tests.test_rio', |
481 | + 'bzrlib.tests.test_rules', |
482 | + 'bzrlib.tests.test_sampler', |
483 | + 'bzrlib.tests.test_selftest', |
484 | + 'bzrlib.tests.test_serializer', |
485 | + 'bzrlib.tests.test_setup', |
486 | + 'bzrlib.tests.test_sftp_transport', |
487 | + 'bzrlib.tests.test_shelf', |
488 | + 'bzrlib.tests.test_shelf_ui', |
489 | + 'bzrlib.tests.test_smart', |
490 | + 'bzrlib.tests.test_smart_add', |
491 | + 'bzrlib.tests.test_smart_request', |
492 | + 'bzrlib.tests.test_smart_transport', |
493 | + 'bzrlib.tests.test_smtp_connection', |
494 | + 'bzrlib.tests.test_source', |
495 | + 'bzrlib.tests.test_ssh_transport', |
496 | + 'bzrlib.tests.test_status', |
497 | + 'bzrlib.tests.test_store', |
498 | + 'bzrlib.tests.test_strace', |
499 | + 'bzrlib.tests.test_subsume', |
500 | + 'bzrlib.tests.test_switch', |
501 | + 'bzrlib.tests.test_symbol_versioning', |
502 | + 'bzrlib.tests.test_tag', |
503 | + 'bzrlib.tests.test_testament', |
504 | + 'bzrlib.tests.test_textfile', |
505 | + 'bzrlib.tests.test_textmerge', |
506 | + 'bzrlib.tests.test_timestamp', |
507 | + 'bzrlib.tests.test_trace', |
508 | + 'bzrlib.tests.test_transactions', |
509 | + 'bzrlib.tests.test_transform', |
510 | + 'bzrlib.tests.test_transport', |
511 | + 'bzrlib.tests.test_transport_log', |
512 | + 'bzrlib.tests.test_tree', |
513 | + 'bzrlib.tests.test_treebuilder', |
514 | + 'bzrlib.tests.test_tsort', |
515 | + 'bzrlib.tests.test_tuned_gzip', |
516 | + 'bzrlib.tests.test_ui', |
517 | + 'bzrlib.tests.test_uncommit', |
518 | + 'bzrlib.tests.test_upgrade', |
519 | + 'bzrlib.tests.test_upgrade_stacked', |
520 | + 'bzrlib.tests.test_urlutils', |
521 | + 'bzrlib.tests.test_version', |
522 | + 'bzrlib.tests.test_version_info', |
523 | + 'bzrlib.tests.test_weave', |
524 | + 'bzrlib.tests.test_whitebox', |
525 | + 'bzrlib.tests.test_win32utils', |
526 | + 'bzrlib.tests.test_workingtree', |
527 | + 'bzrlib.tests.test_workingtree_4', |
528 | + 'bzrlib.tests.test_wsgi', |
529 | + 'bzrlib.tests.test_xml', |
530 | + ] |
531 | +>>>>>>> MERGE-SOURCE |
532 | |
533 | loader = TestUtil.TestLoader() |
534 | |
535 | |
536 | === added file 'bzrlib/tests/test_cleanup.py' |
537 | --- bzrlib/tests/test_cleanup.py 1970-01-01 00:00:00 +0000 |
538 | +++ bzrlib/tests/test_cleanup.py 2009-09-25 02:11:12 +0000 |
539 | @@ -0,0 +1,259 @@ |
540 | +# Copyright (C) 2009 Canonical Ltd |
541 | +# |
542 | +# This program is free software; you can redistribute it and/or modify |
543 | +# it under the terms of the GNU General Public License as published by |
544 | +# the Free Software Foundation; either version 2 of the License, or |
545 | +# (at your option) any later version. |
546 | +# |
547 | +# This program is distributed in the hope that it will be useful, |
548 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
549 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
550 | +# GNU General Public License for more details. |
551 | +# |
552 | +# You should have received a copy of the GNU General Public License |
553 | +# along with this program; if not, write to the Free Software |
554 | +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA |
555 | + |
556 | +from cStringIO import StringIO |
557 | +import re |
558 | + |
559 | +from bzrlib.cleanup import ( |
560 | + do_with_cleanups, |
561 | + run_cleanup, |
562 | + ) |
563 | +from bzrlib.tests import TestCase |
564 | +from bzrlib import ( |
565 | + debug, |
566 | + trace, |
567 | + ) |
568 | + |
569 | + |
570 | +class CleanupsTestCase(TestCase): |
571 | + |
572 | + def setUp(self): |
573 | + super(CleanupsTestCase, self).setUp() |
574 | + self.call_log = [] |
575 | + |
576 | + def no_op_cleanup(self): |
577 | + self.call_log.append('no_op_cleanup') |
578 | + |
579 | + def assertLogContains(self, regex): |
580 | + log = self._get_log(keep_log_file=True) |
581 | + self.assertContainsRe(log, regex, re.DOTALL) |
582 | + |
583 | + def failing_cleanup(self): |
584 | + self.call_log.append('failing_cleanup') |
585 | + raise Exception("failing_cleanup goes boom!") |
586 | + |
587 | + |
588 | +class TestRunCleanup(CleanupsTestCase): |
589 | + |
590 | + def test_no_errors(self): |
591 | + """The function passed to run_cleanup is run.""" |
592 | + self.assertTrue(run_cleanup(self.no_op_cleanup)) |
593 | + self.assertEqual(['no_op_cleanup'], self.call_log) |
594 | + |
595 | + def test_cleanup_with_args_kwargs(self): |
596 | + def func_taking_args_kwargs(*args, **kwargs): |
597 | + self.call_log.append(('func', args, kwargs)) |
598 | + run_cleanup(func_taking_args_kwargs, 'an arg', kwarg='foo') |
599 | + self.assertEqual( |
600 | + [('func', ('an arg',), {'kwarg': 'foo'})], self.call_log) |
601 | + |
602 | + def test_cleanup_error(self): |
603 | + """An error from the cleanup function is logged by run_cleanup, but not |
604 | + propagated. |
605 | + |
606 | + This is there's no way for run_cleanup to know if there's an existing |
607 | + exception in this situation:: |
608 | + try: |
609 | + some_func() |
610 | + finally: |
611 | + run_cleanup(cleanup_func) |
612 | + So, the best run_cleanup can do is always log errors but never raise |
613 | + them. |
614 | + """ |
615 | + self.assertFalse(run_cleanup(self.failing_cleanup)) |
616 | + self.assertLogContains('Cleanup failed:.*failing_cleanup goes boom') |
617 | + |
618 | + def test_cleanup_error_debug_flag(self): |
619 | + """The -Dcleanup debug flag causes cleanup errors to be reported to the |
620 | + user. |
621 | + """ |
622 | + log = StringIO() |
623 | + trace.push_log_file(log) |
624 | + debug.debug_flags.add('cleanup') |
625 | + self.assertFalse(run_cleanup(self.failing_cleanup)) |
626 | + self.assertContainsRe( |
627 | + log.getvalue(), |
628 | + "bzr: warning: Cleanup failed:.*failing_cleanup goes boom") |
629 | + |
630 | + def test_prior_error_cleanup_succeeds(self): |
631 | + """Calling run_cleanup from a finally block will not interfere with an |
632 | + exception from the try block. |
633 | + """ |
634 | + def failing_operation(): |
635 | + try: |
636 | + 1/0 |
637 | + finally: |
638 | + run_cleanup(self.no_op_cleanup) |
639 | + self.assertRaises(ZeroDivisionError, failing_operation) |
640 | + self.assertEqual(['no_op_cleanup'], self.call_log) |
641 | + |
642 | + def test_prior_error_cleanup_fails(self): |
643 | + """Calling run_cleanup from a finally block will not interfere with an |
644 | + exception from the try block even when the cleanup itself raises an |
645 | + exception. |
646 | + |
647 | + The cleanup exception will be logged. |
648 | + """ |
649 | + def failing_operation(): |
650 | + try: |
651 | + 1/0 |
652 | + finally: |
653 | + run_cleanup(self.failing_cleanup) |
654 | + self.assertRaises(ZeroDivisionError, failing_operation) |
655 | + self.assertLogContains('Cleanup failed:.*failing_cleanup goes boom') |
656 | + |
657 | + |
658 | +#class TestRunCleanupReportingErrors(CleanupsTestCase): |
659 | +# |
660 | +# def test_cleanup_error_reported(self): |
661 | +# xxx |
662 | + |
663 | + |
664 | +class TestDoWithCleanups(CleanupsTestCase): |
665 | + |
666 | + def trivial_func(self): |
667 | + self.call_log.append('trivial_func') |
668 | + return 'trivial result' |
669 | + |
670 | + def test_runs_func(self): |
671 | + """do_with_cleanups runs the function it is given, and returns the |
672 | + result. |
673 | + """ |
674 | + result = do_with_cleanups(self.trivial_func, []) |
675 | + self.assertEqual('trivial result', result) |
676 | + |
677 | + def test_runs_cleanups(self): |
678 | + """Cleanup functions are run (in the given order).""" |
679 | + cleanup_func_1 = lambda: self.call_log.append('cleanup 1') |
680 | + cleanup_func_2 = lambda: self.call_log.append('cleanup 2') |
681 | + do_with_cleanups(self.trivial_func, [cleanup_func_1, cleanup_func_2]) |
682 | + self.assertEqual( |
683 | + ['trivial_func', 'cleanup 1', 'cleanup 2'], self.call_log) |
684 | + |
685 | + def failing_func(self): |
686 | + self.call_log.append('failing_func') |
687 | + 1/0 |
688 | + |
689 | + def test_func_error_propagates(self): |
690 | + """Errors from the main function are propagated (after running |
691 | + cleanups). |
692 | + """ |
693 | + self.assertRaises( |
694 | + ZeroDivisionError, do_with_cleanups, self.failing_func, |
695 | + [self.no_op_cleanup]) |
696 | + self.assertEqual(['failing_func', 'no_op_cleanup'], self.call_log) |
697 | + |
698 | + def test_func_error_trumps_cleanup_error(self): |
699 | + """Errors from the main function a propagated even if a cleanup raises |
700 | + an error. |
701 | + |
702 | + The cleanup error is be logged. |
703 | + """ |
704 | + self.assertRaises( |
705 | + ZeroDivisionError, do_with_cleanups, self.failing_func, |
706 | + [self.failing_cleanup]) |
707 | + self.assertLogContains('Cleanup failed:.*failing_cleanup goes boom') |
708 | + |
709 | + def test_func_passes_and_error_from_cleanup(self): |
710 | + """An error from a cleanup is propagated when the main function doesn't |
711 | + raise an error. Later cleanups are still executed. |
712 | + """ |
713 | + exc = self.assertRaises( |
714 | + Exception, do_with_cleanups, self.trivial_func, |
715 | + [self.failing_cleanup, self.no_op_cleanup]) |
716 | + self.assertEqual('failing_cleanup goes boom!', exc.args[0]) |
717 | + self.assertEqual( |
718 | + ['trivial_func', 'failing_cleanup', 'no_op_cleanup'], |
719 | + self.call_log) |
720 | + |
721 | + def test_multiple_cleanup_failures(self): |
722 | + """When multiple cleanups fail (as tends to happen when something has |
723 | + gone wrong), the first error is propagated, and subsequent errors are |
724 | + logged. |
725 | + """ |
726 | + cleanups = self.make_two_failing_cleanup_funcs() |
727 | + self.assertRaises(ErrorA, do_with_cleanups, self.trivial_func, |
728 | + cleanups) |
729 | + self.assertLogContains('Cleanup failed:.*ErrorB') |
730 | + log = self._get_log(keep_log_file=True) |
731 | + self.assertFalse('ErrorA' in log) |
732 | + |
733 | + def make_two_failing_cleanup_funcs(self): |
734 | + def raise_a(): |
735 | + raise ErrorA('Error A') |
736 | + def raise_b(): |
737 | + raise ErrorB('Error B') |
738 | + return [raise_a, raise_b] |
739 | + |
740 | + def test_multiple_cleanup_failures_debug_flag(self): |
741 | + log = StringIO() |
742 | + trace.push_log_file(log) |
743 | + debug.debug_flags.add('cleanup') |
744 | + cleanups = self.make_two_failing_cleanup_funcs() |
745 | + self.assertRaises(ErrorA, do_with_cleanups, self.trivial_func, cleanups) |
746 | + self.assertContainsRe( |
747 | + log.getvalue(), "bzr: warning: Cleanup failed:.*Error B\n") |
748 | + self.assertEqual(1, log.getvalue().count('bzr: warning:'), |
749 | + log.getvalue()) |
750 | + |
751 | + def test_func_and_cleanup_errors_debug_flag(self): |
752 | + log = StringIO() |
753 | + trace.push_log_file(log) |
754 | + debug.debug_flags.add('cleanup') |
755 | + cleanups = self.make_two_failing_cleanup_funcs() |
756 | + self.assertRaises(ZeroDivisionError, do_with_cleanups, |
757 | + self.failing_func, cleanups) |
758 | + self.assertContainsRe( |
759 | + log.getvalue(), "bzr: warning: Cleanup failed:.*Error A\n") |
760 | + self.assertContainsRe( |
761 | + log.getvalue(), "bzr: warning: Cleanup failed:.*Error B\n") |
762 | + self.assertEqual(2, log.getvalue().count('bzr: warning:')) |
763 | + |
764 | + def test_func_may_mutate_cleanups(self): |
765 | + """The main func may mutate the cleanups before it returns. |
766 | + |
767 | + This allows a function to gradually add cleanups as it acquires |
768 | + resources, rather than planning all the cleanups up-front. |
769 | + """ |
770 | + # XXX: this is cute, but an object with an 'add_cleanup' method may |
771 | + # make a better API? |
772 | + cleanups_list = [] |
773 | + def func_that_adds_cleanups(): |
774 | + self.call_log.append('func_that_adds_cleanups') |
775 | + cleanups_list.append(self.no_op_cleanup) |
776 | + return 'result' |
777 | + result = do_with_cleanups(func_that_adds_cleanups, cleanups_list) |
778 | + self.assertEqual('result', result) |
779 | + self.assertEqual( |
780 | + ['func_that_adds_cleanups', 'no_op_cleanup'], self.call_log) |
781 | + |
782 | + def test_cleanup_error_debug_flag(self): |
783 | + """The -Dcleanup debug flag causes cleanup errors to be reported to the |
784 | + user. |
785 | + """ |
786 | + log = StringIO() |
787 | + trace.push_log_file(log) |
788 | + debug.debug_flags.add('cleanup') |
789 | + self.assertRaises(ZeroDivisionError, do_with_cleanups, |
790 | + self.failing_func, [self.failing_cleanup]) |
791 | + self.assertContainsRe( |
792 | + log.getvalue(), |
793 | + "bzr: warning: Cleanup failed:.*failing_cleanup goes boom") |
794 | + self.assertEqual(1, log.getvalue().count('bzr: warning:')) |
795 | + |
796 | + |
797 | +class ErrorA(Exception): pass |
798 | +class ErrorB(Exception): pass |
[Trying an RFC for a branch by cross-posting to both the merge proposal on
Launchpad, and the bazaar@ list.]
Hi all,
Here's the problem. In this example, Python gives no way for final_func to know
if try_func raised an error or not:
try:
final_ func()
try_func()
finally:
Not even by sys._getframe hacks, AFAICT. I can go into some gory details about
why not, but more important is what to do about it.
It's problematic because, in general, we don't want an error from final_func to ntRequests bugs are the most
override an error from try_func. This means that sometimes users get errors
about, say, abort_write_group failing, when what originally went wrong was
“connection lost”. The various TooManyConcurre
common symptom of this.
This brings me to <https:/ /bugs.launchpad .net/bzr/ +bug/429747>, and /code.launchpad .net/~spiv/ bzr/cleanup- hof/+merge/ 12318>.
<https:/
Bug 429747 proposes a higher- order-function that all cleanups should use to at
least give us a common place to control policy about log vs. raise, etc. That
branch implements that, as a function called run_cleanup (and some variations).
It lives in a new module, bzrlib.cleanups. I have tried to give the new module
and its contents fairly reasonable docstrings, and clear tests.
The downside of run_cleanup is that it will suppress some exceptions that should
be propagated, but that seems to be unavoidable given the limitations of Python.
(And note that developers can always use -Dcleanup to get UI warnings about
failures in run_cleanup).
The branch also implements something called do_with_cleanups, which can actually
be pretty robust and correct, at the cost of making simple callsites a bit
uglier. I have a follow-on patch that adjusts bzrlib.commit to use this, which
would fix some outstanding bugs.
So, here's the RFC: I think the policy should be:
* use run_cleanup if a cleanup failure is likely to be unimportant to a user write_group( suppress_ error=True) . Then idiomatic try/finally is safe.
(or at least, the chance of it being important is < the chance of the main
func raising an error that should not be obscured).
* consider changing the cleanup function itself to never raise errors, e.g.
abort_
Again, only applicable if failures during cleanup are "uninteresting".
* use run_cleanups if you are running multiple cleanups; it takes care of
running them all even if an exception happens.
* if you want very correct behaviour, use do_with_cleanups. This tends to make
simple callsites a bit uglier, but in already complex code (like commit) it
may be no worse.
If agreed, I can add this to HACKING.txt.
I don't think we necessarily need to update all existing try/finally blocks at
once, but we should at least fix the cases that have been the source of bug
reports.
-Andrew.