Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Need an end of lexical scope action which can die normally

I need the ability to add actions to the end of a lexical block where the action might die. And I need the exception to be thrown normally and be able to be caught normally.

Unfortunately, Perl special cases exceptions during DESTROY both by adding "(in cleanup)" to the message and making them untrappable. For example:

{
    package Guard;

    use strict;
    use warnings;

    sub new {
        my $class = shift;
        my $code = shift;
        return bless $code, $class;
    }

    sub DESTROY {
        my $self = shift;
        $self->();
    }
}

use Test::More tests => 2;

my $guard_triggered = 0;

ok !eval {
    my $guard = Guard->new(
#line 24
        sub {
            $guard_triggered++;
            die "En guarde!"
        }
    );
    1;
}, "the guard died";

is $@, "En guarde! at $@ line 24\n",    "with the right error message";
is $guard_triggered, 1,                 "the guard worked";

I want that to pass. Currently the exception is totally swallowed by the eval.

This is for Test::Builder2, so I cannot use anything but pure Perl.

The underlying issue is I have code like this:

{
    $self->setup;

    $user_code->();

    $self->cleanup;
}

That cleanup must happen even if the $user_code dies, else $self gets into a weird state. So I did this:

{
    $self->setup;

    my $guard = Guard->new(sub { $self->cleanup });

    $user_code->();
}

The complexity comes because the cleanup runs arbitrary user code and it is a use case where that code will die. I expect that exception to be trappable and unaltered by the guard.

I'm avoiding wrapping everything in eval blocks because of the way that alters the stack.

like image 553
Schwern Avatar asked Jan 05 '11 23:01

Schwern


1 Answers

Is this semantically sound? From what I understand, you have this (in pseudocode):

try {
    user_code(); # might throw
}
finally {
    clean_up(); # might throw
}

There are two possibilities:

  • user_code() and clean_up() will never throw in the same run, in which case you can just write it as sequential code without any funny guard business and it will work.
  • user_code() and clean_up() may, at some point, both throw in the same run.

If both functions may throw, then you have two active exceptions. I don't know any language which can handle multiple active currently thrown exceptions, and I'm sure there's a good reason for this. Perl adds (in cleanup) and makes the exception untrappable; C++ calls terminate(), Java drops the original exception silently, etc etc.

If you have just come out of an eval in which both user_code() and cleanup() threw exceptions, what do you expect to find in $@?

Usually this indicates you need to handle the cleanup exception locally, perhaps by ignoring the cleanup exception:

try {
    user_code();
}
finally {
    try {
        clean_up();
    }
    catch {
        # handle exception locally, cannot propagate further
    }
}

or you need to choose an exception to ignore when both throw (which is what DVK's solution does; it ignores the user_code() exception):

try {
    user_code();
}
catch {
    $user_except = $@;
}
try {
    cleanup();
}
catch {
    $cleanup_except = $@;
}
die $cleanup_except if $cleanup_except; # if both threw, this takes precedence
die $user_except if $user_except;

or somehow combine the two exceptions into one exception object:

try {
    user_code();
}
catch {
    try {
        clean_up();
    }
    catch {
        throw CompositeException; # combines user_code() and clean_up() exceptions
    }
    throw; # rethrow user_code() exception
}
clean_up();

I feel there should be a way to avoid repeating the clean_up() line in the above example, but I can't think of it.

In short, without knowing what you think should happen when both parts throw, your problem cannot be answered.

like image 80
Philip Potter Avatar answered Sep 21 '22 04:09

Philip Potter