Skip to content

Commit 3dfc1b3

Browse files
committed
Rewrote using Fibers instead of coroutines.
1 parent 2f3ea33 commit 3dfc1b3

File tree

4 files changed

+90
-112
lines changed

4 files changed

+90
-112
lines changed

composer.json

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,12 @@
1313
}
1414
],
1515
"require": {
16-
"php": "^7.2|^8"
16+
"php": "^8.1"
1717
},
1818
"require-dev": {
19+
"amphp/amp": "^3-beta.9",
1920
"phpunit/phpunit": "^8.5|^9",
20-
"amphp/amp": "^2.2"
21+
"revolt/event-loop": "^0.2"
2122
},
2223
"autoload": {
2324
"files": [
@@ -34,5 +35,8 @@
3435
},
3536
"scripts": {
3637
"test": "phpunit -c test"
38+
},
39+
"config": {
40+
"sort-packages": true
3741
}
3842
}

src/retry.php

Lines changed: 29 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,7 @@
33

44
namespace ScriptFUSION\Retry;
55

6-
use Amp\Coroutine;
7-
use Amp\Promise;
8-
use Amp\Success;
6+
use Amp\Future;
97

108
/**
119
* Tries the specified operation up to the specified number of times. If specified, the exception handler will be
@@ -18,72 +16,44 @@
1816
*
1917
* @return mixed Result of running the operation if tries is greater than zero, otherwise null.
2018
*
19+
* @throws FailingTooHardException The maximum number of attempts was reached.
20+
* @throws \UnexpectedValueException The operation returned an unsupported type.
2121
*/
22-
function retry(int $tries, callable $operation, callable $onError = null)
22+
function retry(int $tries, callable $operation, callable $onError = null): mixed
2323
{
24-
$generator = (static function () use ($tries, $operation, $onError): \Generator {
25-
// Nothing to do if tries less than or equal to zero.
26-
if ($tries <= $attempts = 0) {
27-
return;
28-
}
24+
// Nothing to do if tries less than or equal to zero.
25+
if ($tries <= $attempts = 0) {
26+
return null;
27+
}
2928

30-
try {
31-
beginning:
29+
try {
30+
beginning:
3231

33-
if (($result = $operation()) instanceof Promise) {
34-
// Wait for promise to resolve.
35-
$result = yield $result;
36-
}
37-
} catch (\Exception $exception) {
38-
if ($tries === ++$attempts) {
39-
throw new FailingTooHardException($attempts, $exception);
40-
}
41-
42-
if ($onError) {
43-
if (($result = $onError($exception, $attempts, $tries)) instanceof Promise) {
44-
$result = yield $result;
45-
}
32+
if (($result = $operation()) instanceof Future) {
33+
// Wait for Future to complete.
34+
$result = $result->await();
35+
}
36+
} catch (\Exception $exception) {
37+
if ($tries === ++$attempts) {
38+
throw new FailingTooHardException($attempts, $exception);
39+
}
4640

47-
if ($result === false) {
48-
return;
49-
}
41+
if ($onError) {
42+
if (($result = $onError($exception, $attempts, $tries)) instanceof Future) {
43+
$result = $result->await();
5044
}
5145

52-
goto beginning;
53-
}
54-
55-
if ($result instanceof \Generator) {
56-
throw new \UnexpectedValueException('Cannot retry a Generator. You probably meant something else.');
46+
if ($result === false) {
47+
return null;
48+
}
5749
}
5850

59-
return $result;
60-
})();
61-
62-
// Normal code path: generator runs without yielding.
63-
if (!$generator->valid()) {
64-
return $generator->getReturn();
51+
goto beginning;
6552
}
6653

67-
// Async code path: generator yields promises.
68-
return $generator;
69-
}
70-
71-
/**
72-
* Tries the specified operation up to the specified number of times. If specified, the exception handler will be
73-
* called immediately before retrying the operation. If the error handler returns false, the operation will not be
74-
* retried.
75-
*
76-
* @param int $tries Number of times.
77-
* @param callable $operation Operation.
78-
* @param callable|null $onError Optional. Exception handler.
79-
*
80-
* @return Promise Promise that returns the result of running the operation if tries is greater than zero, otherwise
81-
* a promise that yields null.
82-
*
83-
*/
84-
function retryAsync(int $tries, callable $operation, callable $onError = null): Promise
85-
{
86-
$generator = retry($tries, $operation, $onError);
54+
if ($result instanceof \Generator) {
55+
throw new \UnexpectedValueException('Cannot retry a Generator. You probably meant something else.');
56+
}
8757

88-
return $generator !== null ? new Coroutine($generator) : new Success(null);
58+
return $result;
8959
}

test/RetryAsyncTest.php

Lines changed: 53 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,46 @@
11
<?php
2+
declare(strict_types=1);
3+
24
namespace ScriptFUSIONTest\Retry;
35

4-
use Amp\Delayed;
5-
use Amp\Promise;
6-
use Amp\Success;
6+
use Amp\Future;
77
use PHPUnit\Framework\TestCase;
88
use ScriptFUSION\Retry\FailingTooHardException;
9-
use function Amp\Promise\wait;
10-
use function ScriptFUSION\Retry\retryAsync;
9+
use function Amp\async;
10+
use function Amp\delay;
11+
use function ScriptFUSION\Retry\retry;
1112

1213
final class RetryAsyncTest extends TestCase
1314
{
1415
/**
15-
* Tests that a successful promise is returned without retrying.
16+
* Tests that a successful Future is returned without retrying.
1617
*/
17-
public function testWithoutFailingAsync()
18+
public function testWithoutFailingAsync(): void
1819
{
1920
$invocations = 0;
2021

21-
$value = wait(
22-
retryAsync($tries = 1, static function () use (&$invocations) {
22+
$value =
23+
retry($tries = 1, static function () use (&$invocations) {
2324
++$invocations;
2425

25-
return new Delayed(0, 'foo');
26+
return self::delayAndReturn(0, 'foo');
2627
})
27-
);
28+
;
2829

2930
self::assertSame($tries, $invocations);
3031
self::assertSame('foo', $value);
3132
}
3233

3334
/**
34-
* Tests that a failed promise is retried.
35+
* Tests that a failed Future is retried.
3536
*/
36-
public function testFailingOnceAsync()
37+
public function testFailingOnceAsync(): void
3738
{
3839
$invocations = 0;
3940
$failed = false;
4041

41-
$value = wait(
42-
retryAsync($tries = 2, static function () use (&$invocations, &$failed) {
42+
$value =
43+
retry($tries = 2, static function () use (&$invocations, &$failed) {
4344
++$invocations;
4445

4546
if (!$failed) {
@@ -48,9 +49,9 @@ public function testFailingOnceAsync()
4849
throw new \RuntimeException;
4950
}
5051

51-
return new Delayed(0, 'foo');
52+
return self::delayAndReturn(0, 'foo');
5253
})
53-
);
54+
;
5455

5556
self::assertTrue($failed);
5657
self::assertSame($tries, $invocations);
@@ -60,17 +61,17 @@ public function testFailingOnceAsync()
6061
/**
6162
* Tests that trying zero times yields null.
6263
*/
63-
public function testZeroTriesAsync()
64+
public function testZeroTriesAsync(): void
6465
{
6566
$invocations = 0;
6667

67-
$value = wait(
68-
retryAsync($tries = 0, static function () use (&$invocations) {
68+
$value =
69+
retry($tries = 0, static function () use (&$invocations) {
6970
++$invocations;
7071

71-
return new Delayed(0, 'foo');
72+
return self::delayAndReturn(0, 'foo');
7273
})
73-
);
74+
;
7475

7576
self::assertSame($tries, $invocations);
7677
self::assertNull($value);
@@ -79,13 +80,13 @@ public function testZeroTriesAsync()
7980
/**
8081
* Tests that reaching maximum tries throws FailingTooHardException.
8182
*/
82-
public function testFailingTooHardAsync()
83+
public function testFailingTooHardAsync(): void
8384
{
8485
$invocations = 0;
8586
$outerException = $innerException = null;
8687

8788
try {
88-
retryAsync($tries = 3, static function () use (&$invocations, &$innerException) {
89+
retry($tries = 3, static function () use (&$invocations, &$innerException) {
8990
++$invocations;
9091

9192
throw $innerException = new \RuntimeException;
@@ -101,13 +102,13 @@ public function testFailingTooHardAsync()
101102
/**
102103
* Tests that the error callback is called before each retry.
103104
*/
104-
public function testErrorCallbackAsync()
105+
public function testErrorCallbackAsync(): void
105106
{
106107
$invocations = $errors = 0;
107108
$outerException = $innerException = null;
108109

109110
try {
110-
retryAsync($tries = 2, static function () use (&$invocations, &$innerException) {
111+
retry($tries = 2, static function () use (&$invocations, &$innerException) {
111112
++$invocations;
112113

113114
throw $innerException = new \RuntimeException;
@@ -127,76 +128,77 @@ public function testErrorCallbackAsync()
127128
}
128129

129130
/**
130-
* Tests that an error callback that returns a promise has its promise resolved.
131+
* Tests that an error callback that returns a Future has its Future resolved.
131132
*/
132-
public function testPromiseErrorCallback()
133+
public function testFutureErrorCallback(): void
133134
{
134-
$delay = 250; // Quarter of a second.
135+
$delay = .25; // Quarter of a second.
135136
$start = microtime(true);
136137

137138
try {
138-
wait(
139-
retryAsync($tries = 3, static function () {
140-
throw new \DomainException;
141-
}, static function () use ($delay): Promise {
142-
return new Delayed($delay);
143-
})
144-
);
139+
retry($tries = 3, static function () {
140+
throw new \DomainException;
141+
}, fn () => self::delayAndReturn($delay));
145142
} catch (FailingTooHardException $outerException) {
146143
self::assertInstanceOf(\DomainException::class, $outerException->getPrevious());
147144
}
148145

149146
self::assertTrue(isset($outerException));
150-
self::assertGreaterThan($start + $delay * ($tries - 1) / 1000, microtime(true));
147+
self::assertGreaterThan($start + $delay * ($tries - 1), microtime(true));
151148
}
152149

153150
/**
154151
* Tests that when error handler that returns false, it aborts retrying.
155152
*/
156-
public function testErrorCallbackHaltAsync()
153+
public function testErrorCallbackHaltAsync(): void
157154
{
158155
$invocations = 0;
159156

160-
retryAsync(2, static function () use (&$invocations) {
157+
retry(2, static function () use (&$invocations): never {
161158
++$invocations;
162159

163160
throw new \RuntimeException;
164-
}, static function () {
165-
return false;
166-
});
161+
}, fn () => false);
167162

168163
self::assertSame(1, $invocations);
169164
}
170165

171166
/**
172-
* Tests that when an error handler returns a promise that false, it aborts retrying.
167+
* Tests that when an error handler returns a Future that false, it aborts retrying.
173168
*/
174-
public function testPromiseErrorCallbackHaltAsync()
169+
public function testFutureErrorCallbackHaltAsync(): void
175170
{
176171
$invocations = 0;
177172

178-
retryAsync(2, static function () use (&$invocations) {
173+
retry(2, static function () use (&$invocations): never {
179174
++$invocations;
180175

181176
throw new \RuntimeException;
182-
}, static function (): Promise {
183-
return new Success(false);
184-
});
177+
}, fn () => Future::complete(false));
185178

186179
self::assertSame(1, $invocations);
187180
}
188181

189182
/**
190183
* Tests that the exception handler can throw an exception that will not be caught.
191184
*/
192-
public function testErrorCallbackCanThrow()
185+
public function testErrorCallbackCanThrow(): void
193186
{
194187
$this->expectException(\LogicException::class);
195188

196-
retryAsync(2, static function () {
189+
retry(2, static function (): never {
197190
throw new \RuntimeException;
198-
}, static function () {
191+
}, static function (): never {
199192
throw new \LogicException;
200193
});
201194
}
195+
196+
private static function delayAndReturn(float $delay, string $return = null): Future
197+
{
198+
return async(static function () use ($delay, $return): ?string {
199+
delay($delay);
200+
201+
return $return;
202+
});
203+
}
202204
}

test/RetryTest.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
<?php
2+
declare(strict_types=1);
3+
24
namespace ScriptFUSIONTest\Retry;
35

46
use PHPUnit\Framework\TestCase;

0 commit comments

Comments
 (0)