File Coverage

lib/UR/DataSource/RDBMSRetriableOperations.pm
Criterion Covered Total %
statement 55 59 93.2
branch 16 18 88.8
condition 3 6 50.0
subroutine 10 11 90.9
pod 0 2 0.0
total 84 96 87.5


line stmt bran cond sub pod time code
1             package UR::DataSource::RDBMSRetriableOperations;
2              
3 3     3   131 use strict;
  3         4  
  3         88  
4 3     3   10 use warnings;
  3         4  
  3         80  
5              
6 3     3   9 use Time::HiRes;
  3         5  
  3         27  
7              
8             # A mixin class that provides methods to retry queries and syncs
9             #
10             # Consumers should provide should_retry_operation_after_error().
11             # It's passed the SQL that generated the error and the DBI error string.
12             # It should return true if the operation generating that error should be
13             # retried.
14              
15             class UR::DataSource::RDBMSRetriableOperations {
16             has_optional => [
17             retry_sleep_start_sec => { is => 'Integer', value => 1, doc => 'Initial inter-error sleep time' },
18             retry_sleep_max_sec => { is => 'Integer', value => 3600, doc => 'Maximum inter-error sleep time' },
19             ],
20             valid_signals => ['retry']
21             };
22              
23              
24             # The guts of the thing. Consumers that want a base-datasource method to
25             # be retriable should override the method to call this instead, and pass
26             # a code ref to perform the retriable action
27              
28             sub _retriable_operation {
29 16     16   72 my $self = UR::Util::object(shift);
30 16         26 my $code = shift;
31              
32 16         62 $self->_make_retriable_operation_observer();
33              
34             RETRY_LOOP:
35 16         109 for( my $db_retry_sec = $self->retry_sleep_start_sec;
36             $db_retry_sec < $self->retry_sleep_max_sec;
37             $db_retry_sec *= 2
38             ) {
39 21         43 my @rv = eval { $code->(); };
  21         49  
40              
41 21 100       91 if ($@) {
42 14 100       62 if ($@ =~ m/DB_RETRY/) {
43 10         27 $self->error_message("DB_RETRY");
44 10         145 $self->debug_message("Disconnecting and sleeping for $db_retry_sec seconds...\n");
45 10         48 $self->disconnect_default_handle;
46 10         151286 Time::HiRes::sleep($db_retry_sec);
47 10         188 $self->__signal_observers__('retry', $db_retry_sec);
48 10         127 next RETRY_LOOP;
49             }
50 4         538 Carp::croak($@); # re-throw other exceptions
51             }
52 7         85 return $self->context_return(@rv);
53             }
54 5         203 die "Maximum database retries reached";
55             }
56              
57              
58             {
59             my %retry_observers;
60             sub _make_retriable_operation_observer {
61 16     16   22 my $self = shift;
62 16 100       50 unless ($retry_observers{$self->class}++) {
63 5         13 for (qw(query_failed commit_failed do_failed connect_failed sequence_nextval_failed)) {
64 25         135 $self->add_observer(
65             aspect => $_,
66             priority => 99999, # Super low priority to fire last
67             callback => \&_db_retry_observer,
68             );
69             }
70             }
71             }
72             }
73              
74             # Default is to not retry
75             sub should_retry_operation_after_error {
76 0     0 0 0 my($self, $sql, $dbi_errstr) = @_;
77 0         0 return 0;
78             }
79              
80              
81             # The callback for the retry observer
82             sub _db_retry_observer {
83 15     15   31 my($self, $aspect, $db_operation, $sql, $dbi_errstr) = @_;
84              
85 15 50       41 unless (defined $sql) {
86 0         0 $sql = '(no sql)';
87             }
88 15         114 $self->error_message("SQL failed during $db_operation\nerror: $dbi_errstr\nsql: $sql");
89              
90 15 100       56 die "DB_RETRY" if $self->should_retry_operation_after_error($sql, $dbi_errstr);
91              
92             # just fall off the end here...
93             # Code triggering the observer will throw an exception
94             }
95              
96              
97             # Searches the parentage of $self for a RDBMS datasource class
98             # and returns a ref to the named sub in that package
99             # This is necessary because we're using a mixin class and not
100             # a real role
101             my %cached_rdbms_datasource_method_for;
102             sub rdbms_datasource_method_for {
103 20     20 0 29 my $self = shift;
104 20         28 my $method = shift;
105 20         26 my $target_class_name = shift;
106              
107 20   66     129 $target_class_name ||= $self->class;
108 20 100       67 if ($cached_rdbms_datasource_method_for{$target_class_name}) {
109 12         93 return $cached_rdbms_datasource_method_for{$target_class_name}->can($method);
110             }
111              
112 8         28 foreach my $parent_class_name ( $target_class_name->__meta__->parent_class_names ) {
113 12 100       49 if ( $parent_class_name->isa('UR::DataSource::RDBMS') ) {
114 8 100       34 if ($parent_class_name->isa(__PACKAGE__) ) {
115 4 50       12 if (my $sub = $self->rdbms_datasource_method_for($method, $parent_class_name)) {
116 4         60 return $sub;
117             }
118             } else {
119 4         10 $cached_rdbms_datasource_method_for{$target_class_name} = $parent_class_name;
120 4         31 return $parent_class_name->can($method);
121             }
122             }
123             }
124 0         0 return;
125             }
126              
127             # The retriable methods we want to wrap
128              
129             foreach my $parent_method (qw(
130             create_iterator_closure_for_rule
131             create_default_handle
132             _sync_database
133             do_sql
134             autogenerate_new_object_id_for_class_name_and_rule
135             )) {
136             my $override = sub {
137 16     16   1316 my $self = shift;
138 16         37 my @params = @_;
139              
140             # Installing this as the $parent_method leads to infinte recursion if
141             # the parent does not directly inherit this class.
142 3     3   1811 use warnings FATAL => qw( recursion );
  3         3  
  3         343  
143              
144 16   33     174 my $parent_sub ||= $self->rdbms_datasource_method_for($parent_method);
145             $self->_retriable_operation(sub {
146 21     21   112 $self->$parent_sub(@params);
147 16         283 });
148             };
149              
150             Sub::Install::install_sub({
151             into => __PACKAGE__,
152             as => $parent_method,
153             code => $override,
154             });
155             }
156              
157             1;