The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.

NAME

Rinci::function::Undo - Protocol for undo operations in functions

VERSION

version 1.1.21

SPECIFICATION VERSION

 1.1

SPECIFICATION

This document describes the Rinci undo protocol. This protocol must be followed by functions that claim that they support undo (have their undo feature set to true).

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 -undo_action with the value of do and undo to execute or undo a command, respectively. For do and undo, the same set of arguments are passed. The function later returns an undo data containing a list of undo steps (or, in the case of transactional function, record the undo steps in the transaction manager's undo journal).

Do and undo without transaction

Performing do. To indicate that we need undo, we call function by passing special argument -undo_action with the value of do. Function should perform its operation and save undo data along the way. If -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).

Performing undo. To perform an undo, caller must call the function again with the same previous arguments, except -undo_action should be set to undo and -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 (i.e., redo data, since it can be used to undo the undo operation).

Performing redo. To perform redo, caller can call the function again with <-undo_action> set to undo and -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 setenv to set an environment variable (%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");

Do and undo with transaction

If a function is declared to be transactional (tx feature set to at least {use=>1} or {req=>1}, and idempotent feature currently also needs to be set to true), the protocol is different.

Performing do. Aside from -undo_action set to do, the function is passed -tx_manager special argument (the transaction manager object, described in Rinci::Transaction) and -tx_call_id (produced by calling record_call() method on the transaction manager; can be done by the function itself if this argument is not passed). The whole operation and its undo as well must be comprised of a series of unit steps. Before performing each step, its undo step must be recorded in the undo journal, which will record the information to a stable storage:

 my $tx      = $args{-tx_manager};
 my $call_id = $args{-tx_call_id};
 if (!$call_id) {
     $res = $tx->record_call(args=>\%args);
     return $res unless $res->[0] == 200;
     $call_id = $res->[2];
 }

 my $res;
 for my $step (@steps) {
     my $undo_step = undo_of($step);
     $res = $tx->record_undo_step(call_id=>$call_id, data=>$undo_step);
     $res = perform_step($step) if $res->[0] == 200;
     if ($res->[0] != 200) {
         $rollres = $tx->rollback;
         if ($rollres->[0] == 200) {
             return [500, "Rollbacked (after failed step $step->[0]: ".
                     "$res->[0] - $res->[1]"];
         } else {
             return [532, "Failed rolling back: $rollres->[0] - $rollres->[1] ".
                     "(after failed step $step->[0]: $res->[0] - $res->[1])"];
         }
     }
 }
 [200, "OK"];

If the system crashes after a certain step, the next time transaction manager is started again it will perform a rollback recovery of the transaction using the recorded undo steps.

On success the function must return 200 but need not return undo_data in the result metadata, since undo data is already recorded by the transaction manager.

Performing undo. To perform undo, caller sets -undo_action to undo and also passes -tx_manager and -tx_call_id. Function gets the list of steps from the transaction manager object:

 my $res = $tx->get_undo_steps(call_id=>$call_id);
 return $res unless $res->[0] == 200;
 my @steps = @{$res->[2]};

It should then perform each step, preceded by recording each step's redo step. On success it should return 200.

 for my $step (@steps) {
     my $undo_step = undo_of($step);
     $res = $tx->record_redo_step(call_id=>$call_id, data=>$undo_step);
     $res = perform_step($step) if $res->[0] == 200;
     if ($res->[0] != 200) {
         $rollres = $tx->rollback;
         if ($rollres->[0] == 200) {
             return [500, "Rollbacked (after failed step $step->[0]: ".
                     "$res->[0] - $res->[1]"];
         } else {
             return [532, "Failed rolling back: $rollres->[0] - $rollres->[1] ".
                     "(after failed step $step->[0]: $res->[0] - $res->[1])"];
         }
     }
 }

Performing redo. To perform redo, caller sets -undo_action to redo and also passes -tx_manager and -tx_call_id. Function gets the list of redo steps from the transaction manager:

 my $res = $tx->get_redo_steps(call_id=>$call_id);
 return $res unless $res->[0] == 200;
 my @steps = @{$res->[2]};

and then it performs steps like in the do steps.

Steps

Action step and undo step should be an a hash with the following known keys: _seq (used internally by TM, don't touch), _f (fully qualified name of function, you don't have to set it since TM will use something like caller() to detect), n (step name), a (step arguments, usually an array).

Saving undo data in external storage

Although the complete undo data can be returned by the function in the 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 undo_data result metadata.

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

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

When calling function, caller needs to provide path to undo trash directory via special argument -undo_trash_dir. Or if function is transactional, function should get undo trash dir using $tx->get_undo_trash_dir. For example:

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

SEE ALSO

Riap::Transaction

Setup

Implementations: Perinci::Tx::Manager, Perinci::Sub::Gen::Undoable

AUTHOR

Steven Haryanto <stevenharyanto@gmail.com>

COPYRIGHT AND LICENSE

This software is copyright (c) 2012 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.