File Coverage

blib/lib/DBIx/Timeout.pm
Criterion Covered Total %
statement 15 52 28.8
branch 0 12 0.0
condition 0 3 0.0
subroutine 5 9 55.5
pod 1 1 100.0
total 21 77 27.2


line stmt bran cond sub pod time code
1             package DBIx::Timeout;
2              
3             require 5.008; # v5.8.0+ needed for safe signals
4              
5 2     2   114565 use warnings;
  2         5  
  2         84  
6 2     2   12 use strict;
  2         3  
  2         111  
7              
8             our $VERSION = '1.01';
9              
10 2     2   2006 use Params::Validate qw(validate CODEREF);
  2         23165  
  2         157  
11 2     2   17 use Carp qw(croak);
  2         4  
  2         84  
12 2     2   1752 use POSIX qw(_exit);
  2         24213  
  2         17  
13              
14             our $TIMEOUT_EXIT_CODE = 29;
15              
16             sub call_with_timeout {
17 0     0 1   my $pkg = shift;
18              
19 0           my %args =
20             validate(@_,
21             {dbh => {isa => 'DBI::db'},
22             code => {type => CODEREF},
23             timeout => 1
24             });
25 0           my ($dbh, $code, $timeout) = @args{('dbh', 'code', 'timeout')};
26              
27 0           my $child_pid = $pkg->_fork_child($dbh, $timeout);
28              
29             # run code, trapping error since it may have come from the timeout
30             # connection killing
31 0           eval { $code->() };
  0            
32 0           my $err = $@;
33              
34             # signal the child that processing is done - will wake up from
35             # sleep if timeout didn't already pass. It's ok if this fails,
36             # that means the child is probably already done.
37 0           kill USR1 => $child_pid;
38              
39             # reap the child, examining exit code to determine if timeout fired
40 0 0         if (waitpid $child_pid, 0) {
41 0           my $exit_code = $? >> 8;
42 0 0         if ($exit_code == $TIMEOUT_EXIT_CODE) {
43 0           return 0;
44             }
45             } else {
46 0           croak("waitpid() failed: $!");
47             }
48              
49             # the error wasn't a timeout, rethrow
50 0 0         die $err if $err;
51              
52             # everything is all right
53 0           return 1;
54             }
55              
56             # forks off the child process
57             sub _fork_child {
58 0     0     my ($pkg, $dbh, $timeout) = @_;
59              
60             # pull a list of active handles for use after the fork
61 0           my %drivers = DBI->installed_drivers();
62 0 0 0       my @active_dbh =
63 0           grep { $_ and $_->isa('DBI::db') and $_->{Active} }
64 0           map { @{$_->{ChildHandles}} } values %drivers;
  0            
65              
66             # do the fork, return in the parent
67 0           my $child_pid = fork();
68 0 0         croak("Failed to fork(): $!") unless defined $child_pid;
69 0 0         return $child_pid if $child_pid;
70              
71             # do the dance needed to keep open DBI connections from causing
72             # errors when this child exits
73 0           foreach my $active_dbh (@active_dbh) {
74 0           $active_dbh->{InactiveDestroy} = 1;
75             }
76              
77             # setup a (safe) signal handler for USR1 which will exit early
78             # from sleep()
79 0     0     local $SIG{USR1} = sub { _exit(0) };
  0            
80              
81             # now running in the child, sleep for $timeout seconds
82 0           sleep $timeout;
83              
84             # turn off USR1 handler, signalling now won't kill us in the
85             # middle of killing the parent's thread
86 0           $SIG{USR1} = 'IGNORE';
87              
88             # woke up, time to kill parent's thread
89 0           $pkg->_kill_connection($dbh);
90              
91             # tell the parent what happened (use POSIX::_exit() to make sure
92             # the parent really gets the message - otherwise END blocks can
93             # change the exit code)
94 0           _exit($TIMEOUT_EXIT_CODE);
95             }
96              
97              
98             # MySQL specific thread-killer
99             sub _kill_connection {
100 0     0     my ($self, $dbh) = @_;
101              
102 0           my $thread_id = $dbh->{thread_id};
103              
104 0           my $new_dbh = $dbh->clone();
105 0           $new_dbh->{InactiveDestroy} = 0;
106 0           $new_dbh->do("KILL $thread_id");
107             }
108              
109              
110             1;
111              
112             __END__