Object::Franger: Wear a Raincoat in your Code
-
Upload
workhorse-computing -
Category
Technology
-
view
809 -
download
2
Transcript of Object::Franger: Wear a Raincoat in your Code
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 reusable: forks, timeouts, and signals all use common code.
● The division of labor isolates the wrapper and inner portions into reusable 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.
● Reusable 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 & Bottomhalf 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 inplace.● Object::Wraper (AUTOLOAD).
My particular itch: DBI with forks.
● DBI objects cannot be recycled 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 longerlived 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 rechecking 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 sanitychecked on method calls, dealt with carefully in DESTROY.
Looks objective:
● The code is reusable.● 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 redispatches 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:● predispatch for the sanity check.● straightjacket 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 asis.
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 NotComplex: 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 tiedhash 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 outoforder 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
● Longlived 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.