How to Use a PHP Closure to Implement a SoapClient Method Staggering Retry

The Problem

I ran into an situation where I have to interact with an extremely unreliable SOAP web service using PHP’s SoapClient. I needed a way to consistently retry failures before eventually dying if it doesn’t ever succeed.

I prototyped this using procedural code, but when I implemented it for real, it was in object-oriented fashion. Procedural is an easier way for me to prototype and to explain programming ideas.

Imagine you have a Soap webservice called butt that poops.

Normally, you would do something like this.

try {
    $butt = new SoapClient();
    $butt->poop();
} catch (SoapFault $sf) {
    echo $sf->getMessage();
}

But now imagine that your butt doesn’t work right all of the time.

The Solution

To deal with this irregularity, let’s automatically retry poop to see if it eventually works.

In this example, I’m going to create a function called retriable.

/**
 * This function attempts to call $method on $client the $numRetries 
 * specified waiting for the $wait in seconds for the $maxWait 
 * in seconds.
 * 
 * If $wait is -1, rand(1, $maxWait) is used to determine wait time. 
 * This is meant to juggle requests that all may have come in at 
 * the exact same time. All of these requests hammering the
 * SoapServer at the exact time is causing performance degradation. 
 * By juggling the $wait, it could help spread out the requests.
 *
 * @param SoapClient $client
 * @param string $method Name of SOAP method to call
 * @param array $params An array of parameters to pass to the SOAP method, 
 *   if not use array()
 * @param integer $numRetries The number of times to retry
 * @param integer $wait The number of seconds to wait, if -1, retries 
 *   will wait between 1 and $maxWait seconds.
 * @param integer $maxWait The maximum number of seconds to ever wait.
 */
function retriable(SoapClient $client, $method, $params, $numRetries, $wait = -1, $maxWait = 4)
{
    $retry = 1;
    $waitInSeconds = function() use($wait, $maxWait, $params) {
        $ret = 1;
        if ($wait != -1) {
            $ret = rand(1, $maxWait);
        } else {
            $ret = $wait;
        }
        echo $ret . "\n";
        return $ret;
    };

    while ($retry <= $numRetries) {
        try {
            return call_user_func_array(array($client, $method), $params);
        } catch (\SoapFault $sf) {
            // If num retries is hit, throw the SoapFault
            if ($retry == $numRetries) {
                throw $sf;
            }
            $retry++;
            sleep($waitInSeconds());
        }

    };
}

Now instead of my original implementation, to call the SOAP method, I do it like this.

try {
    $client = new SoapClient();
    retriable($client, 'poop', array(), 3, 9);
} catch (SoapFault $sf) {
    echo $sf->getMessage();
}

This will call $client->poop().

Where’s the Closure?

The closure came into to play when calculating the number of seconds to wait. Obviously, I could have just used the $wait parameter value. But in my situation, the problem with the SOAP web service was that if a bunch of requests came through at the same time, the backend system would shit itself.

That happened a lot in my case because the requests coming into my application were from another business system sending me SOAP requests. Sometimes in the middle of the night, it would dump a bunch of requests on me all at once. While my code had no problem handling the requests, when I interacted with this other Soap web service, it struggled to keep up.

That’s why I concocted this retry method, with the additional feature to not just retry on a fixed number of seconds. If they all retried in 4 seconds, the backend system would continue to choke on the requests because they’d all come through again at the same time. By using PHP’s rand function, I could put some distance in time between retry attempts.

While this method will never totally alleviate the issues with this poorly performing business system, it did mitigate a lot of exceptions that required a human to get involved.

Share Button