Skip to content

Instantly share code, notes, and snippets.

@SammyK
Last active May 25, 2017 13:11
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save SammyK/d11854b08004aa0ed5972942c7e9fa8d to your computer and use it in GitHub Desktop.
Save SammyK/d11854b08004aa0ed5972942c7e9fa8d to your computer and use it in GitHub Desktop.
Proposed `retry` keyword in PHP 7.NEXT

Proposed retry keyword in PHP

This is a pivot of the original syntax proposal thanks to feedback from twitter.

The retry keyword adds to the try\catch\finally block to optionally execute an arbitrary statement before jumping to the top of the try block n times.

TL;DR The retry keyword offers a cleaner, more readable & more efficient solution to a common problem.

A simple example

class RecoverableException extends Exception {}

$id = 42;
try {
    throw new RecoverableException("FAILED getting ID #{$id}");
} retry 3 => $attempt (RecoverableException $e) {
    echo "Failed getting ID #{$id} on try #{$attempt}. Retrying...\n";
    sleep(1);
} catch (RecoverableException $e) {
    echo $e->getMessage().PHP_EOL;
}

Output:

Failed getting ID #42 on try #1. Retrying...
Failed getting ID #42 on try #2. Retrying...
Failed getting ID #42 on try #3. Retrying...
FAILED getting ID #42

Features

The full feature set of the new retry syntax includes the following:

  • Retry the try block n times (or forever with INF or by omitting the int literal)
  • Execute arbitrary code before each retry (to sleep, log, check exception code, etc)
  • Access to the instance of the thrown exception before the try block is retried
  • Access to the number of times the try block has been executed (with $attempt)
  • Specify the exceptions to be retried
  • Retry multiple recoverable exceptions with multi-catch syntax (added in PHP 7.1)
  • Break out of retry attempts for reasons other than the exception not being thrown or running out of retry attempts (using break)

User-land implementation

In order to achieve all the features described above with a user-land implementation, the following implementation of a retry() function would suffice.

function retry(int $retryCount, callable $tryThis, callable $beforeRetry = null, array $targetExceptions = ['Exception'])
{
    $attempts = 0;
    tryCode:
    try {
        return $tryThis();
    } catch (\Throwable $e) {
        $isTargetException = false;
        foreach ($targetExceptions as $targetException) {
            if ($e instanceof $targetException) {
                $isTargetException = true;
                break;
            }
        }
        if (!$retryCount || !$isTargetException) {
            throw $e;
        }
        $retryCount--;
        $shouldRetry = true;
        if ($beforeRetry) {
            $shouldRetry = $beforeRetry($e, ++$attempts);
        }
        if ($shouldRetry) {
            goto tryCode;
        }
        throw $e;
    }
}

Full usage of all the features of the retry() function might look like this.

class RecoverableException extends Exception {}
class AnotherRecoverableException extends Exception {}
class NonRecoverableException extends Exception {}

$id = 42;
try {
    $result = retry(3, function () use ($id) {
        throw new AnotherRecoverableException("FAILED getting ID #{$id}");
    }, function ($e, $attempt) use ($id) {
        if (42 === $e->getCode()) {
            return false;
        }
        echo "Failed getting ID #{$id} on try #{$attempt}. Retrying...\n";
        sleep(1);
        return true;
    }, [RecoverableException::class, AnotherRecoverableException::class]);
} catch (RecoverableException | AnotherRecoverableException $e) {
    echo $e->getMessage().PHP_EOL;
} catch (NonRecoverableException $e) {
    echo "I could not retry this exception\n";
    echo $e->getMessage().PHP_EOL;
}

Problems with the user-land implementation

  • Not easily readable
  • Specifying exceptions to catch for retry requires invoking a lot of opcodes
  • Requires two levels of try/catches
  • ???Adds a new scope/execution context to the stack for each callback
  • Due to closure, have to use use ($foo) if you want to include vars outside of callback scope (harder to read & easy to forget)

Same example with retry keyword

With the new retry keyword, the above example could be changed to this.

$id = 42;
try {
	throw new RecoverableException("FAILED getting ID #{$id}");
} retry 3 => $attempt (RecoverableException | AnotherRecoverableException $e) {
	if (42 === $e->getCode()) {
		break;
	}
	echo "Failed getting ID #{$id} on try #{$attempt}. Retrying...\n";
	sleep(1);
} catch (RecoverableException | AnotherRecoverableException $e) {
	echo $e->getMessage().PHP_EOL;
} catch (NonRecoverableException $e) {
	echo "I could not retry this exception\n";
	echo $e->getMessage().PHP_EOL;
}

Why it's better

  • Way more readable
  • More efficient at the op-code level
  • n% less boilerplate code
  • Offers a clean out-of-the-box solution to a common problem

Opposition

Why not just wrap the try/catch block in a loop?

Like this:

for ($i = 0; $i < $n; $i++) {
	try {
		/* Stuffs here */
		break;
	} catch(Exception $e) {
		/* Handle $e */
	}
}

The problem (other than readability) is if you want to execute arbitrary code before the retry, but not on the final fail. A common use-case is the need to execute sleep(1) before each retry and log_error(/* */) (or whatever) on the final fail. In the above example, sleep(1) would be added to the catch block and will be called even after all the retries are used up, thus causing an unnecessary extra second added to the execution time. In addition, we might want to halt script execution with exit on the final fail only.

This of course can be handled pretty easily with an if/else to detect the last try, but this just adds even more boilerplate & impairs readability.

Opcode Analysis

The user-land implementation of the retry() function invokes the following 63 opcodes.

function name:  retry
number of ops:  63
compiled vars:  !0 = $retryCount, !1 = $tryThis, !2 = $beforeRetry, !3 = $targetExceptions, !4 = $attempts, !5 = $e, !6 = $isTargetException, !7 = $targetException, !8 = $shouldRetry
line     #* E I O op                           fetch          ext  return  operands
-------------------------------------------------------------------------------------
   7     0  E >   EXT_NOP
         1        RECV                                             !0
         2        RECV                                             !1
         3        RECV_INIT                                        !2      null
         4        RECV_INIT                                        !3      <array>
   9     5        EXT_STMT
         6        ASSIGN                                                   !4, 0
  11     7    >   NOP
  12     8        EXT_STMT
         9        INIT_DYNAMIC_CALL                                        !1
        10        EXT_FCALL_BEGIN
        11        DO_FCALL                                      0  $10
        12        EXT_FCALL_END
        13      > RETURN                                                   $10
        14*       JMP                                                      ->61
  13    15  E > > CATCH                                                    'Throwable', !5
  14    16    >   EXT_STMT
        17        ASSIGN                                                   !6, <false>
  15    18        EXT_STMT
        19      > FE_RESET_R                                       $12     !3, ->30
        20    > > FE_FETCH_R                                               $12, !7, ->30
  16    21    >   EXT_STMT
        22        FETCH_CLASS                                 640  :13     !7
        23        INSTANCEOF                                       ~14     !5, $13
        24      > JMPZ                                                     ~14, ->29
  17    25    >   EXT_STMT
        26        ASSIGN                                                   !6, <true>
  18    27        EXT_STMT
        28      > JMP                                                      ->30
        29    > > JMP                                                      ->20
        30    >   FE_FREE                                                  $12
  21    31        EXT_STMT
        32        BOOL_NOT                                         ~16     !0
        33      > JMPNZ_EX                                         ~16     ~16, ->36
        34    >   BOOL_NOT                                         ~17     !6
        35        BOOL                                             ~16     ~17
        36    > > JMPZ                                                     ~16, ->39
  22    37    >   EXT_STMT
        38      > THROW                                         0          !5
  24    39    >   EXT_STMT
        40        POST_DEC                                         ~18     !0
        41        FREE                                                     ~18
  25    42        EXT_STMT
        43        ASSIGN                                                   !8, <true>
  26    44        EXT_STMT
        45      > JMPZ                                                     !2, ->55
  27    46    >   EXT_STMT
        47        INIT_DYNAMIC_CALL                                        !2
        48        EXT_FCALL_BEGIN
        49        SEND_VAR_EX                                              !5
        50        PRE_INC                                          $20     !4
        51        BRK
        52        DO_FCALL                                      0  $21
        53        EXT_FCALL_END
        54        ASSIGN                                                   !8, $21
  29    55    >   EXT_STMT
        56      > JMPZ                                                     !8, ->59
  30    57    >   EXT_STMT
        58      > JMP                                                      ->7
  32    59    >   EXT_STMT
        60      > THROW                                         0          !5
  34    61*       EXT_STMT
        62*     > RETURN                                                   null

The $tryThis closure invokes the following 13 opcodes.

function name:  {closure}
number of ops:  13
compiled vars:  !0 = $id
line     #* E I O op                           fetch          ext  return  operands
-------------------------------------------------------------------------------------
  39     0  E >   EXT_NOP
         1        BIND_STATIC                                              !0, 'id'
  40     2        EXT_STMT
         3        NEW                                              $1      :-4
         4        EXT_FCALL_BEGIN
         5        NOP
         6        FAST_CONCAT                                      ~2      'FAILED+getting+ID+%23', !0
         7        SEND_VAL_EX                                              ~2
         8        DO_FCALL                                      0
         9        EXT_FCALL_END
        10      > THROW                                         0          $1
  41    11*       EXT_STMT
        12*     > RETURN                                                   null

Finally, the $beforeRetry closure invokes the following 30 opcodes.

function name:  {closure}
number of ops:  30
compiled vars:  !0 = $e, !1 = $attempt, !2 = $id
line     #* E I O op                           fetch          ext  return  operands
-------------------------------------------------------------------------------------
         0  E >   EXT_NOP
         1        RECV                                             !0
         2        RECV                                             !1
         3        BIND_STATIC                                              !2, 'id'
  42     4        EXT_STMT
         5        INIT_METHOD_CALL                                         !0, 'getCode'
         6        EXT_FCALL_BEGIN
         7        DO_FCALL                                      0  $3
         8        EXT_FCALL_END
         9        IS_IDENTICAL                                     ~4      42, $3
        10      > JMPZ                                                     ~4, ->13
  43    11    >   EXT_STMT
        12      > RETURN                                                   <false>
  45    13    >   EXT_STMT
        14        ROPE_INIT                                     5  ~6      'Failed+getting+ID+%23'
        15        ROPE_ADD                                      1  ~6      ~6, !2
        16        ROPE_ADD                                      2  ~6      ~6, '+on+try+%23'
        17        ROPE_ADD                                      3  ~6      ~6, !1
        18        ROPE_END                                      4  ~5      ~6, '.+Retrying...%0A'
        19        ECHO                                                     ~5
  46    20        EXT_STMT
        21        INIT_FCALL                                               'sleep'
        22        EXT_FCALL_BEGIN
        23        SEND_VAL                                                 1
        24        DO_FCALL                                      0
        25        EXT_FCALL_END
  47    26        EXT_STMT
        27      > RETURN                                                   <true>
  48    28*       EXT_STMT
        29*     > RETURN                                                   null

@TODO Opcode Analysis of retry keyword

@jens1o
Copy link

jens1o commented May 25, 2017

LOVE it. PLEASE!!!11

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment