X-Git-Url: http://git.shadowcat.co.uk/gitweb/gitweb.cgi?a=blobdiff_plain;f=lib%2FDBIx%2FClass%2FStorage.pm;h=dfff9a1a499e4776c328aa318da4bd9471794b01;hb=f4dc39d649672ff4452cf827ca204a1e937bc8b7;hp=e8bc77be0908ef1349873bf1fc58dc339da0309c;hpb=fd323bf1046faa7de5a8c985268d80ec5b703361;p=dbsrgits%2FDBIx-Class.git diff --git a/lib/DBIx/Class/Storage.pm b/lib/DBIx/Class/Storage.pm index e8bc77b..dfff9a1 100644 --- a/lib/DBIx/Class/Storage.pm +++ b/lib/DBIx/Class/Storage.pm @@ -6,35 +6,29 @@ use warnings; use base qw/DBIx::Class/; use mro 'c3'; -use DBIx::Class::Exception; -use Scalar::Util(); -use IO::File; +BEGIN { + no warnings 'once'; + @DBIx::Class::Storage::NESTED_ROLLBACK_EXCEPTION::ISA + = 'DBIx::Class::Exception'; +} + +use DBIx::Class::Carp; +use DBIx::Class::Storage::BlockRunner; +use Scalar::Util qw/blessed weaken/; use DBIx::Class::Storage::TxnScopeGuard; -use Try::Tiny; +use DBIx::Class::_Util qw( dbic_internal_try dbic_internal_catch fail_on_internal_call ); use namespace::clean; -__PACKAGE__->mk_group_accessors('simple' => qw/debug debugobj schema/); -__PACKAGE__->mk_group_accessors('inherited' => 'cursor_class'); +__PACKAGE__->mk_group_accessors(simple => qw/debug schema transaction_depth auto_savepoint savepoints/); +__PACKAGE__->mk_group_accessors(component_class => 'cursor_class'); __PACKAGE__->cursor_class('DBIx::Class::Cursor'); -sub cursor { shift->cursor_class(@_); } - -package # Hide from PAUSE - DBIx::Class::Storage::NESTED_ROLLBACK_EXCEPTION; - -use overload '"' => sub { - 'DBIx::Class::Storage::NESTED_ROLLBACK_EXCEPTION' -}; - -sub new { - my $class = shift; - my $self = {}; - return bless $self, $class; +sub cursor :DBIC_method_is_indirect_sugar { + DBIx::Class::_ENV_::ASSERT_NO_INTERNAL_INDIRECT_CALLS and fail_on_internal_call; + shift->cursor_class(@_); } -package DBIx::Class::Storage; - =head1 NAME DBIx::Class::Storage - Generic Storage Handler @@ -59,18 +53,13 @@ sub new { $self = ref $self if ref $self; - my $new = {}; - bless $new, $self; + my $new = bless( { + savepoints => [], + }, $self); $new->set_schema($schema); - $new->debugobj(new DBIx::Class::Storage::Statistics()); - - #my $fh; - - my $debug_env = $ENV{DBIX_CLASS_STORAGE_DBI_DEBUG} - || $ENV{DBIC_TRACE}; - - $new->debug(1) if $debug_env; + $new->debug(1) + if $ENV{DBIX_CLASS_STORAGE_DBI_DEBUG} || $ENV{DBIC_TRACE}; $new; } @@ -85,7 +74,7 @@ storage object, such as during L. sub set_schema { my ($self, $schema) = @_; $self->schema($schema); - Scalar::Util::weaken($self->{schema}) if ref $self->{schema}; + weaken $self->{schema} if ref $self->{schema}; } =head2 connected @@ -122,7 +111,7 @@ Throws an exception - croaks. sub throw_exception { my $self = shift; - if ($self->schema) { + if (ref $self and $self->schema) { $self->schema->throw_exception(@_); } else { @@ -162,10 +151,10 @@ For example, my $rs; try { $rs = $schema->txn_do($coderef); - } catch { + } dbic_internal_catch { my $error = shift; # Transaction failed - die "something terrible has happened!" # + die "something terrible has happened!" if ($error =~ /Rollback failed/); # Rollback failed deal_with_failed_transaction(); @@ -187,52 +176,17 @@ transaction failure. =cut sub txn_do { - my ($self, $coderef, @args) = @_; - - ref $coderef eq 'CODE' or $self->throw_exception - ('$coderef must be a CODE reference'); - - my (@return_values, $return_value); - - $self->txn_begin; # If this throws an exception, no rollback is needed - - my $wantarray = wantarray; # Need to save this since the context - # inside the try{} block is independent - # of the context that called txn_do() - try { - - # Need to differentiate between scalar/list context to allow for - # returning a list in scalar context to get the size of the list - if ($wantarray) { - # list context - @return_values = $coderef->(@args); - } elsif (defined $wantarray) { - # scalar context - $return_value = $coderef->(@args); - } else { - # void context - $coderef->(@args); - } - $self->txn_commit; - } - catch { - my $error = shift; - - try { - $self->txn_rollback; - } catch { - my $exception_class = "DBIx::Class::Storage::NESTED_ROLLBACK_EXCEPTION"; - $self->throw_exception($error) # propagate nested rollback - if $_ =~ /$exception_class/; - - $self->throw_exception( - "Transaction aborted: $error. Rollback failed: $_" - ); - } - $self->throw_exception($error); # txn failed but rollback succeeded - }; + my $self = shift; - return $wantarray ? @return_values : $return_value; + DBIx::Class::Storage::BlockRunner->new( + storage => $self, + wrap_txn => 1, + retry_handler => sub { + $_[0]->failed_attempt_count == 1 + and + ! $_[0]->storage->connected + }, + )->run(@_); } =head2 txn_begin @@ -244,7 +198,20 @@ an entire code block to be executed transactionally. =cut -sub txn_begin { die "Virtual method!" } +sub txn_begin { + my $self = shift; + + if($self->transaction_depth == 0) { + $self->debugobj->txn_begin() + if $self->debug; + $self->_exec_txn_begin; + } + elsif ($self->auto_savepoint) { + $self->svp_begin; + } + $self->{transaction_depth}++; + +} =head2 txn_commit @@ -255,7 +222,23 @@ transaction currently in effect (i.e. you called L). =cut -sub txn_commit { die "Virtual method!" } +sub txn_commit { + my $self = shift; + + if ($self->transaction_depth == 1) { + $self->debugobj->txn_commit() if $self->debug; + $self->_exec_txn_commit; + $self->{transaction_depth}--; + $self->savepoints([]); + } + elsif($self->transaction_depth > 1) { + $self->{transaction_depth}--; + $self->svp_release if $self->auto_savepoint; + } + else { + $self->throw_exception( 'Refusing to commit without a started transaction' ); + } +} =head2 txn_rollback @@ -265,7 +248,135 @@ which allows the rollback to propagate to the outermost transaction. =cut -sub txn_rollback { die "Virtual method!" } +sub txn_rollback { + my $self = shift; + + if ($self->transaction_depth == 1) { + $self->debugobj->txn_rollback() if $self->debug; + $self->{transaction_depth}--; + + # in case things get really hairy - just disconnect + dbic_internal_try { $self->_exec_txn_rollback; 1 } or do { + my $rollback_error = $@; + + # whatever happens, too low down the stack to care + # FIXME - revisit if stackable exceptions become a thing + dbic_internal_try { $self->disconnect }; + + die $rollback_error; + }; + + $self->savepoints([]); + } + elsif ($self->transaction_depth > 1) { + $self->{transaction_depth}--; + + if ($self->auto_savepoint) { + $self->svp_rollback; + $self->svp_release; + } + else { + DBIx::Class::Storage::NESTED_ROLLBACK_EXCEPTION->throw( + "A txn_rollback in nested transaction is ineffective! (depth $self->{transaction_depth})" + ); + } + } + else { + $self->throw_exception( 'Refusing to roll back without a started transaction' ); + } +} + +# to be called by several internal stacked transaction handler codepaths +# not for external consumption +# *DOES NOT* throw exceptions, instead: +# - returns false on success +# - returns the exception on failed rollback +sub __delicate_rollback { + my $self = shift; + + if ( + ( $self->transaction_depth || 0 ) > 1 + and + # FIXME - the autosvp check here shouldn't be happening, it should be a role-ish thing + # The entire concept needs to be rethought with the storage layer... or something + ! $self->auto_savepoint + and + # the handle seems healthy, and there is nothing for us to do with it + # just go ahead and bow out, without triggering the txn_rollback() "nested exception" + # the unwind will eventually fail somewhere higher up if at all + # FIXME: a ::Storage::DBI-specific method, not a generic ::Storage one + $self->_seems_connected + ) { + # all above checks out - there is nothing to do on the $dbh itself + # just a plain soft-decrease of depth + $self->{transaction_depth}--; + return; + } + + my @args = @_; + my $rbe; + + dbic_internal_try { + $self->txn_rollback; 1 + } + dbic_internal_catch { + + $rbe = $_; + + # we were passed an existing exception to augment (think DESTROY stacks etc) + if (@args) { + my ($exception) = @args; + + # append our text - THIS IS A TEMPORARY FIXUP! + # + # If the passed in exception is a reference, or an object we don't know + # how to augment - flattening it is just damn rude + if ( + # FIXME - a better way, not liable to destroy an existing exception needs + # to be created. For the time being perpetuating the sin below in order + # to break the deadlock of which yak is being shaved first + 0 + and + length ref $$exception + and + ( + ! defined blessed $$exception + or + ! $$exception->isa( 'DBIx::Class::Exception' ) + ) + ) { + + ################## + ### FIXME - TODO + ################## + + } + else { + + # SUCH HIDEOUS, MUCH AUGH! (and double WOW on the s/// at the end below) + $rbe =~ s/ at .+? line \d+$//; + + ( + ( + defined blessed $$exception + and + $$exception->isa( 'DBIx::Class::Exception' ) + ) + ? ( + $$exception->{msg} = + "Transaction aborted: $$exception->{msg}. Rollback failed: $rbe" + ) + : ( + $$exception = + "Transaction aborted: $$exception. Rollback failed: $rbe" + ) + ) =~ s/Transaction aborted: (?=Transaction aborted:)//; + } + } + }; + + return $rbe; +} =head2 svp_begin @@ -276,7 +387,30 @@ is provided, a random name will be used. =cut -sub svp_begin { die "Virtual method!" } +sub svp_begin { + my ($self, $name) = @_; + + $self->throw_exception ("You can't use savepoints outside a transaction") + unless $self->transaction_depth; + + my $exec = $self->can('_exec_svp_begin') + or $self->throw_exception ("Your Storage implementation doesn't support savepoints"); + + $name = $self->_svp_generate_name + unless defined $name; + + push @{ $self->{savepoints} }, $name; + + $self->debugobj->svp_begin($name) if $self->debug; + + $exec->($self, $name); +} + +sub _svp_generate_name { + my ($self) = @_; + return 'savepoint_'.scalar(@{ $self->{'savepoints'} }); +} + =head2 svp_release @@ -288,7 +422,38 @@ release all savepoints created after the one explicitly released as well. =cut -sub svp_release { die "Virtual method!" } +sub svp_release { + my ($self, $name) = @_; + + $self->throw_exception ("You can't use savepoints outside a transaction") + unless $self->transaction_depth; + + my $exec = $self->can('_exec_svp_release') + or $self->throw_exception ("Your Storage implementation doesn't support savepoints"); + + if (defined $name) { + my @stack = @{ $self->savepoints }; + my $svp = ''; + + while( $svp ne $name ) { + + $self->throw_exception ("Savepoint '$name' does not exist") + unless @stack; + + $svp = pop @stack; + } + + $self->savepoints(\@stack); # put back what's left + } + else { + $name = pop @{ $self->savepoints } + or $self->throw_exception('No savepoints to release');; + } + + $self->debugobj->svp_release($name) if $self->debug; + + $exec->($self, $name); +} =head2 svp_rollback @@ -300,9 +465,39 @@ release all savepoints created after the savepoint we rollback to. =cut -sub svp_rollback { die "Virtual method!" } +sub svp_rollback { + my ($self, $name) = @_; + + $self->throw_exception ("You can't use savepoints outside a transaction") + unless $self->transaction_depth; + + my $exec = $self->can('_exec_svp_rollback') + or $self->throw_exception ("Your Storage implementation doesn't support savepoints"); + + if (defined $name) { + my @stack = @{ $self->savepoints }; + my $svp; + + # a rollback doesn't remove the named savepoint, + # only everything after it + while (@stack and $stack[-1] ne $name) { + pop @stack + }; -=for comment + $self->throw_exception ("Savepoint '$name' does not exist") + unless @stack; + + $self->savepoints(\@stack); # put back what's left + } + else { + $name = $self->savepoints->[-1] + or $self->throw_exception('No savepoints to rollback');; + } + + $self->debugobj->svp_rollback($name) if $self->debug; + + $exec->($self, $name); +} =head2 txn_scope_guard @@ -311,8 +506,8 @@ L: my $txn_guard = $storage->txn_scope_guard; - $row->col1("val1"); - $row->update; + $result->col1("val1"); + $result->update; $txn_guard->commit; @@ -333,7 +528,7 @@ sub txn_scope_guard { =head2 sql_maker Returns a C object - normally an object of class -C. +C. =cut @@ -341,18 +536,18 @@ sub sql_maker { die "Virtual method!" } =head2 debug -Causes trace information to be emitted on the C object. -(or C if C has not specifically been set). +Causes trace information to be emitted on the L object. +(or C if L has not specifically been set). This is the equivalent to setting L in your shell environment. =head2 debugfh -Set or retrieve the filehandle used for trace/debug output. This should be -an IO::Handle compatible object (only the C method is used. Initially -set to be STDERR - although see information on the -L environment variable. +An opportunistic proxy to L<< ->debugobj->debugfh(@_) +|DBIx::Class::Storage::Statistics/debugfh >> +If the currently set L does not have a L method, caling +this is a no-op. =cut @@ -371,13 +566,73 @@ of L that is compatible with the original method of using a coderef as a callback. See the aforementioned Statistics class for more information. +=cut + +sub debugobj { + my $self = shift; + + if (@_) { + return $self->{debugobj} = $_[0]; + } + + $self->{debugobj} ||= do { + if (my $profile = $ENV{DBIC_TRACE_PROFILE}) { + require DBIx::Class::Storage::Debug::PrettyPrint; + my @pp_args; + + if ($profile =~ /^\.?\//) { + + require DBIx::Class::Optional::Dependencies; + if ( my $missing = DBIx::Class::Optional::Dependencies->req_missing_for ('config_file_reader') ) { + $self->throw_exception("Unable to parse TRACE_PROFILE config file '$profile' without $missing"); + } + + my $cfg = dbic_internal_try { + Config::Any->load_files({ files => [$profile], use_ext => 1 }); + } dbic_internal_catch { + # sanitize the error message a bit + $_ =~ s/at \s+ .+ Storage\.pm \s line \s \d+ $//x; + $self->throw_exception("Failure processing \$ENV{DBIC_TRACE_PROFILE}: $_"); + }; + + @pp_args = values %{$cfg->[0]}; + } + else { + @pp_args = { profile => $profile }; + } + + # FIXME - FRAGILE + # Hash::Merge is a sorry piece of shit and tramples all over $@ + # *without* throwing an exception + # This is a rather serious problem in the debug codepath + # Insulate the condition here with a try{} until a review of + # DBIx::Class::Storage::Debug::PrettyPrint takes place + # we do rethrow the error unconditionally, the only reason + # to try{} is to preserve the precise state of $@ (down + # to the scalar (if there is one) address level) + # + # Yes I am aware this is fragile and TxnScopeGuard needs + # a better fix. This is another yak to shave... :( + dbic_internal_try { + DBIx::Class::Storage::Debug::PrettyPrint->new(@pp_args); + } dbic_internal_catch { + $self->throw_exception($_); + } + } + else { + require DBIx::Class::Storage::Statistics; + DBIx::Class::Storage::Statistics->new + } + }; +} + =head2 debugcb Sets a callback to be executed each time a statement is run; takes a sub reference. Callback is executed as $sub->($op, $info) where $op is SELECT/INSERT/UPDATE/DELETE and $info is what would normally be printed. -See L for a better way. +See L for a better way. =cut @@ -474,7 +729,7 @@ sub columns_info_for { die "Virtual method!" } =head2 DBIC_TRACE If C is set then trace information -is produced (as when the L method is set). +is produced (as when the L method is set). If the value is of the form C<1=/path/name> then the trace output is written to the file C. @@ -484,6 +739,15 @@ created (when you call connect on your schema). So, run-time changes to this environment variable will not take effect unless you also re-connect on your schema. +=head2 DBIC_TRACE_PROFILE + +If C is set, L +will be used to format the output from C. The value it +is set to is the C that it will be used. If the value is a +filename the file is read with L and the results are +used as the configuration for tracing. See L +for what that structure should look like. + =head2 DBIX_CLASS_STORAGE_DBI_DEBUG Old name for DBIC_TRACE @@ -493,15 +757,16 @@ Old name for DBIC_TRACE L - reference storage implementation using SQL::Abstract and DBI. -=head1 AUTHORS - -Matt S. Trout +=head1 FURTHER QUESTIONS? -Andy Grundman +Check the list of L. -=head1 LICENSE +=head1 COPYRIGHT AND LICENSE -You may distribute this code under the same terms as Perl itself. +This module is free software L +by the L. You can +redistribute it and/or modify it under the same terms as the +L. =cut