The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package Rinci::Undo; # just to make PodWeaver happy

# VERSION

1;
# ABSTRACT: (DEPRECATED) Protocol for undo operations in functions

__END__

=pod

=encoding UTF-8

=head1 NAME

Rinci::Undo - (DEPRECATED) Protocol for undo operations in functions

=head1 VERSION

This document describes version 1.1.52 of Rinci::Undo (from Perl distribution Rinci), released on 2014-06-29.

=head1 SPECIFICATION VERSION

 1.1

=head1 STATUS

This protocol (riundo for short) is now deprecated in favor of
L<Rinci::Transaction> (ritx for short) for several reasons:

=over 4

=item * riundo is inherently unreliable

Undo information is returned by function I<after> the function has performed the
action. If function dies in the middle of action, client does not have the
information to undo the (partially completed) action. That is why in ritx, the
TM asks the function first for undo information before asking the function to
perform its action.

=item * ritx does not limit using the same function for undo

In riundo, we must call the same function (passing the previously obtained undo
data from the that function) to undo the information. This is sometimes slightly
cumbersome. The undo action might be provided by other functions, but we still
have to go through the same function first.

=item * ritx can also implement undo/redo

So there is no need for maintaining two specifications.

=back

=head1 SPECIFICATION

This document describes the Rinci undo protocol. This protocol must be followed
by functions that claim that they support undo (have their C<undo> C<feature>
set to true). Such functions are from here on called I<undoable function> (or
just function, unless when ambiguous).

The protocol is basically the non-OO version of the command pattern, a design
pattern most commonly used to implement undo/redo functionality. In this case,
each function behaves like a command object. You pass a special argument
C<-undo_action> with the value of C<do> and C<undo> to execute or undo a
command, respectively. For C<do> and C<undo>, the same set of arguments are
passed.

=head2 Requirements

Function MUST check special argument C<-undo_action> before it checks other
arguments. Function MUST at least support the following undo action: C<do>,
C<undo>. On unsupported/unknown undo action, function MUST return status 400,
with message like "Unsupported undo action".

If C<-undo_action> is not set, it means caller does not care about undo.
Undoable function should execute as any normal function.

=head2 Performing 'do'

To indicate that we need undo, we call function by passing special argument
C<-undo_action> with the value of C<do>. Function should perform its operation
and save undo data along the way. If C<-undo_action> is not passed or
false/undef, function should assume that caller does not need undo later, so
function need not save any undo data. After completing operation successfully,
function should return status 200, the result, and undo data. Undo data is
returned in the result metadata (the fourth element of result envelope),
example:

 [200, "OK", $result, {undo_data=>$undo_data}]

Undo data should be serializable so it is easy to be made persistent if
necessary (e.g. by some undo/transaction manager).

=head2 Performing 'undo'

To perform an undo, caller must call the function again with the same previous
arguments, except C<-undo_action> should be set to C<undo> and C<-undo_data> set
to undo data previously given by the function. Function should perform the undo
operation using the undo data. Upon success, it must return status 200, the
result, and an undo data (in other words, redo data, since it can be used to
undo the undo operation).

=head2 Performing 'redo'

To perform redo, caller can call the function again with <-undo_action> set to
C<undo> and C<-undo_data> set to the redo data given in the undo step. Or,
alternatively, caller can just perform a normal do (see above).

An example:

 $SPEC{setenv} = {
     v => 1.1,
     summary  => 'Set environment variable',
     args     => {
         name  => {req=>1, schema=>'str*'},
         value => {req=>1, schema=>'str*'},
     },
     features => {undo=>1},
 };
 sub setenv {
     my %args        = @_;
     my $name        = $args{name};
     my $value       = $args{value};
     my $undo_action = $args{-undo_action} // '';
     my $undo_data   = $args{-undo_data};

     my $old;
     if ($undo_action) {
         # save original value and existence state
         $old = [exists($ENV{$name}), $ENV{$name}];
     }

     if ($undo_action eq 'undo') {
         if ($undo_data->[0]) {
             $ENV{$name} = $undo_data->[1];
         } else {
             delete $ENV{$name};
         }
     } else {
         $ENV{$name} = $value;
     }

     [200, "OK", undef, $undo_action ? {undo_data=>$old} : {}];
 }

The above example declares an undoable command C<setenv> to set an environment
variable (C<%ENV>).

To perform command:

 my $res = setenv(name=>"DEBUG", value=>1, -undo_action=>"do");
 die "Failed: $res->[0] - $res->[1]" unless $res->[0] == 200;
 my $undo_data = $res->[3]{undo_data};

To perform undo:

 $res = setenv(name=>"DEBUG", value=>1,
               -undo_action="undo", -undo_data=>$undo_data);
 die "Can't undo: $res->[0] - $res->[1]" unless $res->[0] == 200;

After this undo, DEBUG environment variable will be set to original value. If it
did not exist previously, it will be deleted.

To perform redo:

 my $redo_data = $res->[3]{undo_data};
 $res = setenv(name=>"DEBUG", value=>1,
               -undo_action="undo", -undo_data=>$redo_data);

or you can just do:

 $res = setenv(name=>"DEBUG", value=>1, -undo_action="do");

=head2 Saving undo data in external storage

Although the complete undo data can be returned by the function in the
C<undo_data> result metadata property, sometimes it is more efficient to just
return a pointer to said undo data, while saving the actual undo data in some
external storage.

For example, if a function deletes a big file and wants to save undo data, it is
more efficient to move the file to trash directory and return its path as the
undo data, instead of reading the whole file content and its metadata to memory
and return it in C<undo_data> result metadata.

Functions which require undo trash directory should specify this in its
metadata, through the C<undo_trash_dir> dependency clause. For example:

 deps => {
     ...
     trash_dir => 1,
 }

When calling function, caller needs to provide path to undo trash directory via
special argument C<-trash_dir>, for example:

 -trash_dir => "/home/.trash/2fe2f4ad-a494-0044-b2e0-94b2b338056e"

=head2 What about non-undoable actions?

Like in real life, not all actions are undoable. Examples of
undoable/irreversible actions include wiping a file/directory (more generally
speaking, any action to permanently delete/destroy something, without backing up
the data first), sending an email (more generally speaking, any action that is
sent to an external entity beyond our control, unless that external entity
provides a way to undo the action).

An undoable function MUST NOT mix undoable and non-undoable actions. For
example:

 safe_delete(file=>'/path/to/file'); # puts file into Trash, undoable action
 safe_delete(file=>'/path/to/file', permanent=>1); # deletes file, non-undoable

The C<safe_delete> function above mixes undoable action (putting a file into
Trash directory) and non-undoable action (permanently deleting a file without
putting it in Trash). Without domain knowledge of the function, a caller cannot
know whether a call will be undoable or not. This will also prevent the function
from participating in a transaction, because transaction requires function call
to always be undoable, for rollback purpose.

The solution is to separate non-undoable action in another function, for
example:

 trash(file=>'/path/to/file');  # undoable, can execute inside transaction
 delete(file=>'/path/to/file'); # non-undoable, executes outside transaction
 empty_trash();                 # non-undoable, executes outside transaction

The non-undoable function is also non-transactional (it operates outside the
scope of a transaction). But it can still be idempotent. And it can manipulate
the transactions if it needs too. In the example, the empty_trash() function
instructs the transaction manager to discard the trash() transactions, since
after the trash is emptied, the trash() transactions cannot be undone anyway.

=head1 SEE ALSO

Related specifications: L<Rinci::Transaction>

=head1 HOMEPAGE

Please visit the project's homepage at L<https://metacpan.org/release/Rinci>.

=head1 SOURCE

Source repository is at L<https://github.com/sharyanto/perl-Rinci>.

=head1 BUGS

Please report any bugs or feature requests on the bugtracker website L<https://rt.cpan.org/Public/Dist/Display.html?Name=Rinci>

When submitting a bug or request, please include a test-file or a
patch to an existing test-file that illustrates the bug or desired
feature.

=head1 AUTHOR

Steven Haryanto <stevenharyanto@gmail.com>

=head1 COPYRIGHT AND LICENSE

This software is copyright (c) 2014 by Steven Haryanto.

This is free software; you can redistribute it and/or modify it under
the same terms as the Perl 5 programming language system itself.

=cut