dgaramond2 has asked for the wisdom of the Perl Monks concerning the following question:

eval { TX: { # step 1: database $dbh->do("CREATE DATABASE foo"); push @undo, sub { $dbh->do("DROP DATABASE foo"); }; # step 2a: filesystem mkdir "/path/foo" or last TX; push @undo, sub { rmdir "/path/foo" }; # step 2b push @undo, sub { remove_tree "/path/foo" }; create_a_bunch_of_files_in("/path/foo") or last TX; # step 3: logger (usually also on filesystem) write_log("Database and files setup") or last TX; # step 4: email (it's okay to fail this step) send_email('foo@bar.com'); # mark success @undo = (); } # rollback, we force each step even though it dies for (reverse @undo) { eval { $_->() }; } };

Is there a module to do this sort of thing more clearly and correctly? In essence, I am changing different resources (filesystem, database, etc) in a number of steps. In the absense of a global transaction manager, the next best thing I can do is rollback/undo the previous steps should things go wrong in the middle.

Replies are listed 'Best First'.
Re: Poor man's transaction construct
by ikegami (Patriarch) on Nov 24, 2010 at 03:23 UTC
    You have an eval, but you don't take advantage of it. In fact, you even rollback inside the eval, which makes no sense.
    use Sub::ScopeFinalizer qw( scope_finalizer ); { my @undo; my $guard = scope_finalizer { for (reverse(@undo)) { eval { $_->(); 1 } or warn($@); } }; $dbh->do("CREATE DATABASE foo"); push @undo, sub { $dbh->do("DROP DATABASE foo"); }; mkdir("/path/foo") or die(...); push @undo, sub { rmdir "/path/foo" }; ... @undo = (); # Or $guard->disable(); }

    The outer curlies could be an eval, a loop or just bare curlies; it's not relevant. It the block is exited via die, last, etc, the rollback will occur.

      Ah yes, thank you. That saves me a level of indentation.

      As for rollback in eval(), I want to make sure that when a rollback step dies, the next step is still executed, so that "cleanup is robust". I need to dwell some more on whether this is actually a good thing or bad.

        the rollback should die if a step dies..or you will quite possibly be undoing things that cannot be undone if the prior (undo) step failed. automatically undoing with commands like "rm -r -f dirname" should not (in my opinion) be ever run automatically, and as part of an undo sequence even more dangerous.
        i think it is a bit ambitious to be constructing code to automatically undo other code. the original (non-undo) code fails for some unknown reason, which was not catered for in the code...so how does one then produce undo code that should be even more robust?
        also the entire undo sequence does not need to follow exactly every step of normal run code. if the normal code creates a db, then creates tables, and then populates those..the undo should be merely a drop db. so if you absolutely must write this code, by grouping certain steps linked to a single undo step, it will be more robust by virtue of fewer undo steps code to run.
        the hardest line to type correctly is: stty erase ^H

        As for rollback in eval(),

        You're talking about the eval in the rollback code. I was talking about the other eval.

Re: Poor man's transaction construct
by duelafn (Parson) on Nov 24, 2010 at 02:28 UTC

    Don't forget to reverse those undo steps!

    unshift @undo, sub { ... };

    Good Day,
        Dean

      Yup, sorry, missed a 'reverse' there. Fixed.
Re: Poor man's transaction construct
by aquarium (Curate) on Nov 24, 2010 at 01:55 UTC
    i don't know of a module but have worked with large cross-platform upgrade scripts in perl, wrapped in a shell script. Anyway, the point i was going to make is that you cannot guarantee that any undo step will do the right thing, depending on the many variables involved on a server environment. in unforseen circumstances, any automated undo can wreak more havoc. the upgrade scripts i worked with had lots of sanity checking and logging of every step in a separate logging directory. in fact the upgrade could run from a separate mount point or drive, so it used as little space in main system as possible. anyway, the upgrade script had milestones for groups of smaller steps, and the script "knew" which small steps were just logged as upgrade log warnings or contributed to milestone failing and upgrade stopped. the upgrade could pick up from where it left off, as long as it was a milestone...so any human operator intervention could concentrate on either undoing or finalising a few small steps, by hand.
    the kind of sanity checking and defensive programming would include such things as: if you're creating a database, try to connect and do a fake select that doesn't require a table, and or use return codes from db when executing the statement, making sure to specify username/password and admin level connect style. for creating tables, you can populate one row of the table with well chosen fake data, and make sure you can read it back and insert values match returned values. the upgrade log showed had a timestamp for each step starting and ending, and also included the information returned from any of the rigorous sanity checking, in unadulturated form returned from DB or OS etc. this allowed the upgrade operator to pinpoint fairly well exactly where things fell over.
    the hardest line to type correctly is: stty erase ^H
Re: Poor man's transaction construct
by dgaramond2 (Monk) on Nov 24, 2010 at 02:44 UTC