Need an end of lexical scope action which can die normally
Asked Answered
B

2

7

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.

Beneficent answered 5/1, 2011 at 23:49 Comment(5)
search.cpan.org/~rgarcia/perl-5.10.0-RC2/pod/… explains the '(in cleanup)' mechanism, cpansearch.perl.org/src/JESSE/perl-5.12.2/ext/XS-APItest/t/… shows using it. this is all XS related, but maybe it will help point you or someone else to a pure perl answer?Cenotaph
@Eric Thanks for the extra info, didn't know about that. I don't think I have any way of getting at that flag without XS. Sure would be handy!Beneficent
+1 for making me learn and remember a couple of cool Perl things as part of trying to answer (and wish I could give +100 for Test::Builder2 :). I think I may have found a possible solution for you - see the answer below - please critique if I didn't get your requirements rightPursuit
apologies for spazzing out, but why are you restricted to pure Perl and not XS for Test::Builder2?Pursuit
@Pursuit 80% of CPAN will depend on Test::Builder2 (via Test::Builder via Test::More). Test::Builder2 can have no external dependencies else anything it depends on cannot use test modules to test itself. If TB2 relies on a C compiler, then 80% of CPAN relies on a C compiler.Beneficent
I
2

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.

Indonesia answered 6/1, 2011 at 5:34 Comment(2)
IMHO the semantic uncertainty is not really very relevant - it's the un-trappability of DESTROY-wrapped exception that is the problem. I don't think that (in cleanup) "composite excetion approximation" that Perl employs is as big of an issue, if the resulting exception could have been actually caught by external eval which it can't be because of G_KEEPERR.Pursuit
@Pursuit and why do you think the DESTROY-wrapped exception is untrappable? It's because you're silently dropping any existing exception currently being thrown and not yet caught. DESTROY makes the exception untrappable because you're in a situation from which you cannot recover without losing information about what happened. You shouldn't be in a situation where exceptions can be thrown and silently dropped without being caught or causing the program to die. That loss of information kills exception safety by preventing higher levels from knowing about exceptional conditions in lower levels.Indonesia
P
0

UPDATE: The approach below doesn't seem to work as written as Eric noted!.

I'm leaving this answer up in case someone can wrangle it into working shape.

The problem is:

I expected that popping old global value back onto the global tied variable once the local one goes out of scope will involve a call to FETCH/STORE, but somehow it just happens silently without hitting the tied mechanism (the issue is irrelevant to exception handling).


Schwern - I'm not 100% sure you can use the tie technique (stolen from Perlmonks post by Abigail) for your use case - here's my attempt to do what I think you were trying to do

use Test::More tests => 6;

my $guard_triggered = 0;
sub user_cleanup { $guard_triggered++; die "En guarde!" }; # Line 4;
sub TIESCALAR {bless \(my $dummy) => shift}
sub FETCH     { user_cleanup(); }
sub STORE     {1;}
our $guard;
tie $guard => __PACKAGE__; # I don't think the actual value matters

sub x {
    my $x = 1; # Setup
    local $guard = "in x";
    my $y = 2; #user_code;
}

sub x2 {
    my $x = 1; # Setup
    local $guard = "in x2";
    die "you bastard"; #user_code;
}

ok !eval {
    x();
}, "the guard died";
is $@, "En guarde! at $0 line 4.\n",    "with the right error message";
is $guard_triggered, 1,                 "the guard worked";

ok !eval {
    x2();
}, "the guard died";
is $@, "En guarde! at $0 line 4.\n",    "with the right error message";
is $guard_triggered, 2,                 "the guard worked";

OUTPUT:

1..6
ok 1 - the guard died
ok 2 - with the right error message
ok 3 - the guard worked
ok 4 - the guard died
ok 5 - with the right error message
ok 6 - the guard worked
Pursuit answered 6/1, 2011 at 2:14 Comment(7)
the guard code does not seem to be executing at the end of scope with this method. in the event that the guard dies, it does so before all statements in the block have executed. place a print ... after the local linesCenotaph
@Eric - hmm.... you're right. I was expecting the FETCH to be called when global scope value needs to be restored but I was wrong and it instead calls it when you assign locals. Let me play with this some morePursuit
Interesting approach! Unfortunately I'd really rather avoid tying in Test::Builder2. Too finky.Beneficent
@Beneficent - hmm... I can't argue with that, but I don't see another alternative at the moment unfortunately besides wrapper evals or ties. I'll give it some more thought, and please post if you come up with an alternate solution - inquiring minds are very curious :)Pursuit
@Pursuit I wound up just using an eval realizing there's already a lot of other stuff in the stack. I'm still interested in an end-of-scope solution.Beneficent
@Beneficent - BTW, why is tying a problem? I don't have all that much experience directly using it (I'm sure plenty of modules I use tie internally - I know for sure Embperl stack does after having to debug the bloody thing for 3 weeks :)Pursuit
@Beneficent - I was able to make some progress with tying solution, though not 100% there (Apparently, it DOES call STORE on the exit from scope, I just need to figure out ironclad rules for figuring out from caller's stack data which STORE is outside the scope and which one inside). Tell me if you're interested and i'll try to spend some more time on it.Pursuit

© 2022 - 2024 — McMap. All rights reserved.