File Coverage

blib/lib/DBIx/ParseError/MySQL.pm
Criterion Covered Total %
statement 35 35 100.0
branch 10 10 100.0
condition n/a
subroutine 10 10 100.0
pod n/a
total 55 55 100.0


line stmt bran cond sub pod time code
1             package DBIx::ParseError::MySQL;
2              
3 2     2   399287 use utf8;
  2         718  
  2         14  
4 2     2   84 use strict;
  2         4  
  2         51  
5 2     2   11 use warnings;
  2         25  
  2         101  
6              
7 2     2   1616 use Moo;
  2         22263  
  2         11  
8              
9 2     2   3968 use Scalar::Util qw( blessed );
  2         6  
  2         131  
10 2     2   1625 use Types::Standard qw( Str Bool Object );
  2         329109  
  2         56  
11              
12             # ABSTRACT: Error parser for MySQL
13 2     2   8499 use version;
  2         2609  
  2         17  
14             our $VERSION = 'v1.0.4'; # VERSION
15              
16             #pod =head1 SYNOPSIS
17             #pod
18             #pod use DBIx::ParseError::MySQL;
19             #pod
20             #pod eval {
21             #pod my $result = $dbh->do('SELECT 1');
22             #pod };
23             #pod if ($@) {
24             #pod if (DBIx::ParseError::MySQL->new($@)->is_transient) { $dbh->reconnect }
25             #pod else { die; }
26             #pod }
27             #pod
28             #pod =head1 DESCRIPTION
29             #pod
30             #pod This module is a database error categorizer, specifically for MySQL. This module is also
31             #pod compatible with Galera's WSREP errors.
32             #pod
33             #pod =head1 ATTRIBUTES
34             #pod
35             #pod =head2 orig_error
36             #pod
37             #pod Returns the original, untouched error object or string.
38             #pod
39             #pod =cut
40              
41             has orig_error => (
42             is => 'ro',
43             isa => Str|Object,
44             required => 1,
45             );
46              
47             #pod =head2 error_string
48             #pod
49             #pod Returns the stringified version of the error.
50             #pod
51             #pod =cut
52              
53             has error_string => (
54             is => 'lazy',
55             isa => Str,
56             init_arg => undef,
57             );
58              
59             sub _build_error_string {
60 25     25   272 my $self = shift;
61              
62             # All of the exception objects should support this, too.
63 25         614 return $self->orig_error."";
64             }
65              
66             #pod =head2 error_type
67             #pod
68             #pod Returns a string that describes the type of error. These can be one of the following:
69             #pod
70             #pod lock Lock errors, like a lock wait timeout or deadlock
71             #pod connection Connection/packet failures, disconnections
72             #pod shutdown Errors that happen when a server is shutting down
73             #pod duplicate_value Duplicate entry errors
74             #pod unknown Any other error
75             #pod
76             #pod =cut
77              
78             has error_type => (
79             is => 'lazy',
80             isa => Str,
81             init_arg => undef,
82             );
83              
84             sub _build_error_type {
85 25     25   2918 my $self = shift;
86              
87 25         616 my $error = $self->error_string;
88              
89             # We have to capture just the first error, not other errors that may be buried in the
90             # stack trace.
91 25         1049 $error =~ s/ at [^\n]+ line \d+\.?(?s:\n.*)?//;
92              
93             # Disable /x flag to allow for whitespace within string, but turn it on for newlines
94             # and comments.
95             #
96             # These error messages are purposely long and case-sensitive, because we're looking
97             # for these errors -anywhere- in the string. Best to get as exact of a match as
98             # possible.
99              
100             # Locks
101 25 100       373 return 'lock' if $error =~ m<
102             (?-x:Deadlock found when trying to get (?:user-level |locking service )?lock; try )(?:
103             (?-x:restarting transaction)|
104             (?-x:rolling back transaction/releasing locks and restarting lock acquisition)|
105             (?-x:releasing locks and restarting lock acquisition)
106             )|
107             (?-x:Lock wait timeout exceeded; try restarting transaction)|
108             (?-x:Service lock wait timeout exceeded)|
109             (?-x:WSREP detected deadlock/conflict and aborted the transaction.\s+Try restarting the transaction)
110             >x;
111              
112             # Various connection/packet problems
113 19 100       441 return 'connection' if $error =~ m<
114             # Connection dropped/interrupted
115             (?-x:MySQL server has gone away)|
116             (?-x:Lost connection to MySQL server)|
117             # NOTE: Exclude max_execution_time interruptions, since these are not connection
118             # failures, and retrying them would just produce the same results
119             (?-x:Query execution was interrupted(?!, maximum statement execution time exceeded))|
120              
121             # Initial connection failure
122             (?-x:Bad handshake)|
123             (?-x:Too many connections)|
124             (?-x:Host '\S+' is blocked because of many connection errors)|
125             (?-x:Can't get hostname for your address)|
126             (?-x:Can't connect to (?:local )?MySQL server)|
127              
128             # Packet corruption
129             (?-x:Got a read error from the connection pipe)|
130             (?-x:Got (?:an error|timeout) (?:reading|writing) communication packets)|
131             (?-x:Malformed communication packet)|
132              
133             # XXX: This _might be_ a connection failure, but the DBD::mysql error message
134             # does not expose the direct failure cause. See DBD::mysql/dbdimp.c#L2551.
135             (?-x:Turning (?:off|on) AutoCommit failed)
136             >x;
137              
138             # Failover/shutdown of node/server
139 8 100       96 return 'shutdown' if $error =~ m<
140             (?-x:WSREP has not yet prepared node for application use)|
141             (?-x:Server shutdown in progress)|
142             (?-x:Normal shutdown)|
143             (?-x:Shutdown complete)
144             >x;
145              
146             # Duplicate entry error
147 6 100       86 return 'duplicate_value' if $error =~ m<
148             # Any value can be in the first piece here...
149             (?-x:Duplicate entry '.+?' for key '\S+')
150             >xs; # include \n in .+
151              
152 4         112 return 'unknown';
153             }
154              
155              
156             #pod =head2 is_transient
157             #pod
158             #pod Returns a true value if the error is the type that is likely transient. For example,
159             #pod errors that recommend retrying transactions or connection failures. This check can be
160             #pod used to figure out if it's worth retrying a transaction.
161             #pod
162             #pod This is merely a check for the following L:
163             #pod C<< lock connection shutdown >>.
164             #pod
165             #pod =cut
166              
167             has is_transient => (
168             is => 'lazy',
169             isa => Bool,
170             init_arg => undef,
171             );
172              
173             sub _build_is_transient {
174 25     25   33343 my $self = shift;
175              
176 25         779 my $type = $self->error_type;
177              
178 25 100       752 return 1 if $type =~ /^(lock|connection|shutdown)$/;
179 6         151 return 0;
180             }
181              
182             #pod =head1 CONSTRUCTORS
183             #pod
184             #pod =head2 new
185             #pod
186             #pod my $parsed_error = DBIx::ParseError::MySQL->new($@);
187             #pod
188             #pod Returns a C object. Since the error is the only parameter, it
189             #pod can be passed by itself.
190             #pod
191             #pod =for Pod::Coverage BUILDARGS
192             #pod
193             #pod =cut
194              
195             around BUILDARGS => sub {
196             my ($orig, $class, @args) = @_;
197              
198             if (@args == 1 && defined $args[0] && (!ref $args[0] || blessed $args[0])) {
199             my $error = shift @args;
200             push @args, ( orig_error => $error );
201             }
202              
203             return $class->$orig(@args);
204             };
205              
206             #pod =head1 SEE ALSO
207             #pod
208             #pod L - A similar parser, but specifically tailored to L.
209             #pod
210             #pod =cut
211              
212             1;
213              
214             __END__