rfc:retry-keyword
Differences
This shows you the differences between two versions of the page.
Both sides previous revisionPrevious revisionNext revision | Previous revision | ||
rfc:retry-keyword [2015/11/03 21:07] – clarification sammyk | rfc:retry-keyword [2017/09/22 13:28] (current) – external edit 127.0.0.1 | ||
---|---|---|---|
Line 1: | Line 1: | ||
- | ====== PHP RFC: Retry keyword in catch blocks | + | ====== PHP RFC: Retry functionality |
- | * Version: | + | * Version: |
- | * Date: 2015-11-03 | + | * Date: 2016-06-19 |
* Author: Sammy Kaye Powers, me@sammyk.me | * Author: Sammy Kaye Powers, me@sammyk.me | ||
* Status: Under Discussion | * Status: Under Discussion | ||
Line 7: | Line 7: | ||
===== Introduction ===== | ===== Introduction ===== | ||
- | The '' | + | The '' |
- | PHP is primarily used for web apps and many times these apps need to communicate with 3rd-party services that can temporality fail with recoverable errors. Failures typically throw exceptions which can be captured in a '' | + | PHP is primarily used for web apps, and many times these apps need to communicate with third-party services |
- | This RFC proposes adding the '' | + | This RFC proposes adding |
+ | ==== Block-level implementation example ==== | ||
+ | |||
+ | The '' | ||
+ | |||
+ | * Retry the '' | ||
+ | * Execute arbitrary code before each retry (to sleep, log, check exception code, etc) | ||
+ | * Access to the number of times the try block has been executed (with '' | ||
+ | * Use the '' | ||
+ | |||
+ | <code php> | ||
+ | try { | ||
+ | somethingSketchy(); | ||
+ | } retry 3 (RecoverableException $e, $attempt) { | ||
+ | echo " | ||
+ | sleep(1); | ||
+ | } catch (RecoverableException $e) { | ||
+ | echo $e-> | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | ==== Keyword implementation example ==== | ||
+ | |||
+ | The '' | ||
+ | |||
+ | <code php> | ||
+ | try { | ||
+ | somethingSketchy(); | ||
+ | } catch (RecoverableException $e) | ||
+ | retry; // Go to top of try block | ||
+ | } | ||
+ | </ | ||
===== Proposal ===== | ===== Proposal ===== | ||
- | Currently in order to retry a block of code that failed with a recoverable error in user-land, the developer needs to write quite a bit of bootstap | + | This new keyword makes what is currently somewhat of a chore into a quick and easy activity. |
+ | |||
+ | Currently in order to retry a block of code that failed with a recoverable error in user-land, the developer needs to write quite a bit of bootstrap | ||
+ | |||
+ | **Currently** | ||
+ | |||
+ | <code php> | ||
+ | function uploadImage($path) { | ||
+ | $attempt = function() use ($path) { | ||
+ | $obj = $this-> | ||
+ | $obj-> | ||
+ | }; | ||
+ | |||
+ | try { | ||
+ | $attempt(); | ||
+ | } catch (AWS\S3\UploadException $e) | ||
+ | $attempt(); | ||
+ | } | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | **Proposed** | ||
+ | |||
+ | <code php> | ||
+ | function uploadImage($path) { | ||
+ | try { | ||
+ | $obj = $this-> | ||
+ | $obj-> | ||
+ | } catch (AWS\S3\UploadException $e) | ||
+ | retry; | ||
+ | } | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | There are currently | ||
+ | |||
+ | === Recursive Functions === | ||
<code php> | <code php> | ||
function myRetryFunction($maxTries) { | function myRetryFunction($maxTries) { | ||
- | try { | + | |
- | somethingSketchy(); | + | somethingSketchy(); |
- | } catch (RecoverableException $e) { | + | } catch (RecoverableException $e) { |
- | if ($maxTries === 0) { | + | if ($maxTries === 0) { |
- | die(' | + | die(' |
- | } | + | } |
- | myRetryFunction(--$maxTries); | + | myRetryFunction(--$maxTries); |
- | } | + | } |
} | } | ||
Line 33: | Line 100: | ||
</ | </ | ||
- | Wrapping recoverable failures in functions/closers | + | Wrapping recoverable failures in functions/closures |
+ | |||
+ | === For Loops === | ||
<code php> | <code php> | ||
$maxTries = 5; | $maxTries = 5; | ||
+ | |||
for ($x=0; $x< | for ($x=0; $x< | ||
- | try { | + | |
- | somethingSketchy(); | + | somethingSketchy(); |
- | break; | + | break; |
- | } catch (RecoverableException $e) { | + | } catch (RecoverableException $e) { |
- | die(' | + | die(' |
- | } | + | } |
} | } | ||
</ | </ | ||
- | Wrapping the recoverable code in '' | + | Wrapping the recoverable code in '' |
- | With the '' | + | === Use Goto === |
<code php> | <code php> | ||
$maxTries = 5; | $maxTries = 5; | ||
+ | retryTheThing: | ||
try { | try { | ||
- | somethingSketchy(); | + | |
} catch (RecoverableException $e) { | } catch (RecoverableException $e) { | ||
- | if (--$maxTries > 0) { | + | |
- | retry; | + | goto retryTheThing; |
- | } | + | } |
- | die(' | + | die(' |
} | } | ||
</ | </ | ||
- | There are myriad use cases in which '' | + | While this is arguably the cleanest option, it still requires the developer to define and manage a label which over several refactors might gradually move further away from the top of the '' |
+ | |||
+ | <code php> | ||
+ | $maxTries = 5; | ||
+ | |||
+ | retryTheThing: | ||
+ | |||
+ | someCodeIDoNotWantToRetry(); | ||
+ | |||
+ | try { | ||
+ | somethingSketchy(); | ||
+ | } catch (RecoverableException $e) { | ||
+ | if (--$maxTries > 0) { | ||
+ | goto retryTheThing; | ||
+ | } | ||
+ | die(' | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | ==== Use Retry ==== | ||
+ | |||
+ | Using the '' | ||
+ | |||
+ | <code php> | ||
+ | const MAX_TRIES = 5; | ||
+ | $attempt = 0; | ||
+ | try { | ||
+ | somethingSketchy(); | ||
+ | } catch (RecoverableException $e) { | ||
+ | if (++$attempt < MAX_TRIES) { | ||
+ | sleep(1); | ||
+ | // And log stuff maybe | ||
+ | retry; | ||
+ | } | ||
+ | die(' | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | Alternatively using the block-level implementation of '' | ||
+ | |||
+ | <code php> | ||
+ | try { | ||
+ | somethingSketchy(); | ||
+ | } retry 5 (RecoverableException $e, $attempt) { | ||
+ | sleep(1); | ||
+ | // And maybe log stuff when $attempt > 3 or something | ||
+ | } catch (RecoverableException $e) { | ||
+ | die(' | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | The code becomes quite a bit more readable because it removes the nested structures one's eyes has to jump through while scanning the code. | ||
+ | |||
+ | Both the '' | ||
+ | |||
+ | === Catching multiple exceptions === | ||
+ | |||
+ | As the '' | ||
+ | |||
+ | <code php> | ||
+ | try { | ||
+ | throw new RecoverableException(" | ||
+ | } retry 3 (RecoverableException | AnotherRecoverableException $e, $attempt) { | ||
+ | echo " | ||
+ | sleep(1); | ||
+ | } catch (RecoverableException $e) { | ||
+ | echo $e-> | ||
+ | } | ||
+ | |||
+ | // AnotherRecoverableException bubbles up after 3 retries | ||
+ | </ | ||
+ | |||
+ | === Bubbling up exceptions | ||
+ | |||
+ | When a '' | ||
+ | |||
+ | <code php> | ||
+ | try { | ||
+ | throw new RecoverableException(" | ||
+ | } retry 10 (RecoverableException $e, $attempt) { | ||
+ | echo " | ||
+ | } | ||
+ | |||
+ | // After 10 times RecoverableException is still thrown & uncaught | ||
+ | </ | ||
+ | |||
+ | === Implicit infinite retries === | ||
+ | |||
+ | The number of retries can be defined with an int literal or a constant. If omitted, '' | ||
+ | |||
+ | <code php> | ||
+ | try { | ||
+ | throw new RecoverableException(" | ||
+ | } retry (RecoverableException $e, $attempt) { | ||
+ | echo " | ||
+ | } catch (RecoverableException $e) { | ||
+ | echo $e-> | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | Alternatively the '' | ||
+ | |||
+ | <code php> | ||
+ | try { | ||
+ | throw new RecoverableException(" | ||
+ | } retry INF (RecoverableException $e, $attempt) { | ||
+ | echo " | ||
+ | } catch (RecoverableException $e) { | ||
+ | echo $e-> | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | === Breaking out of retry === | ||
+ | |||
+ | It is sometimes necessary to have some logic that would abort any more retry attempts, like in the case of retrying forever. That can be done using the '' | ||
+ | |||
+ | <code php> | ||
+ | try { | ||
+ | throw new RecoverableException(" | ||
+ | } retry INF (RecoverableException $e, $attempt) { | ||
+ | if (42 === $e-> | ||
+ | break; | ||
+ | } | ||
+ | echo " | ||
+ | } catch (RecoverableException $e) { | ||
+ | echo $e-> | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | === A full example === | ||
+ | |||
+ | Below is a full example that illustrates the full potential of the '' | ||
+ | |||
+ | <code php> | ||
+ | class RecoverableException extends Exception {} | ||
+ | class AnotherRecoverableException extends Exception {} | ||
+ | class NonRecoverableException extends Exception {} | ||
+ | |||
+ | $id = 42; | ||
+ | try { | ||
+ | throw new RecoverableException(" | ||
+ | } retry 3 (RecoverableException | AnotherRecoverableException $e, $attempt) { | ||
+ | if (42 === $e-> | ||
+ | break; | ||
+ | } | ||
+ | echo " | ||
+ | sleep(1); | ||
+ | } catch (RecoverableException | AnotherRecoverableException $e) { | ||
+ | echo $e-> | ||
+ | } catch (NonRecoverableException $e) { | ||
+ | echo $e-> | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | In order to illustrate just how much boilerplate the '' | ||
+ | |||
+ | <code php> | ||
+ | class RecoverableException extends Exception {} | ||
+ | class AnotherRecoverableException extends Exception {} | ||
+ | class NonRecoverableException extends Exception {} | ||
+ | |||
+ | function retry(int $retryCount, | ||
+ | { | ||
+ | $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, | ||
+ | } | ||
+ | if ($shouldRetry) { | ||
+ | goto tryCode; | ||
+ | } | ||
+ | throw $e; | ||
+ | } | ||
+ | } | ||
+ | |||
+ | $id = 42; | ||
+ | try { | ||
+ | $result = retry(3, function () use ($id) { | ||
+ | throw new AnotherRecoverableException(" | ||
+ | }, function ($e, $attempt) use ($id) { | ||
+ | if (42 === $e-> | ||
+ | return false; | ||
+ | } | ||
+ | echo " | ||
+ | sleep(1); | ||
+ | return true; | ||
+ | }, [RecoverableException:: | ||
+ | } catch (RecoverableException | AnotherRecoverableException $e) { | ||
+ | echo $e-> | ||
+ | } catch (NonRecoverableException $e) { | ||
+ | echo $e-> | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | When comparing the readability of those two code snippets one can see the amount of cognitive overhead that the '' | ||
+ | |||
+ | It's important not to confuse the use of this feature, with developers just not using enough methods. More and more and more methods is quite popular with the OOP mindset, but 1) more methods are not in fact always a superior alternative to retry, and 2) PHP is not just an OOP language. | ||
+ | |||
+ | === Retry in the wild === | ||
+ | |||
+ | A retry feature is also not unheard of in other languages. | ||
+ | |||
+ | * Ruby has the '' | ||
+ | * Spring does [[http:// | ||
+ | * Python third party library offers [[https:// | ||
+ | |||
+ | Other languages seem to lack retry logic directly, but Google is full of people trying to work out how to do it with a whole range of complex approaches. We'd make a lot of lives easier if the keyword existed, instead of forcing people to loop and break and count and recurse and... goto. | ||
+ | |||
+ | ===== Use Cases ===== | ||
+ | |||
+ | There are myriad use cases in which '' | ||
+ | |||
+ | A) A popular | ||
+ | |||
+ | B) Attempting to make an OAuth 2.0-based API request, getting a 401 due to an expired token, refreshing that token then retrying to original request. | ||
+ | |||
+ | C) Temporarily locked I/O. | ||
+ | |||
+ | D) Find/Create logic. This is used in the Rails world, when their ActiveRecord " | ||
+ | |||
+ | Ideally apps would attempt to retry recoverable failures | ||
===== Backward Incompatible Changes ===== | ===== Backward Incompatible Changes ===== | ||
- | This RFC would not introduce | + | This RFC would not introduce |
===== Proposed PHP Version ===== | ===== Proposed PHP Version ===== | ||
- | Next PHP 7.1. | + | Next PHP 7.3. |
===== Proposed Voting Choices ===== | ===== Proposed Voting Choices ===== | ||
Requires a 2/3 majority. | Requires a 2/3 majority. | ||
+ | |||
+ | ==== Add block-level retry ==== | ||
+ | |||
+ | <code php> | ||
+ | try { | ||
+ | somethingSketchy(); | ||
+ | } retry 3 (RecoverableException $e, $attempt) { | ||
+ | sleep(1); | ||
+ | } catch (RecoverableException $e) { | ||
+ | echo $e-> | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | <doodle title=" | ||
+ | * Yes | ||
+ | * No | ||
+ | </ | ||
+ | |||
+ | ==== Add retry keyword ==== | ||
+ | |||
+ | <code php> | ||
+ | try { | ||
+ | somethingSketchy(); | ||
+ | } catch (RecoverableException $e) | ||
+ | retry; | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | <doodle title=" | ||
+ | * Yes | ||
+ | * No | ||
+ | </ | ||
===== Patches and Tests ===== | ===== Patches and Tests ===== | ||
- | There is no patch yet, but if approved, Sammy Kaye Powers will submit one. | + | There are a few slightly outdated WIP implementations of '' |
+ | |||
+ | * [[https:// | ||
+ | * [[https:// | ||
===== Credits ===== | ===== Credits ===== | ||
[[https:// | [[https:// | ||
- | |||
rfc/retry-keyword.1446584839.txt.gz · Last modified: 2017/09/22 13:28 (external edit)