File Coverage

blib/lib/Eval/Reversible.pm
Criterion Covered Total %
statement 55 58 94.8
branch 17 22 77.2
condition n/a
subroutine 14 16 87.5
pod 6 6 100.0
total 92 102 90.2


line stmt bran cond sub pod time code
1             package Eval::Reversible;
2              
3             our $AUTHORITY = 'cpan:GSG';
4             our $VERSION = '0.90';
5              
6 2     2   389931 use v5.10;
  2         13  
7 2     2   1116 use Moo;
  2         25862  
  2         13  
8              
9 2     2   4249 use Types::Standard qw( Bool Str ArrayRef CodeRef );
  2         160428  
  2         24  
10 2     2   3646 use MooX::HandlesVia;
  2         22136  
  2         14  
11              
12 2     2   293 use Scalar::Util qw( blessed );
  2         6  
  2         128  
13              
14 2     2   1063 use namespace::clean; # don't export the above
  2         25416  
  2         19  
15              
16             # Goes after namespace::clean, since we actually want to include this into our
17             # namespace
18 2     2   899 use Exporter 'import';
  2         7  
  2         114  
19              
20             BEGIN {
21 2     2   1730 our @EXPORT_OK = qw( to_undo reversibly );
22             };
23              
24             our $Current_Reversible;
25              
26             =head1 NAME
27              
28             Eval::Reversible - Evals with undo stacks
29              
30             =head1 SYNOPSIS
31              
32             use Eval::Reversible;
33              
34             my $reversible = Eval::Reversible->new(
35             failure_warning => "Undoing actions..",
36             );
37              
38             $reversible->run_reversibly(sub {
39             # Do something with a side effect
40             open my $fh, '>', '/tmp/file' or die;
41              
42             # Specify how that side effect can be undone
43             # (assuming '/tmp/file' did not exist before)
44             $reversible->add_undo(sub { close $fh; unlink '/tmp/file' });
45              
46             operation_that_might_die($fh);
47             operation_that_might_get_SIGINTed($fh);
48              
49             close $fh;
50             unlink '/tmp/file';
51              
52             $reversible->clear_undo;
53             $reversible->failure_warning("Wasn't quite finished yet...");
54              
55             another_operation_that_might_die;
56             $reversible->add_undo(sub { foobar; });
57              
58             $reversible->disarm;
59              
60             # This could die without an undo stack
61             another_operation_that_might_die;
62              
63             $reversible->arm;
64              
65             # Previous undo stack back in play
66             });
67              
68             # Alternative caller
69             Eval::Reversible->run_reversibly(sub {
70             my $reversible = $_[0];
71              
72             $reversible->add_undo(...);
73             ...
74             });
75              
76             # Alternative function interface
77             reversibly {
78             to_undo { ... };
79             die;
80             } 'Failed to run code; undoing...';
81              
82             =head1 DESCRIPTION
83              
84             Run code and automatically reverse their side effects if the code fails. This is done by
85             way of an undo stack. By calling L right after a side effect, the effect is
86             undone on the event that the L sub dies. For example:
87              
88             $reversible->run_reversibly(sub {
89             print "hello\n";
90             $reversible->add_undo(sub { print "goodbye\n" });
91             die "uh oh\n" if $something_bad;
92             });
93              
94             This prints "hello" if C<$something_bad> is false. If it's true, then both "hello" and
95             "goodbye" are printed and the exception "uh oh" is rethrown.
96              
97             Upon failure, any code refs provided by calling L are executed in reverse
98             order. Conceptually, we're unwinding the stack of side effects that C<$code> performed
99             up to the point of failure.
100              
101             =head1 ATTRIBUTES
102              
103             =head2 failure_warning
104              
105             This is the message that will warn as soon as the operation failed. After this, the undo
106             stack is unwound, and the exception is rethrown. Default is no message.
107              
108             =cut
109              
110             has failure_warning => (
111             is => 'rw',
112             isa => Str,
113             required => 0,
114             );
115              
116             =head2 undo_stack
117              
118             The undo stack, managed in LIFO order, as an arrayref of coderefs.
119              
120             This attribute has the following handles, which is what you should really interact with:
121              
122             =head3 add_undo
123              
124             Adds another coderef to the undo stack via push.
125              
126             =head3 pop_undo
127              
128             Removes the last coderef and returns it via pop.
129              
130             =head3 clear_undo
131              
132             Clears all undo coderefs from the stack. Handy if the undo stack needs to be cleared out
133             early if a "point of no return" has been reached prior the end of the L
134             code block. Alternatively, L could be used, but it doesn't clear the existing
135             stack.
136              
137             =head3 is_undo_empty
138              
139             Returns a boolean that indicates whether the undo stack is empty or not.
140              
141             =cut
142              
143             has undo_stack => (
144             is => 'ro',
145             isa => ArrayRef[CodeRef],
146             required => 1,
147             default => sub { [] },
148             handles_via => 'Array',
149             handles => {
150             add_undo => 'push',
151             pop_undo => 'pop',
152             clear_undo => 'clear',
153             is_undo_empty => 'is_empty',
154             },
155             );
156              
157             =head2 armed
158              
159             Boolean that controls if L code blocks will actually run the undo stack
160             upon failure. Turned on by default, but this can be enabled and disabled at will before
161             or inside the code block.
162              
163             Has the following handles:
164              
165             =head3 arm
166              
167             Arms the undo stack.
168              
169             =head3 disarm
170              
171             Disarms the undo stack.
172              
173             =cut
174              
175             # XXX: MooX::HandlesVia can't really handle write operations on non-refs. So, we're
176             # faking the handles here.
177              
178             has armed => (
179             is => 'rw',
180             isa => Bool,
181             required => 0,
182             default => 1,
183             );
184              
185 1     1 1 1006 sub arm { shift->armed(1) }
186 2     2 1 797 sub disarm { shift->armed(0) }
187              
188             =head1 METHODS
189              
190             =head2 run_reversibly
191              
192             $reversible->run_reversibly($code);
193             Eval::Reversible->run_reversibly($code);
194              
195             Executes a code reference (C<$code>) allowing operations with side effects to be
196             automatically reversed if C<$code> fails or is interrupted. Automatically clears the
197             undo stack before the start of the code block.
198              
199             Can be called as a class method, which will auto-create a new object, and pass it along
200             as the first parameter to C<$code>.
201              
202             If C<$code> is interrupted with SIGINT, the side effects are undone and an
203             exception "SIGINT\n" is thrown.
204              
205             =cut
206              
207             sub run_reversibly {
208 13     13 1 6243 my ($self, $code) = @_;
209 13 50       30 die "Cannot call run_reversibly without a code block!" unless $code;
210              
211 13 100       43 unless (blessed $self) {
212 1         2 my $class = $self;
213 1         22 $self = $class->new;
214             }
215              
216 13         263 $self->clear_undo;
217              
218             # If disarmed, just run the code without eval
219 13 100       715 unless ($self->armed) {
220 1         7 $self->$code();
221 0         0 return;
222             }
223              
224 12     0   268 local $SIG{INT} = sub { die "SIGINT\n" };
  0         0  
225 12     0   192 local $SIG{TERM} = sub { die "SIGTERM\n" };
  0         0  
226              
227 12         45 eval { $self->$code() };
  12         37  
228 12 100       4955 if ( my $exception = $@ ) {
229             # Re-check this because it may change inside the code block
230 8 100       135 if ($self->armed) {
231 7 50       138 warn $self->failure_warning if $self->failure_warning;
232 7         56 $self->run_undo;
233              
234             # Re-throw the exception, with commentary
235 7         533 die "\nThe exception that caused rollback was: $exception";
236             }
237             else {
238             # Just die like it wasn't even in an eval
239 1         31 die $exception;
240             }
241             }
242             }
243              
244             =head2 run_undo
245              
246             $reversible->run_undo;
247              
248             Runs the undo stack thus far. Always runs the bottom of the stack first (LIFO order). A
249             finished run will clear out the stack via pop.
250              
251             Can be called outside of L if the eval was successful, but the undo
252             stack still needs to be ran.
253              
254             =cut
255              
256             sub run_undo {
257 8     8 1 602 my ($self) = @_;
258              
259 8         125 while (my $undo = $self->pop_undo) {
260 11         480 eval { $self->$undo() };
  11         25  
261 11 50       3574 warn "Exception during undo: $@" if $@;
262             }
263             }
264              
265             1;
266              
267             =head1 EXPORTABLE FUNCTIONS
268              
269             Eval::Reversible also supports an exportable function interface. Though its usage is
270             somewhat legacy, the functions are prototyped for reduced sigilla.
271              
272             None of the functions are exported by default.
273              
274             =head2 reversibly
275              
276             reversibly {
277             ...
278             } 'Failure message';
279              
280             Creates a new localized Eval::Reversible object and calls L on it. An
281             optional failure message can be added to the end of coderef.
282              
283             =cut
284              
285             sub reversibly (&;$) {
286 3     3 1 6572 my ($code, $fail_msg) = @_;
287 3 50       10 die "Cannot call reversibly without a code block!" unless $code;
288              
289 3         50 local $Current_Reversible = __PACKAGE__->new;
290 3 100       101 $Current_Reversible->failure_warning($fail_msg) if defined $fail_msg;
291 3         52 $Current_Reversible->run_reversibly($code);
292             }
293              
294             =head2 to_undo
295              
296             # Only inside of a reversibly block
297             to_undo { rollback_everything };
298              
299             Adds to the existing undo stack. Dies if called outside of a L block.
300              
301             =cut
302              
303             sub to_undo (&) {
304 11     11 1 4478 my ($code) = @_;
305 11 50       42 die "Cannot call to_undo without a code block!" unless $code;
306 11 100       39 die "Cannot call to_undo outside of an reversibly block!" unless $Current_Reversible;
307              
308 9         177 $Current_Reversible->add_undo($code);
309             }
310              
311             =head1 SEE ALSO
312              
313             L, L, L.
314              
315             =head1 AUTHOR
316              
317             Grant Street Group
318              
319             =head1 LICENSE AND COPYRIGHT
320              
321             Copyright 2018 Grant Street Group
322              
323             This program is free software; you can redistribute it and/or modify it
324             under the terms of the the Artistic License (2.0). You may obtain a
325             copy of the full license at:
326              
327             L
328              
329             =cut
330              
331             1;