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~