Skip Menu |

This queue is for tickets about the DBIx-Timeout CPAN distribution.

Report information
The Basics
Id: 87459
Status: new
Priority: 0/
Queue: DBIx-Timeout

People
Owner: Nobody in particular
Requestors: DSTERLING [...] cpan.org
Cc:
AdminCc:

Bug Information
Severity: Critical
Broken in:
  • 1.00
  • 1.01
Fixed in: (no value)



Subject: [PATCH] sharing a dbh across a fork causes libmysql to hang
Forking and sharing a dbh in the child and parent can cause the libmysql code to hang. This patch fixes the issue by accepting a new, optional dbh_callback argument, which is used by both the child and parent to get a new dbh. The parent then passes the thread id to the child, so it can kill the right thread. The calling code should disconnect all open dbh handles before calling the call_with_timeout method. This patch also includes the code for ticket #75279
Subject: dbixtimeout-callback.diff
Only in DBIx-Timeout-1.01/lib/DBIx: ._Timeout.pm diff -ru DBIx-Timeout-1.01.orig/lib/DBIx/Timeout.pm DBIx-Timeout-1.01/lib/DBIx/Timeout.pm --- DBIx-Timeout-1.01.orig/lib/DBIx/Timeout.pm 2012-02-24 11:36:02.000000000 -0500 +++ DBIx-Timeout-1.01/lib/DBIx/Timeout.pm 2013-07-30 15:43:48.272371745 -0400 @@ -10,6 +10,7 @@ use Params::Validate qw(validate CODEREF); use Carp qw(croak); use POSIX qw(_exit); +use IO::Pipe; our $TIMEOUT_EXIT_CODE = 29; @@ -18,17 +19,20 @@ my %args = validate(@_, - {dbh => {isa => 'DBI::db'}, + {dbh => { isa => 'DBI::db', optional => 1 }, code => {type => CODEREF}, - timeout => 1 + timeout => 1, + dbh_callback => { type => CODEREF, optional => 1 }, }); - my ($dbh, $code, $timeout) = @args{('dbh', 'code', 'timeout')}; + my ($dbh, $code, $timeout, $dbh_callback) = + @args{('dbh', 'code', 'timeout', 'dbh_callback')}; - my $child_pid = $pkg->_fork_child($dbh, $timeout); + my $child_pid; + ($child_pid, $dbh) = $pkg->_fork_child($dbh, $timeout, $dbh_callback); # run code, trapping error since it may have come from the timeout # connection killing - eval { $code->() }; + eval { $code->($dbh) }; my $err = $@; # signal the child that processing is done - will wake up from @@ -55,7 +59,7 @@ # forks off the child process sub _fork_child { - my ($pkg, $dbh, $timeout) = @_; + my ($pkg, $dbh, $timeout, $dbh_callback) = @_; # pull a list of active handles for use after the fork my %drivers = DBI->installed_drivers(); @@ -63,10 +67,28 @@ grep { $_ and $_->isa('DBI::db') and $_->{Active} } map { @{$_->{ChildHandles}} } values %drivers; - # do the fork, return in the parent + # create pipe and fork + my $pipe = IO::Pipe->new(); my $child_pid = fork(); croak("Failed to fork(): $!") unless defined $child_pid; - return $child_pid if $child_pid; + + # in the parent, send the child the dbh thread id + if ($child_pid) { + # grab a dbh if we have a callback to do so + if ($dbh_callback) { + $dbh = $dbh_callback->(); + } + my $thread_id = $dbh->{thread_id}; + $pipe->writer(); + print $pipe $thread_id, "\n"; + # return in the parent + return ($child_pid, $dbh); + } + + # in the child, grab the thread id + $pipe->reader(); + my $thread_id = readline($pipe); + chomp($thread_id); # do the dance needed to keep open DBI connections from causing # errors when this child exits @@ -81,12 +103,19 @@ # now running in the child, sleep for $timeout seconds sleep $timeout; + # woke up, time to kill parent's thread + # turn off USR1 handler, signalling now won't kill us in the # middle of killing the parent's thread $SIG{USR1} = 'IGNORE'; - # woke up, time to kill parent's thread - $pkg->_kill_connection($dbh); + # grab a dbh if we have a callback to do so + if ($dbh_callback) { + $dbh = $dbh_callback->(); + } + + # kill + $pkg->_kill_connection($dbh, $thread_id); # tell the parent what happened (use POSIX::_exit() to make sure # the parent really gets the message - otherwise END blocks can @@ -97,9 +126,11 @@ # MySQL specific thread-killer sub _kill_connection { - my ($self, $dbh) = @_; + my ($self, $dbh, $thread_id) = @_; - my $thread_id = $dbh->{thread_id}; + unless ($thread_id) { + $thread_id = $dbh->{thread_id}; + } my $new_dbh = $dbh->clone(); $new_dbh->{InactiveDestroy} = 0; @@ -202,6 +233,24 @@ either case the connection for C<$dbh> may be killed after this method returns. +=head2 dbh callback + +Sharing a database handle across forked processes can confuse the database +client library, so DBIx::Timeout can use a callback to grab a dbh instead: + + $ok = DBIx::Timeout->call_with_timeout( + dbh_callback => sub { get_my_dbh() }, + code => sub { my $dbh = shift; $dbh->do('LONG-RUNNING SQL HERE') }, + timeout => 300, + ); + +When using this method, DBIx::Timeout forks first, then grabs a fresh dbh in +both the parent and child. The parent then sends the thread id to kill to the +child. The parent's dbh is provided as an argument to your callback code. + +When using this method, the calling code should disconnect all active +database handles before calling C<call_with_timeout>. + =head1 KNOWN ISSUES When the timeout fires a warning may occur that looks like: Only in DBIx-Timeout-1.01/lib/DBIx: Timeout.pm~
On Tue Jul 30 15:49:12 2013, DSTERLING wrote: Show quoted text
> This patch also includes the code for ticket #75279
Whoops, no, this is wrong. We are using this code and the code from ticket #75279 in production; this patch builds on that patch but does not duplicate it.