File Coverage

blib/lib/DBIx/Fast/Transaction.pm
Criterion Covered Total %
statement 109 137 79.5
branch 30 56 53.5
condition 2 4 50.0
subroutine 13 17 76.4
pod 5 9 55.5
total 159 223 71.3


line stmt bran cond sub pod time code
1             package DBIx::Fast::Transaction;
2              
3 15     15   233 use v5.38;
  15         92  
4 15     15   87 use Object::Pad 0.807;
  15         115  
  15         645  
5 15     15   2450 use Time::HiRes qw(time);
  15         27  
  15         131  
6              
7             use constant {
8 15         4293 DELAY => 1,
9             RETRIES => 3,
10             SLOW => 5
11 15     15   1555 };
  15         33  
12              
13             class DBIx::Fast::Transaction :isa(DBIx::Fast::Base) {
14 0     0 0 0 field $level :accessor = 0;
15 0 0       0 field $savepoints :accessor = {};
16 0 0   0 0 0 field $start_time :accessor = undef;
  0         0  
17 0 0   0 0 0 field $stats :accessor = {
  0         0  
18 0     0 0 0 avg_duration => 0,
19             deadlock_count => 0,
20             error_count => 0,
21             success_count => 0,
22             total_count => 0,
23             total_duration => 0,
24             max_duration => 0
25             };
26              
27 0 0   8 1 0 method do ($code, $options = {}) {
  8         16  
  8         20  
  8         10  
  8         13  
  8         9  
28 8 50       25 $self->dbix->Exception("Transaction code block required")
29             unless ref $code eq 'CODE';
30              
31 8   50     36 my $max_retries = $options->{max_retries} // RETRIES;
32 8   50     22 my $retry_delay = $options->{retry_delay} // DELAY;
33              
34 8         24 $start_time = time();
35 8         13 $stats->{total_count}++;
36              
37 8         10 my $result;
38 8         12 my $retries = 0;
39              
40 8         18 while ( $retries <= $max_retries ) {
41 8         13 eval {
42 8 100       25 $self->_begin_work if $level == 0;
43              
44 8         12 $level++;
45              
46 8         63 $result = $code->();
47              
48 5 100       32 $self->_commit if $level == 1;
49              
50 5         7 $level--;
51 5         10 $self->_update_stats(1);
52             };
53              
54 8 100       36 if ( my $error = $@ ) {
55 3 50       16 if ( $error =~ /deadlock/i ) {
56 0         0 $stats->{deadlock_count}++;
57 0 0       0 if ( $retries < $max_retries ) {
58 0         0 $retries++;
59 0         0 sleep($retry_delay);
60 0         0 next;
61             }
62             }
63              
64             # Rollback only at outermost level
65 3 100       8 if ( $level == 1 ) {
66 2         3 eval { $self->_rollback() };
  2         7  
67             }
68              
69 3 50       8 $level-- if $level > 0;
70 3         10 $self->_update_stats(0);
71              
72 3 100       11 if ( $level == 0 ) {
73 2         4 $savepoints = {};
74             }
75              
76 3         12 $self->dbix->set_error( 500, "Transaction failed: $error" );
77 3         25 die $error;
78             }
79 5         9 last;
80             }
81 5         80 return $result;
82             }
83              
84 6     6   10 method _begin_work () {
  6         11  
  6         7  
85 6         9 eval {
86 6         21 $self->dbix->db->dbh->begin_work;
87             };
88              
89 6 50       524 if ($@) {
90 0         0 $self->dbix->set_error( 500,
91             "Could not begin transaction: $@" );
92 0         0 die $@;
93             }
94             }
95              
96 4     4   6 method _commit () {
  4         9  
  4         3  
97 4         6 eval {
98 4         10 $self->dbix->db->dbh->commit;
99             };
100 4 50       238 if ($@) {
101 0         0 $self->dbix->set_error( 500,
102             "Could not commit transaction: $@" );
103 0         0 die $@;
104             }
105             }
106              
107 2     2   3 method _rollback () {
  2         4  
  2         2  
108 2         3 eval {
109 2         5 $self->dbix->db->dbh->rollback;
110             };
111              
112 2 50       126 if ($@) {
113 0         0 $self->dbix->set_error( 500,
114             "Could not rollback transaction: $@" );
115 0         0 die $@;
116             }
117             }
118              
119 3     3 1 6 method savepoint ($name) {
  3         7  
  3         5  
  3         3  
120 3 50       6 $self->dbix->Exception(
121             "DBIx::Fast::Transaction - Savepoint name required")
122             unless $name;
123 3 50       20 $self->dbix->Exception(
124             "DBIx::Fast::Transaction - Invalid savepoint name: $name")
125             unless $name =~ /^[a-zA-Z_][a-zA-Z0-9_]*$/;
126 3 50       7 return unless $level > 0;
127              
128 3         14 $savepoints->{$name} = {
129             level => $level,
130             time => time()
131             };
132              
133 3         5 eval {
134 3         8 $self->dbix->exec("SAVEPOINT $name");
135             };
136 3 50       9 if ($@) {
137 0         0 delete $savepoints->{$name};
138 0         0 $self->dbix->set_error( 500,
139             "Could not create savepoint: $@" );
140 0         0 die $@;
141             }
142              
143 3         6 return 1;
144             }
145              
146 2     2 1 4 method rollback_to ($name) {
  2         5  
  2         3  
  2         2  
147 2 50       5 $self->dbix->Exception(
148             "DBIx::Fast::Transaction - Savepoint name required")
149             unless $name;
150 2 50       12 $self->dbix->Exception(
151             "DBIx::Fast::Transaction - Invalid savepoint name: $name")
152             unless $name =~ /^[a-zA-Z_][a-zA-Z0-9_]*$/;
153              
154 2 50       5 unless ( exists $savepoints->{$name} ) {
155 0         0 $self->dbix->set_error( 500,
156             "Savepoint '$name' does not exist" );
157 0         0 return;
158             }
159              
160 2         3 eval {
161 2         14 $self->dbix->exec("ROLLBACK TO SAVEPOINT $name");
162             };
163              
164 2 50       6 if ($@) {
165 0         0 $self->dbix->set_error( 500,
166             "Could not rollback to savepoint: $@" );
167 0         0 die $@;
168             }
169              
170 2         5 my $current_level = $savepoints->{$name}->{level};
171 2         3 for my $sp ( keys %{$savepoints} ) {
  2         7  
172 3 50       8 if ( $savepoints->{$sp}->{level} > $current_level ) {
173 0         0 delete $savepoints->{$sp};
174             }
175             }
176              
177 2         5 return 1;
178             }
179              
180 8     8   9 method _update_stats ($success) {
  8         20  
  8         8  
  8         13  
181 8         22 my $duration = time() - $start_time;
182              
183 8 100       26 if ($success) {
184 5         10 $stats->{success_count}++;
185             }
186             else {
187 3         6 $stats->{error_count}++;
188             }
189              
190 8         17 $stats->{total_duration} += $duration;
191             $stats->{max_duration} = $duration
192 8 100       21 if $duration > $stats->{max_duration};
193             $stats->{avg_duration} =
194 8         22 $stats->{total_duration} / $stats->{total_count};
195              
196 8 50       21 if ( $duration > SLOW ) {
197 0         0 warn sprintf(
198             "Slow transaction detected: %.2fs (threshold: %ds)",
199             $duration, SLOW );
200             }
201             }
202              
203 2     2 1 3 method get_stats () { return $stats }
  2         5  
  2         2  
  2         4  
204              
205 1     1 1 2 method reset_stats () {
  1         3  
  1         2  
206 1         7 $stats = {
207             total_count => 0,
208             success_count => 0,
209             error_count => 0,
210             deadlock_count => 0,
211             total_duration => 0,
212             max_duration => 0,
213             avg_duration => 0
214             };
215             }
216             }
217              
218             1;
219              
220             __END__