1
\$\begingroup\$

I'm working with two remote APIs: one which manages customers and the other (a Paypal API interface) which manages billing. To delete an account, I must delete the records from both.

function deleteAccount($account_id) {
    try {
        $api1->deleteAccount( $account_id );
    } catch (Exception $e) {
        Logger::log(...);
        return false;
    }

    try {
        $api2->cancelBilling( $account_id );
    } catch (Exception $e) {
        Logger::log(...);
        return false;
    }
}

Suppose that the first operation (with api1) succeeds but the second operation (with api2) fails. This could happen, for example, if the api2 server was down - a factor which is out of my control.

I would then have deleted the account in api1, but would still be billing the user, which would lead to a corruption in my data (and some unhappy users!).

I have one idea on how to maximise the possibility of success, by doing various checks for errors:

function deleteAccount($account_id) {
    //Check that servers are up
    if(!$api1->canConnectToServer() || !$api2->canConnectToServer()) {
        return false;
    }
    if (!$api1->isAccountIDValid($account_id ) || !$api2->isAccountIDValid($account_id ) ) {
        return false;
    }

    try {
        $api1->deleteAccount( $account_id );
    } catch (Exception $e) {
        Logger::log(...);
        return false;
    }

    try {
        $api2->cancelBilling( $account_id );
    } catch (Exception $e) {
        Logger::log(...);
        return false;
    }
}

However, this seems a little flimsy and doesn't guarantee data integrity. Is there a better way to handle this? I'm also not sure whether I should return false or throw an exception.

\$\endgroup\$
4
\$\begingroup\$

Do you have access to the customer API? If so, I think the deletion of the Paypal account should be handled by it.

If you do not have access to it, it seems the most important call is the Paypal one, so you should first make sure that one succeeds. Once it does, you can delete the customer account. But no matter how you delete it, there will be some time, even if only a few seconds, when the customer account will be without a Paypal account. The customer API needs to handle this situation gracefully.

To make sure the customer account gets eventually deleted, I would add this account to some table and have a cron job process it every X seconds. The cron job should delete all the account in that table. It means that if deletion fails, it will try again a few seconds later, so it's safe to assume the account will eventually get deleted.

\$\endgroup\$
  • \$\begingroup\$ I'd do this as well. But everything that requires a valid account, e.g. placing a new order, would have to check "is this account deleted?" prior to placing that order or viewing the store at all. \$\endgroup\$ – chelmertz May 19 '12 at 8:08
3
\$\begingroup\$

For a few reasons, your best option is to use delayed job execution

That means setting up a job queue such as gearman, and instead of trying to do things all at once and in serial - you simply register a request to perform the job later.

The benefits of working this way are numerous and include:

  • The user doesn't have to sit their waiting while (potentially) slow processes run
  • You disassociate the user request from the api calls. If the api calls fail you can simply try again
  • Generally speaking you end up with much leaner and easier to maintain/test code in this way.

Therefore your code would become similar to:

function deleteAccount($account_id) {
    ... your code return early to abort ...
    $delayedExecution->do('delete from api one', $account_id);
    $delayedExecution->do('cancel billing', $account_id);
    return true;
}

I use gearman as an example - but you can implement it any way you wish. Just ensure your solution account for jobs that fail intermittently (job fails once, reschedule for later) and for jobs that fail consistently (something wrong, job fails 3 times - flag for action).

\$\endgroup\$
1
\$\begingroup\$

I agree with @Laurent about the cron jobs. A better solution would be a two-phase commit protocol but I don't know whether your APIs support it or not.

I'd prefer exceptions. Actually, I'd use two ifs instead of conditions like this:

if(!$api1->canConnectToServer() || !$api2->canConnectToServer()) {
    return false;
}

For example:

if (!$api1->canConnectToServer()) {
    throw new MyException('Unable to connect to API1.');
}
if (!$api2->canConnectToServer()) {
    throw new MyException('Unable to connect to API2.');
}

It provides detailed error messages which the caller could log or handle (different exception types could be helpful here for this). A simple false value does not say anything about the cause of the error and does not help debugging too much.

This answer could be helpful: to throw, to return or to errno?

\$\endgroup\$
  • \$\begingroup\$ Two-phase commit protocol would be the ideal solution if you have access to the API. \$\endgroup\$ – waitinforatrain May 19 '12 at 18:40

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.