object::franger: wear a raincoat in your code

33
Object::Franger Wear a Raincoat in Your Code Steven Lembark Workhorse Computing

Upload: workhorse-computing

Post on 20-Aug-2015

806 views

Category:

Technology


2 download

TRANSCRIPT

Object::Franger

Wear a Raincoatin Your Code

Steven LembarkWorkhorse Computing

Why do you need one?

● Frankly, because you can't control your object:  you never know where it might end up.● Other people can fiddle with it.● Forks can leave it in unintended places.● It might get bugs (or worse).● External issues can require special handling.

Why an object wrapper?

● All of the code required to handle all of the special cases ends up bloating your work.

● Most wrappers are re­usable: forks, timeouts, and signals all use common code.

● The division of labor isolates the wrapper and inner portions into re­usable pieces.

● All of which make these good fodder for OO.

Requirements for Wrappers.

● They should feel like the real thing.● They cannot leak.● These are not easy: making the wrapper feel 

enough like the real thing to fool both your object and the caller.

Where wrappers can help.

● Re­usable sanity checks (what goes on here).● Compatibility layers for changing API's.● Localizing values on the call stack.● Modifying context.● Simplifying the wrapped code, which doesn't 

have to deal with all of this in one place.● Example: Top­ & Bottom­half device drivers.

Perly wrappers.

● There is (of course) more than one way:● Override a method in a derived class.● Replace a method in the symbol table.● AUTOLOAD from stub namespace.

● Example:● Attribute Handlers replace the subroutine before 

it is installed (functional).● Object::Trampoline replaces the object in­place.● Object::Wraper (AUTOLOAD).

My particular itch: DBI with forks.

● DBI objects cannot be re­cycled across forks.● I was writing heavily forked code for high­

volume database access.● Needed the forks, needed the DBI to handle 

the children gracefully.● Wanted child proc's to fail gracefully – 

hopefully without damaging the database.

Why forks hurt DBI

● The points are sharp.● Database connections use PID's to bookkeep 

connections.● Servers cannot handle multiple clients requests 

on the same channel.● Destroying an object in one process brings 

kicks its longer­lived sibling in the socket.

Cleaning Up $dbh

● Forked process:● Disable destroy side effects on the channel for 

each object.● Iterate the handle and cached kids.

● Within process:● Call $kid­>finish for all cached kids.● Call $dbh­>disconnect.

my @kidz = do { my $drh = $dbh->{ Driver };

my $list= $drh? $drh->{ CachedKids }: '';

$list? values %$list: ()

};

if( $$ != $pid ) { $_->{ InactiveDestroy } = 1 for ( $dbh, @kidz ); } else { $_->finish for @kidz;

$dbh->disconnect; }

Cleaning up $dbh

● Extract the list of handles.

● If the process did not create them, then inactivate the DESTROY side effects.

● Otherwise finish the kids and then disconnect.

Sanity checking a PID

● Perl stores he current Process ID (“PID”) in  “$$” (i.e., looks like the shell variable).

● Storing this when the handle is created allows re­checking it before dispatching the call.

● If the stored PID and $$ don't agree then the handle needs to be cleaned up.

● This has to be sanity­checked on method calls, dealt with carefully in DESTROY.

Looks objective:

● The code is re­usable.● All of the handles are cleaned up the same way.● All of the fork checks are the same.● All of the wrapping is done the same way.

● It can be parameterized.● $$ is $$ wherever you are.

● Frangers are good for objects.

My Requirements

● These are called for every method in the wrapped class: they have to be fast.

● They also cannot use bulky storage since they are add to the requirements for all wrapped objects.

● They should also avoid unintended side effects (e.g., modifying object values, calling context).

O::W is built in layers.

● Object::Wrapper base class provides generic new, DESTROY, and an AUTOLOAD.

● The AUTOLOAD calls sanity check hooks in the derived classes and re­dispatches the result or croaks.

● Object::Wrapper::Fork bookkeeps $$.● Object::Wrapper::Fork::DBI deals with the 

cleanups.

A Place to Hang Your Hat

● A few hooks are all that's kneaded:● pre­dispatch for the sanity check.● straight­jacket for failed sanity checks.

● These accommodate all of the necessary customizations for the basic wrapper.

Throwing a Hook● Perl's “can” is rather helpful:

● It returns true if the object “can”.● Its true value is a subref to the  object's handler.

● This makes:

my $handler = $object­>can( $hook );

$object­>$handler­>( @argz );

synonymous with:

$handler­>( $object, @argz );

Structure of a Franger

● Remember the need for speed, flexibility, and encapsulation of the wrapped object.

● Take a look at the calling standard: generic object with arguments.

● The structure is obvious:bless [ $object, @sanity_argz ], $class;

● Resulting in:$handler->( @$obj );

Constructing a Franger

sub new{ my $proto = shift; my $class = blessed $proto || $proto;

my $object = shift or croak "Bogus franger: missing object";

bless [ $object, @_ ], $class}

● Store the call stack for validation as­is.

OK, but what do you do with it?

● Whatever you want.● Object::Wrapper, in fact, does nothing but re­

dispatch the method calls.● Useful for cases where the interesting part of 

the wrapper is in the DESTROY, not the individual calls.

Wrapper AUTOLOAD is Standard

AUTOLOAD{ my $franger = shift;

my $i = rindex $AUTOLOAD, ':'; my $name = substr $AUTOLOAD, ++$i;

my $sub = $franger->[0]->can( $name ) or confess "Bogus $AUTOLOAD: '$franger->[0]' cannot '$name'";

$franger->[0]->$sub( @_ )}

● Does nothing more than necessary.● Useful when the DESTROY check is enogh.

Oedipus Not­Complex: Forks

AUTOLOAD{ my $franger = shift; my ( $obj, $pid ) = @$franger;

$pid == $$ or confess "Bogus $AUTOLOAD: @{$franger} crosses fork.";

my $i = rindex $AUTOLOAD, ':'; my $name = substr $AUTOLOAD, ++$i;

my $sub = $obj->can( $name ) or confess "Bogus $AUTOLOAD: '$obj' cannot '$name'";

# goto &$obj is slower according to Benchmark...

$obj->$sub( @_ )}

Clean Up Your Mess: O::W::Destroy

DESTROY{ my $franger = shift;

my $class = blessed $franger || $franger;

# $cleanupz{ $class } may be a method name or coderef to save time.

my $cleanup = $cleanupz{ $class } || $franger->can( 'cleanup' ) or confess "Bogus franger: no cleanup for '$franger' or '$class'";

my $sub = ref $cleanup ? $cleanup : $franger->can( $cleanup ) or confess "Bogus $class: no cleanup for '$franger' ($class)";

'CODE' eq reftype $sub or confess "Bogus $class: not a coderef '$sub'";

$cleanup->( @$franger );

return}

DBI: It's All How You Clean Up

● Check for cached_kids.● Within the constructing PID: 

● Finish all the kids.● Disconnect the parent.

● Within child Proc's:● Disable destroy side effects in the kids & parent.

First Step: Find the Kidssub cleanup{ my ( $dbh, $pid ) = @_;

my $struct = do { my $drh = $dbh->{ Driver };

$drh ? $drh->{ CachedKids } : '' };

my @kidz = $struct ? values %$struct : () ;

Second Step: Do the Deed

if( $$ != $pid ) { # handle crossed a fork: turn off side # effects of destruction.

$_->{ InactiveDestroy } = 1 for ( $dbh, @kidz ); } else { $_->finish for @kidz;

$dbh->disconnect; }

# at this point the DBI object has been # prepared to go out of scope politely.

return}

Cleaning Up Statements Is Easiersub cleanup{ my ( $sth, $pid ) = @_;

if( $$ ~~ $pid ) { # same process: finalize the handle and disconnect. # caller deals with clones.

$sth->{ Active } and $sth->finish; } else { $sth->{ InactiveDestroy } = 1; }

# at this point the DBD object has been # prepared to go out of scope politely.

return}

Getting What You Want: Overloading Constructors● For DBI this requires versions of connect and 

connect_cached, prepare and prepare_cached.● Connect simply returns the wrapped $dbh:

sub connect{ shift;

my $dbh = DBI->connect( @_ ) or croak 'Fail connect: ' . $DBI::errstr;

Object::Wrapper::Fork::dbh->new( $dbh )}

Overloading STH Constructors

● These get a DBI wrapper object.

● Returning a wrapped DBD.sub prepare{ my $franger = shift;

my ( $dbh, $pid ) = @$franger;

$pid == $$ or confess "Bogus prepare: @{ $franger } crosses fork.";

my $sth = $dbh->prepare( @_ ) or croak 'Failed prepare: ' . $dbh->errstr;

Object::Wrapper::Fork::sth->new( $sth )

}

Wrappers are not 100% effective

● DBI offers a tied­hash interface.● Kinda hard to handle this with a blessed array.● Fortunately, the hash interface is rarely 

necessary.● There is also one more issue for destructors.

Making Happy ENDings

● Perl destroys objects out­of­order on exit.● This means that we also have to wrap 

DBI::DESTROY to get complete coverage.● Fortunately this isn't all that hard to do with the 

Symbol module's qualify_to_ref.● This requires a map of $dbh   O::W::F::DBI →

objects that can be used to dispatch destruction.● No time to describe it here.

Other Uses for Object::Wrappers

● Maximum time:bless [ $obj, ( time + $window ) ];

time < $franger->[1] or ...

● Maximum reuse:bless [ $obj, $counter ];

--$franger->[1] or ...

Only Your Wrapper Knows For Sure

● Long­lived processes may not want to die after  the wrapped object hits its limit.

● Nice thing is that they don't have to:--$franger->[ 1 ]

or @$franger = ( $class->new( ... ), $counter );

● This is handy for classes with memory leaks in the objects.

Summary

● Keeping your object safe is easy:● Use a franger.● Check before you use it.● Make sure you clean up afterwards.