Merge lp:~spiv/bzr/cleanup-hof into lp:bzr

Proposed by Andrew Bennetts
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
Reviewer Review Type Date Requested Status
bzr-core Pending
Review via email: mp+12318@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Andrew Bennetts (spiv) wrote :

[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 TooManyConcurrentRequests bugs are the most
common symptom of this.

This brings me to <https://bugs.launchpad.net/bzr/+bug/429747>, and
<https://code.launchpad.net/~spiv/bzr/cleanup-hof/+merge/12318>.

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
   (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_group(suppress_error=True). Then idiomatic try/finally is safe.
   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.

Revision history for this message
Martin Pool (mbp) wrote :
Download full text (4.5 KiB)

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 TooManyConcurrentRequests bugs are the most
> common symptom of this.
>
> This brings me to <https://bugs.launchpad.net/bzr/+bug/429747>, and
> <https://code.launchpad.net/~spiv/bzr/cleanup-hof/+merge/12318>.
>
> 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
>   (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_group(suppress_error=True).  Then idiomatic try/finally is safe.
>   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
...

Read more...

Revision history for this message
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_func(no_error)

Revision history for this message
Andrew Bennetts (spiv) wrote :
Download full text (4.3 KiB)

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-order-function that runs them in order, that's great.

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='warn')", or
on_error='log'/'ignore'/'raise'. I'm not sure which we'd like to have as
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:
>
> * TooManyConcurrentRequests as a knock-on effect from a previous
> operation being interrupted either because of a client-side bug or
> error, or a connection dropping
> * Unlock failing because ...

Read more...

Revision history for this message
Andrew Bennetts (spiv) wrote :

Vincent Ladeuil wrote:
[...]
> try:
> no_error = False
> try_func()
> no_error = True
> finally:
> final_func(no_error)

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.

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

> Vincent Ladeuil wrote:
> [...]
> > try:
> > no_error = False
> > try_func()
> > no_error = True
> > finally:
> > final_func(no_error)
>
> 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(?connection?) is still in use, then we can be pretty sure that we were in fact interrupted. Now what should it do?

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

Revision history for this message
Martin Pool (mbp) wrote :
Download full text (12.0 KiB)

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-order-function that runs them in order, that's great.
>
> 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='warn')", or
> on_error='log'/'ignore'/'raise'.  I'm not sure which we'd like to have as
> 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...

Revision history for this message
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_func(errflag)

In earlier Pythae you must do the ugly but equivalent

try:
    errflag = "Impossible is nothing."
    try:
        try_func()
    except:
        errflag = "Uh oh ..."
finally:
    final_func(errflag)

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

Revision history for this message
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://launchpad.net/~mbp/>

Revision history for this message
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.

Revision history for this message
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

Revision history for this message
Andrew Bennetts (spiv) wrote :
Download full text (5.1 KiB)

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='warn')", or
> > on_error='log'/'ignore'/'raise'.  I'm not sure which we'd like to have as
> > 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...

Read more...

Revision history for this message
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://launchpad.net/~mbp/>

Revision history for this message
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://launchpad.net/~mbp/>

Revision history for this message
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?

Revision history for this message
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.get_master_branch -> Branch.open ->
   BzrDir.open -> do_catching_redirections... eventually 'BzrDir.open' RPC fails
   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?

Revision history for this message
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.get_master_branch -> Branch.open ->
>   BzrDir.open -> do_catching_redirections... eventually 'BzrDir.open' RPC fails
>   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://launchpad.net/~mbp/>

Revision history for this message
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_raises(errors.LockNotHeld, errors.LockBroken)
    def unlock(self):

And so far it's looking promising, although there's still a little bit of test
suite fall out...

lp:~spiv/bzr/cleanup-hof updated
4675. By Andrew Bennetts

Add some experimental decorators: @only_raises(..) and @cleanup_method.

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

So what is the status of this submission? Is it superseded by your other work?

lp:~spiv/bzr/cleanup-hof updated
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

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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