Exception-safety

Exceptions happen...How can we deal with them?

Answer: Write code that handles exceptions safely

or rather... to write "Exception-safe" code

What is Exception-safe code?

Exception-safe code doesn't

a) leak resources

b) corrupt data structures

How do you not leak resources?

Use a technique called RAII

Acquire resources in an object constructor...

..and free them in a destructor

RIAA


use strict;
use warnings;
use Try::Tiny;

package FileManager {
    use Moose;

    has 'file_handle' => (is => 'rw');

    sub BUILD {
        my $self = shift;

        open(my $fh, '<', '/tmp/myfile') || die ("Cant open /tmp/myfile");
        $self->file_handle($fh);
        print "FILE OPENED\n";
    };

    sub DEMOLISH {
        my $self = shift;
        my $fh = $self->file_handle();
        close($fh);
        print "I CLOSED THE FILE SAFELY\n";
    };
};

sub main {
    print "app starts here\n";

    try {
        func_with_resource();
    };

    print "I could carry on safely if I wanted to\n";
}

sub func_with_resource {
    my $file_manager = FileManager->new();

    die ("this function is just broken");

    # if the close() call were here... it'd never get run!
    # if we used complex try {} catch {} finally blocks we're more likely
    # to make a mistake!
}

main;
    

The Output

        [03:43|vim|: deckjs$]perl RIAA.pl
        app starts here
        FILE OPENED
        I CLOSED THE FILE SAFELY
        I could carry on safely if I wanted to
        

IO::File

use strict;
use warnings;
use IO::File;
use Try::Tiny;

sub main {
    print "app starts here\n";

    try {
        func_with_resource();
    };

    print "I could carry on safely if I wanted to\n";
}

sub func_with_resource {
    my $file_manager = IO::File->new('/tmp/myfile', 'r');

    die ("this function is just broken");

    # IO::File inherits from IO::Handle
}

main;
    

How do you not corrupt data structures?

Exception-safe functions offer a guarantee

The Basic Guarantee

The Strong Guarantee

The nothrow Guarantee

The Basic Guarantee


use strict;
use warnings;
use Try::Tiny;

my $emergency_contact = {
    name => 'Bill',
    phone => '07950322789'
};

sub main {
    print_contact();
    
    try {
        update_emergency_contact('Dave', '9832-83239-32');
    } catch {
        print "ERROR: $_\n";
        # try and recover from error...
    };

    print_contact();
}

sub update_emergency_contact {
    my ($new_name, $new_phone) = @_;

    $emergency_contact->{name} = $new_name;

    die "invalid phone" if ($new_phone !~ /^[0-9]*$/);
    $emergency_contact->{phone} = $new_phone;    

}

sub print_contact {
    print "Current name: ". $emergency_contact->{name} . "\n";
    print "Current phone: ". $emergency_contact->{phone} . "\n";
}

main();
    

The Output


    [03:43|vim|: deckjs$]perl basic_guarantee.pl 
    Current name: Bill
    Current phone: 07950322789
    ERROR: invalid phone at basic_guarantee.pl line 29.

    Current name: Dave
    Current phone: 07950322789
    [03:43|vim|: deckjs$] 
    

From basic to strong with the "copy and swap" pattern

The Strong Guarantee


use strict;
use warnings;
use Try::Tiny;

my $emergency_contact = {
    name => 'Bill',
    phone => '07950322789'
};

sub main {
    print_contact();
    
    try {
        update_emergency_contact('Dave', '9832-83239-32');
    } catch {
        print "ERROR: $_\n";
        # try and recover from error...
    };

    print_contact();
}

sub update_emergency_contact {
    my ($new_name, $new_phone) = @_;

    # FIRST PERFORM A DEEP COPY...
    use Storable 'dclone';
    my $copy = dclone($emergency_contact);

    # update the copy...
    # order doesn't have to change...
    $copy->{name} = $new_name;

    die "invalid phone" if ($new_phone !~ /^[0-9]*$/);
    $copy->{phone} = $new_phone;

    # THEN SWAP WHEN THE WHOLE OPERATION IS COMPLETE
    $emergency_contact = $copy;

}

sub print_contact {
    print "Current name: ". $emergency_contact->{name} . "\n";
    print "Current phone: ". $emergency_contact->{phone} . "\n";
}

main();
    

The Output


    [03:48|vim|: deckjs$] perl strong_guarantee.pl 
    Current name: Bill
    Current phone: 07950322789
    ERROR: invalid phone at strong_guarantee.pl line 35.

    Current name: Bill
    Current phone: 07950322789
    [03:48|vim|: deckjs$] 
    

This also works with Moose


use strict;
use warnings;
use Try::Tiny;

package Phone {
    use Moose;
    has 'number' => ('isa' => 'Int', is => 'rw');
};

package Contact {
    use Moose;
    has 'name' => ('isa' => 'Str', is => 'rw');
    has 'phone' => ('isa' => 'Phone', is => 'rw');
};

my $emergency_contact = Contact->new({
    name => 'Bill',
    phone => Phone->new({
        number => '07950322789'
    })
});

sub main {
    print_contact();
    
    try {
        update_emergency_contact('Dave', '9832-83239-32');
    } catch {
        print "ERROR: $_\n";
        # try and recover from error...
    };

    print_contact();
}

sub update_emergency_contact {
    my ($new_name, $new_phone) = @_;

    # FIRST PERFORM A DEEP COPY...
    use Storable 'dclone';
    my $copy = dclone($emergency_contact);

    # update the copy...
    # order doesn't have to change...
    $copy->name($new_name);
    $copy->phone->number($new_phone);

    # THEN SWAP WHEN THE WHOLE OPERATION IS COMPLETE
    $emergency_contact = $copy;

}

sub print_contact {
    print "Current name: ". $emergency_contact->name . "\n";
    print "Current phone: ". $emergency_contact->phone->number . "\n";
}

main();
    

The Output


    [03:51|vim|: deckjs$] perl strong_moose.pl 
    Current name: Bill
    Current phone: 07950322789
    ERROR: Attribute (number) does not pass the type constraint because: Validation failed for 'Int' with value 9832-83239-32 at accessor Phone::number (defined at strong_moose.pl line 8) line 4.
            Phone::number('Phone=HASH(0x23ecfd0)', '9832-83239-32') called at strong_moose.pl line 47
            main::update_emergency_contact('Dave', '9832-83239-32') called at strong_moose.pl line 28
            main::__ANON__() called at /usr/local/share/perl/5.14.2/Try/Tiny.pm line 76
            eval {...} called at /usr/local/share/perl/5.14.2/Try/Tiny.pm line 67
            Try::Tiny::try('CODE(0x2f539e0)', 'Try::Tiny::Catch=REF(0x23ee438)') called at strong_moose.pl line 32
            main::main() called at strong_moose.pl line 59

    Current name: Bill
    Current phone: 07950322789
    [03:51|vim|: deckjs$] 
    

Caveats


    use strict;
    use warnings;

    # All functions offer the strong guarantee

    sub clear_emergency_contacts {}
    sub add_emergency_contact {}

    sub main {

        my $dave = {
            name => 'Dave',
            phone => '3478-437-23'
        };

        clear_emergency_contacts();
        add_emergency_contact($dave);
    }

    main();

Phill's final thoughts

txn_guard and database transactions are how we've typically accomplished this

txn_guard, IO::File and are good examples of RAII

Use the strongest guarantee you can offer

You can't always offer nothrow (e.g. memory, files, network)

A function cannot make a guarantee stronger than the weakest guarantee of the functions it calls

Calling functions with the strong guarantee doesn't automatically mean your function has it

Scrutinise functions/handlers that contain multiple transactions

The "copy and swap" pattern incurs a temporary memory overhead

Patterns are not just failings of a programming language

Perl books often teach the mechanics without how to use them effectively

Questions?

Phillip Taylor

phillip.taylor@net-a-porter.com

Acknowledgements to Scott Meyer's "Effective C++" and Hurb Stutter's "Exceptional C++"

/

#