File Coverage

lib/UR/Context/Transaction.pm
Criterion Covered Total %
statement 142 173 82.0
branch 52 72 72.2
condition 14 24 58.3
subroutine 17 20 85.0
pod 8 12 66.6
total 233 301 77.4


line stmt bran cond sub pod time code
1             package UR::Context::Transaction;
2              
3 266     266   1610 use strict;
  266         398  
  266         6349  
4 266     266   933 use warnings;
  266         366  
  266         10292  
5              
6             require UR;
7             our $VERSION = "0.46"; # UR $VERSION;
8              
9 266     266   1078 use Carp qw(croak confess shortmess);
  266         384  
  266         16119  
10 266     266   1078 use constant TRANSACTION_STATE_OPEN => 'open';
  266         378  
  266         22681  
11 266     266   1073 use constant TRANSACTION_STATE_COMMITTED => 'committed';
  266         387  
  266         11241  
12              
13 266     266   994 use Exporter qw(import);
  266         379  
  266         370749  
14             our @EXPORT_OK = qw(TRANSACTION_STATE_OPEN TRANSACTION_STATE_COMMITTED);
15              
16             UR::Object::Type->define(
17             class_name => __PACKAGE__,
18             is => ['UR::Context'],
19             has => [
20             begin_point => { is => 'Integer' },
21             end_point => { is => 'Integer', is_optional => 1}, # FIXME is this ever used anywhere?
22             state => { is => 'Text', valid_values => [TRANSACTION_STATE_OPEN, TRANSACTION_STATE_COMMITTED] },
23             commit_validator => { default_value => 'changes_can_be_saved',
24             doc => 'validation function used before commit() can succeed' },
25             ],
26             is_transactional => 1,
27             );
28              
29             our $log_all_changes = 0;
30             our @change_log;
31             our @open_transaction_stack;
32             our $last_transaction_id = 0;
33              
34             sub delete {
35 0     0 1 0 my $self = shift;
36 0         0 $self->rollback;
37             }
38              
39             sub begin {
40 351     351 1 12284 my $class = shift;
41 351         607 my %params = @_;
42              
43 351         818 delete @params{'begin_point', 'end_point', 'state'}; # These are set within this function
44              
45 351         565 my $id = $last_transaction_id++;
46              
47 351         559 my $begin_point = @change_log;
48 351         451 $log_all_changes = 1;
49              
50 351         526 my $last_trans = $open_transaction_stack[-1];
51 351 50 66     1606 if ($last_trans and $last_trans != $UR::Context::current) {
52 0         0 die "Current transaction does not match the top of the transaction stack!?"
53             }
54 351   66     1499 $last_trans ||= $UR::Context::current;
55              
56 351         1805 my $self = $class->create(
57             id => $id,
58             begin_point => $begin_point,
59             state => TRANSACTION_STATE_OPEN,
60             parent => $last_trans,
61             %params,
62             );
63              
64 351 50       1145 unless ($self) {
65 0         0 Carp::confess("Failed to being transaction!");
66             }
67              
68 351         597 push @open_transaction_stack, $self;
69              
70 351         517 $UR::Context::current = $self;
71              
72 351         829 return $self;
73             }
74              
75             sub log_change {
76 5897     5897 0 5850 my $this_class = shift;
77 5897         7837 my ($object, $class, $id, $aspect, $undo_data) = @_;
78              
79 5897 100       11968 return if $class eq "UR::Change";
80              
81             # wrappers (create/delete/load/unload/define) signal change also
82             # and we undo the wrapper, thereby undoing these
83             # -> ignore any signal from a method which is wrapped by another signalling method which gets undone
84 4095 100 66     13748 return if ( $aspect eq "load" or
85             $aspect eq "load_external"
86             );
87              
88 1813 100 66     6423 if (!ref($object) or $class eq "UR::Object::Index") {
89             #print "skipping @_\n";
90 16         37 return;
91             }
92              
93 1797 100       3065 if ($aspect eq "delete") {
94 42         193 $undo_data = Data::Dumper::Dumper($object);
95             }
96              
97 1797 50       7320 Carp::confess() if ref($class);
98              
99 1797         6767 my $change = UR::Change->create(
100             id => scalar(@change_log)+1,
101             changed_class_name => $class,
102             changed_id => $id,
103             changed_aspect => $aspect,
104             undo_data => $undo_data,
105             );
106              
107 1797 50       4281 unless (ref($change)) {
108             #$DB::single = 1;
109             }
110              
111 1797         2550 push @change_log, $change;
112 1797         2954 return $change;
113             }
114              
115             sub has_changes {
116 0     0 1 0 my $self = shift;
117 0         0 my @changes = $self->get_changes();
118 0 0       0 return (@changes > 1 ? 1 : ());
119             }
120              
121             sub get_changes {
122 349     349 1 505 my $self = shift;
123 349         977 my $begin_point = $self->begin_point;
124 349   33     972 my $end_point = $self->end_point || $#change_log;
125 349         1229 my @changes = @change_log[$begin_point..$end_point];
126 349 50       875 if (@_) {
127 0         0 @changes = UR::Change->get(id => \@changes, @_)
128             }
129             else {
130 349         872 return @changes;
131             }
132             }
133              
134             sub get_change_summary {
135             # TODO: This should compress multiple changes to the same object as much as possible
136             # Right now, it just omits the creation event for the transaction object itself.
137             # -> should the creation of the transaction be part of it?
138             # A: It should really be part of the prior transaction, and after commit/rollback
139             # the nesting collapses. The @change_log should be _inside the transaction object,
140             # or the change should contain a transaction id. The list can be destroyed on
141             # rollback, or summarized on commit.
142 0     0 0 0 my $self = shift;
143             my @changes =
144 0         0 grep { $_->changed_aspect !~ /^(load|define)$/ }
  0         0  
145             $self->get_changes;
146 0         0 shift @changes; # $self creation event
147 0         0 return @changes;
148             }
149              
150             sub rollback {
151 56     56 1 10219 my $self = shift;
152              
153             # Support calling as a class method: UR::Context::Transaction->rollback rolls back the current trans
154 56 100       197 unless (ref($self)) {
155 1         2 $self = $open_transaction_stack[-1];
156 1 50       5 unless ($self) {
157 0         0 Carp::confess("No open transaction!? Cannot rollback.");
158             }
159             }
160              
161 56 100       226 if ($self->state ne TRANSACTION_STATE_OPEN) {
162 1         5 Carp::confess("Cannot rollback a transaction that is " . $self->state . ".")
163             }
164              
165 55         254 $self->__signal_change__('prerollback');
166              
167 55         204 my $begin_point = $self->begin_point;
168 55 50       242 unless ($self eq $open_transaction_stack[-1]) {
169             # This is not the top transaction on the stack.
170             # Rollback internally nested transactions in order from the end.
171             my @transactions_with_begin_point =
172 0         0 map { [ $_->begin_point, $_ ] }
  0         0  
173             $self->class->get(
174             begin_point => { operator => ">", value => $begin_point }
175             );
176             my @later_transactions =
177 0         0 map { $_->[1] }
178 0         0 sort { $b->[0] <=> $a->[0] }
  0         0  
179             @transactions_with_begin_point;
180              
181 0         0 for my $later_transaction (@later_transactions) {
182 0 0       0 if ($later_transaction->isa("UR::DeletedRef")) {
183             #$DB::single = 1;
184             }
185 0         0 $later_transaction->rollback;
186             }
187             }
188              
189 55         246 my $parent = $self->parent;
190 55 50 66     326 if ($open_transaction_stack[-2] and $open_transaction_stack[-2] != $parent) {
191 0         0 die "Parent transaction $parent is not below this one on the stack $open_transaction_stack[-2]?";
192             }
193              
194             {
195             # Reverse each change, starting from the most recent, and
196             # ending with the creation of the transaction object itself.
197 55         73 local $log_all_changes = 0;
  55         79  
198              
199              
200 55         154 $self->__signal_change__('rollback', 1);
201 55         185 my @changes_to_undo = reverse $self->get_changes();
202 55         128 my $transaction_change = pop @changes_to_undo;
203 55         214 my $transaction = $transaction_change->changed_class_name->get($transaction_change->changed_id);
204 55 50 33     350 unless ($self == $transaction && $transaction_change->changed_aspect eq 'create') {
205 0         0 die "First change was not the creation of this transaction!";
206             }
207 55         125 for my $change (@changes_to_undo) {
208 280 100       617 if ($change == $changes_to_undo[0]) {
209             # the transaction reverses itself in its own context,
210             # but the removal of the transaction itself happens in the parent context
211 55         78 $UR::Context::current = $parent;
212             }
213              
214 280         702 $change->undo;
215 280         685 $change->delete;
216             }
217              
218 55         122 for my $change (@changes_to_undo) {
219 280 50       3124 unless($change->isa('UR::DeletedRef')) {
220 0         0 Carp::confess("Failed to undo a change during transaction rollback.");
221             }
222             }
223              
224 55         631 $transaction_change->undo;
225 55         131 $transaction_change->delete;
226             }
227              
228 55         273 $#change_log = $begin_point-1;
229              
230 55 50       185 unless($self->isa("UR::DeletedRef")) {
231             #$DB::single = 1;
232 0         0 Carp::confess("Failed to remove transaction during rollback.");
233             }
234              
235 55         502 pop @open_transaction_stack;
236 55 100       251 unless (@open_transaction_stack) {
237 52         77 $log_all_changes = 0;
238             }
239 55         86 $UR::Context::current = $parent;
240              
241 55         179 return 1;
242             }
243              
244             sub commit {
245 294     294 1 1775 my $self = shift;
246              
247             # Support calling as a class method: UR::Context::Transaction->commit commits the current transaction.
248 294 100       958 unless (ref($self)) {
249 1         3 $self = $open_transaction_stack[-1];
250 1 50       5 unless ($self) {
251 0         0 Carp::confess("No open transaction!? Cannot commit.");
252             }
253             }
254              
255 294 50       903 if ($self->state ne TRANSACTION_STATE_OPEN) {
256 0         0 Carp::confess("Cannot commit a transaction that is " . $self->state . ".")
257             }
258              
259 294 50       977 unless ($open_transaction_stack[-1] == $self) {
260             # TODO: decide if this should work like rollback, and commit nested transactions automatically
261 0         0 Carp::confess("Cannot commit a transaction with open sub-transactions!");
262             }
263 294         1241 $self->__signal_change__('precommit');
264              
265 294         1003 my $validator = $self->commit_validator;
266 294 100       1041 unless ($self->$validator()) {
267 1         4 return;
268             }
269              
270 293         797 $self->state(TRANSACTION_STATE_COMMITTED);
271 293 50       744 if ($self->state eq TRANSACTION_STATE_COMMITTED) {
272 293         771 $self->__signal_change__('commit',1);
273             }
274             else {
275 0         0 $self->__signal_change__('commit',0);
276             }
277 293         544 pop @open_transaction_stack;
278 293 100       779 unless (@open_transaction_stack) {
279 270         376 $log_all_changes = 0;
280             }
281              
282 293         1307 $UR::Context::current = $self->parent;
283 293         968 return 1;
284             }
285              
286             sub changes_can_be_saved {
287 293     293 0 413 my $self = shift;
288              
289             # This is very similar to behavior in UR::Context::_sync_databases. The only
290             # reason it isn't re-used from UR::Context is the desire to limit changed
291             # objects to those changed within the transaction.
292             # TODO: limit to objects that changed within transaction as to not duplicate
293             # error checking unnecessarily.
294              
295             my @changed_objects =
296 1000         4510 grep { ! $_->isa('UR::DeletedRef') }
297 293         964 map { $_->changed_object() } $self->get_changes();
  1001         2226  
298              
299             # This is primarily to catch custom validity logic in class overrides.
300 293         532 my @invalid = grep { $_->__errors__ } @changed_objects;
  965         2055  
301 293 100       766 if (@invalid) {
302 1         7 $self->display_invalid_data_for_save(\@invalid);
303 1         3 return;
304             }
305              
306 292         797 return 1;
307             }
308              
309             sub eval_or_do {
310 282     282 0 379 my $is_failure = shift;
311 282         357 my $block = shift;
312              
313 282         403 my $class = __PACKAGE__;
314 282 50       1032 if (@_) {
315 0         0 confess('%s::eval takes one argument', $class);
316             }
317 282         1244 my $tx = $class->begin();
318 282         367 my $result = CORE::eval { $block->() };
  282         793  
319 282         3672 my $eval_error = $@;
320              
321 282 100       743 if ($is_failure->($result, $eval_error)) {
322 21         3345 $class->debug_message(shortmess('Rolling back transaction'));
323 21 100       74 $class->debug_message($eval_error) if ($eval_error);
324 21 100       77 unless($tx->rollback()) {
325 1         113 Carp::croak 'failed to rollback transaction';
326             }
327             } else {
328 261 100       875 unless($tx->commit()) {
329 1         127 Carp::croak 'failed to commit transaction';
330             }
331             }
332              
333 280 100       706 if (wantarray) {
334 278         881 return ($result, $eval_error);
335             } else {
336 2         8 return $result;
337             }
338             }
339              
340             # eval function takes a block (&) sort of like CORE::eval
341             # eval will rollback on a caught die
342             sub eval(&) {
343             my $is_failure = sub {
344 4     4   8 my ($result, $eval_error) = @_;
345 4         9 return $eval_error;
346 4     4 1 1162 };
347 4         13 return eval_or_do($is_failure, @_);
348             }
349              
350             # do function takes a block (&) sort of like CORE::do
351             # do will rollback on a false result as well as before re-throwing a caught die
352             sub do(&) {
353             my $is_failure = sub {
354 278     278   445 my ($result, $eval_error) = @_;
355 278   66     1488 return !$result || $eval_error;
356 278     278 1 9273 };
357 278         918 my ($result, $eval_error) = eval_or_do($is_failure, @_);
358 278 100       665 if ($eval_error) {
359 12         2035 croak $eval_error, "\t...propogated";
360             }
361 266         1467 return $result;
362             }
363              
364             1;
365              
366             =pod
367              
368             =head1 NAME
369              
370             UR::Context::Transaction - API for software transactions
371              
372             =head1 SYNOPSIS
373              
374             my $o = Some::Obj->create(foo => 1);
375             print "o's foo is ",$o->foo,"\n"; # prints 1
376              
377             my $t = UR::Context::Transaction->begin();
378              
379             $o->foo(4);
380              
381             print "In transaction, o's foo is ",$o->foo,"\n"; # prints 4
382              
383             if (&should_we_commit()) {
384             $t->commit();
385             print "Transaction committed, o's foo is ",$o->foo,"\n"; # prints 4
386              
387             } else {
388             $t->rollback();
389             print "Transaction rollback, o's foo is ",$o->foo,"\n"; # prints 1
390             }
391              
392             =head1 DESCRIPTION
393              
394             UR::Context::Transaction instances represent in-memory transactions as a diff
395             of the contents of the object cache in the Process context. Transactions are
396             nestable. Their instances exist in the object cache and are subject to the
397             same scoping rules as other UR-based objects, meaning that they do not
398             disappear mearly because the lexical variable they're assigned to goes out of
399             scope. They must be explicitly disposed of via the commit or rollback methods.
400              
401             =head1 INHERITANCE
402              
403             UR::Context::Transaction is a subclass of UR::Context
404              
405             =head1 CONSTRUCTOR
406              
407             =over 4
408              
409             =item begin
410              
411             $t = UR::Context::Transaction->begin();
412              
413             Creates a new software transaction context to track changes to UR-based
414             objects. As all activity to objects occurs in some kind of transaction
415             context, the newly created transaction exists within whatever context was
416             current before the call to begin().
417              
418             $t = UR::Context::Transaction->begin(commit_validator => sub { ... });
419              
420             A validation function may be assigned with the C property.
421             When the transaction is committed, this function is called. The commit
422             proceeds if this function returns a true value. The default function,
423             C requires that all objects changed within the
424             transaction be valid, ie. that C<$obj->__errors__()> returns an empty list.
425             The validation function is passed one argument: the transaction object
426             being committed.
427              
428             =back
429              
430             =head1 METHODS
431              
432             =over 4
433              
434             =item commit
435              
436             $t->commit();
437              
438             Causes all objects with changes to save those changes back to the underlying
439             context.
440              
441             If the validation function (specified with the C param when
442             the transaction was created with C) returns false, the changes are
443             not committed to the encompassing context, C returns false and this
444             transaction remains in effect.
445              
446             Returns true if all the transaction's changes are committed to the encompassing
447             Context. This transaction object then becomes invalid, and its state will be
448             'committed'.
449              
450             =item rollback
451              
452             $t->rollback();
453              
454             Causes all objects with changes to have those changes reverted to their
455             state when the transaction began. Classes with properties whose meta-property
456             is_transactional => 0 are not tracked within a transaction and will not be
457             reverted.
458              
459             After C, this transaction becomes invalid, and the object will become
460             a L.
461              
462             =item delete
463              
464             $t->delete();
465              
466             delete() is a synomym for rollback
467              
468             =item has_changes
469              
470             $bool = $t->has_changes();
471              
472             Returns true if any UR-based objects have changes within the transaction.
473              
474             =item get_changes
475              
476             @changes = $t->get_changes();
477              
478             Return a list or L objects representing changes within the transaction.
479              
480             =back
481              
482             =head1 CLASS METHODS
483              
484             =over 4
485              
486             =item eval
487              
488             UR::Context::Transaction::eval BLOCK
489              
490             Executes the BLOCK (with no arguments) wrapped by a software transaction and a
491             CORE::eval. If the BLOCK dies then the exception is caught and the software
492             transaction is rolled back.
493              
494             =item do
495              
496             UR::Context::Transaction::do BLOCK
497              
498             Executes the BLOCK (with no arguments) wrapped by a software transaction and a
499             CORE::eval. If the BLOCK returns a true value and does not die then the
500             software transaction is committed. If the BLOCK returns false or dies then the
501             software transaction is rolled back.
502              
503             If the BLOCK throws an exception, it will be caught, the software transaction
504             rolled back, and the exception will be re-thrown with die().
505              
506             =back
507              
508             =head1 EXPORTS
509              
510             This module can export constants that match the valid values of the C
511             property: TRANSACTION_STATE_OPEN and TRANSACTION_STATE_COMMITTED
512              
513             =head1 SEE ALSO
514              
515             L
516              
517             =cut