File Coverage

blib/lib/Eval/Reversible.pm
Criterion Covered Total %
statement 58 61 95.0
branch 17 22 77.2
condition n/a
subroutine 15 17 88.2
pod 6 6 100.0
total 96 106 90.5


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