tags:

views:

243

answers:

10

Hi,

What is the best way to perform a couple of tasks together and if one task fails the next tasks should not be completed? I know if it were the database operations then I should have used Transactions but I am talking about different types of operations like the following:

All tasks must pass:

SendEmail ArchiveReportsInDatabase CreateAFile

In the above scenario all the tasks must pass or else the whole batch operation must be rolback.

+2  A: 

in C#

return SendEmail() && ArchiveResportsInDatabase() && CreateAFile();

Echostorm
Is that the best way?
azamsharp
It stops the process when one fails and has the shortest amount of steps / structure I can see while accomplishing what you want.
Echostorm
Simpler:return SendEmail()
Kevin Conner
If CreateAFile() fails, the other steps will still be done. Is that what you want?
Kristopher Johnson
Extensive indentation of code make its harder to read. What if you had not just 3 but 10 operations?
Ates Goral
If CreateFile fails everything fails.
azamsharp
C# will stop evaluating the IF from left to right on the first false eval
Echostorm
A: 

If your language allows it, this is very tidy:

  1. Put your tasks in an array of code blocks or function pointers.
  2. Iterate over the array.
  3. Break if any block returns failure.
Kevin Conner
+1  A: 

Another idea:

try {
    task1();
    task2();
    task3();
    ...
    taskN();
}
catch (TaskFailureException e) {
    dealWith(e);
}
Kevin Conner
I was using the above approach but thought if there is some other better way out there!
azamsharp
A: 

You didn't mention what programming language/environment you're using. If it's the .NET Framework, you might want to take a look at this article. It describes the Concurrency and Control Runtime from Microsoft's Robotics Studio, which allows you to apply all sorts of rules on a set of (asynchronous) events: for example, you can wait for any number of them to complete, cancel if one event fails, etc. It can run things in multiple threads as well, so you get a very powerful method of doing stuff.

Yuval
using .net C# windows application
azamsharp
A: 

You don't specify your environment. In Unix shell scripting, the && operator does just this.

SendEmail () {
  # ...
}
ArchiveReportsInDatabase () {
  # ...
}
CreateAFile () {
  # ...
}

SendEmail && ArchiveReportsInDatabase && CreateAFile
Glomek
+1  A: 

A couple of suggestions:

In a distributed scenario, some sort of two-phase commit protocol may be needed. Essentially, you send all participants a message saying "Prepare to do X". Each participant must then send a response saying "OK, I guarantee I can do X" or "No, can't do it." If all participants guarantee they can complete, then send the message telling them to do it. The "guarantees" can be as strict as needed.

Another approach is to provide some sort of undo mechanism for each operation, then have logic like this:

try:
    SendEmail()
    try:
        ArchiveReportsInDatabase()
        try:
             CreateAFile()
        except:
            UndoArchiveReportsInDatabase()
            raise
    except:
        UndoSendEmail()
        raise
except:
    // handle failure

(You wouldn't want your code to look like that; this is just an illustration of how the logic should flow.)

Kristopher Johnson
A: 

If you're using a language which uses sort-circuit evaluation (Java and C# do), you can simply do:

return SendEmail() && ArchiveResportsInDatabase() && CreateAFile();

This will return true if all the functions return true, and stop as soon as the first one return false.

Eugene Katz
But if CreateAFile return false then SendEmail and ArchiveReportsInDatabase has already been executed. I want to abort all the operations if one fails. Seems like this is not possible. It is an undo functionality!
azamsharp
I was thinking about that and was going to address it, but then I saw that the first method is SendEmail(), and since you can't really undo that, I figured you really meant "abort" rather than "roll back".
Eugene Katz
You are right! I guess then at this point if the email does not have any dependencies on other functions it can be triggered last.
azamsharp
It's actually called "short-circus"! :)
Constantin
A: 

Exceptions are generally good for this sort of thing. Pseudo-Java/JavaScript/C++ code:

try {
    if (!SendEmail()) {
        throw "Could not send e-mail";
    }

    if (!ArchiveReportsInDatabase()) {
        throw "Could not archive reports in database";
    }

    if (!CreateAFile()) {
        throw "Could not create file";
    }

    ...

} catch (Exception) {
    LogError(Exception);
    ...
}

Better still if your methods throw exceptions themselves:

try {
    SendEmail();
    ArchiveReportsInDatabase();
    CreateAFile();
    ...

} catch (Exception) {
    LogError(Exception);
    ...
}

A very nice outcome of this style is that your code doesn't get increasingly indented as you move down the task chain; all your method calls remain at the same indentation level. Too much indentation makes the code harder to read.

Moreover, you have a single point in the code for error handling, logging, rollback etc.

Ates Goral
Yup! that is the way I originally coded! Thanks
azamsharp
Does that make it the best solution? Its one option, but it sounds like you need the suggestion by Kristopher Johnson
roryf
So if the last step fails how do you retry it later?
JC
Retrying of the steps is not a concern that is being tried to address here. But the *rollback* can be easily implemented in the catch block.
Ates Goral
You can't really rollback sending an email though.
JC
In a real life application, you'd probably make sending an e-mail the last step :)
Ates Goral
A: 

To really do it right you should use an asyncronous messaging pattern. I just finished a project where I did this using nServiceBus and MSMQ.

Basically, each step happens by sending a message to a queue. When nServiceBus finds messages waiting in the queue it calls your Handle method corresponding to that message type. This way each individual step is independently failable and retryable. If one step fails the message ends up in an error queue so you can easily retry it later.

These pure-code solutions being suggested aren't as robust since if a step fails you would have no way to retry only that one step in the future and you'd have to implement rollback code which isn't even possible in some cases.

JC
+4  A: 

Rollbacks are tough - AFAIK, there's really only 2 ways to go about it. Either a 2 phase commit protocol, or compensating transactions. You really have to find a way to structure your tasks in one of these fashions.

Usually, the better idea is to take advantage of other folks' hard work and use technologies that already have 2PC or compensation built in. That's one reason that RDBMS are so popular.

So, the specifics are task dependent...but the pattern is fairly easy:

class Compensator {
   Action Action { get; set; }
   Action Compensate { get; set; }
}

Queue<Compensator> actions = new Queue<Compensator>(new Compensator[] { 
   new Compensator(SendEmail, UndoSendEmail),
   new Compensator(ArchiveReportsInDatabase, UndoArchiveReportsInDatabase),
   new Compensator(CreateAFile, UndoCreateAFile)
});

Queue<Compensator> doneActions = new Queue<Compensator>();
while (var c = actions.Dequeue() != null) {
   try {
      c.Action();
      doneActions.Add(c);
   } catch {
      try {
        doneActions.Each(d => d.Compensate());
      } catch (EXception ex) {
        throw new OhCrapException("Couldn't rollback", doneActions, ex);
      }
      throw;
   }
}

Of course, for your specific tasks - you may be in luck.

  • Obviously, the RDBMS work can already be wrapped in a transaction.
  • If you're on Vista or Server 2008, then you get Transactional NTFS to cover your CreateFile scenario.
  • Email is a bit trickier - I don't know of any 2PC or Compensators around it (I'd only be slightly surprised if someone pointed out that Exchange has one, though) so I'd probably use MSMQ to write a notification and let a subscriber pick it up and eventually email it. At that point, your transaction really covers just sending the message to the queue, but that's probably good enough.

All of these can participate in a System.Transactions Transaction, so you should be in pretty good shape.

Mark Brackett
Thanks for the useful information!
azamsharp