Skip to content

Commit ef3f586

Browse files
committed
Merge pull request #537: exception propagation from yielded generator
(cherry picked from commit 8059ef7)
1 parent 5c86db5 commit ef3f586

File tree

6 files changed

+379
-46
lines changed

6 files changed

+379
-46
lines changed

src/Internal/Workflow/Process/DeferredGenerator.php

Lines changed: 54 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,6 @@ final class DeferredGenerator implements \Iterator
1818
{
1919
private bool $started = false;
2020
private bool $finished = false;
21-
private mixed $key = null;
22-
private mixed $value = null;
23-
private mixed $result = null;
2421
private \Generator $generator;
2522

2623
/** @var array<\Closure(\Throwable): mixed> */
@@ -50,7 +47,6 @@ public static function fromGenerator(\Generator $generator): self
5047
$self = new self();
5148
$self->generator = $generator;
5249
$self->started = true;
53-
$self->fill();
5450
return $self;
5551
}
5652

@@ -67,7 +63,6 @@ public function throw(\Throwable $exception): void
6763
);
6864
try {
6965
$this->generator->throw($exception);
70-
$this->fill();
7166
} catch (\Throwable $e) {
7267
$this->handleException($e);
7368
}
@@ -80,15 +75,12 @@ public function throw(\Throwable $exception): void
8075
*/
8176
public function send(mixed $value): mixed
8277
{
83-
$this->started or throw new \LogicException('Cannot send value to a generator that was not started.');
78+
$this->start();
8479
$this->finished and throw new \LogicException('Cannot send value to a generator that was already finished.');
8580
try {
86-
$result = $this->generator->send($value);
87-
$this->fill();
88-
return $result;
81+
return $this->generator->send($value);
8982
} catch (\Throwable $e) {
9083
$this->handleException($e);
91-
return null;
9284
}
9385
}
9486

@@ -97,8 +89,12 @@ public function send(mixed $value): mixed
9789
*/
9890
public function getReturn(): mixed
9991
{
100-
$this->finished or throw new \LogicException('Cannot get return value of a generator that was not finished.');
101-
return $this->result;
92+
// $this->start();
93+
try {
94+
return $this->generator->getReturn();
95+
} catch (\Throwable $e) {
96+
$this->handleException($e);
97+
}
10298
}
10399

104100
/**
@@ -107,7 +103,11 @@ public function getReturn(): mixed
107103
public function current(): mixed
108104
{
109105
$this->start();
110-
return $this->value;
106+
try {
107+
return $this->generator->current();
108+
} catch (\Throwable $e) {
109+
$this->handleException($e);
110+
}
111111
}
112112

113113
/**
@@ -116,7 +116,11 @@ public function current(): mixed
116116
public function key(): mixed
117117
{
118118
$this->start();
119-
return $this->key;
119+
try {
120+
return $this->generator->key();
121+
} catch (\Throwable $e) {
122+
$this->handleException($e);
123+
}
120124
}
121125

122126
/**
@@ -131,7 +135,6 @@ public function next(): void
131135

132136
try {
133137
$this->generator->next();
134-
$this->fill();
135138
} catch (\Throwable $e) {
136139
$this->handleException($e);
137140
}
@@ -145,12 +148,16 @@ public function next(): void
145148
public function valid(): bool
146149
{
147150
$this->start();
148-
return !$this->finished;
151+
try {
152+
return $this->generator->valid();
153+
} catch (\Throwable $e) {
154+
$this->handleException($e);
155+
}
149156
}
150157

151158
public function rewind(): void
152159
{
153-
$this->started and throw new \LogicException('Cannot rewind a generator that was already run.');
160+
$this->generator->rewind();
154161
}
155162

156163
/**
@@ -164,6 +171,20 @@ public function catch(callable $handler): self
164171
return $this;
165172
}
166173

174+
private static function getDummyGenerator(): \Generator
175+
{
176+
static $generator;
177+
178+
if ($generator === null) {
179+
$generator = (static function (): \Generator {
180+
yield;
181+
})();
182+
$generator->current();
183+
}
184+
185+
return $generator;
186+
}
187+
167188
private function start(): void
168189
{
169190
if ($this->started) {
@@ -176,33 +197,36 @@ private function start(): void
176197

177198
if ($result instanceof \Generator) {
178199
$this->generator = $result;
179-
$this->fill();
180200
return;
181201
}
182202

183-
$this->result = $result;
203+
/** @psalm-suppress all */
204+
$this->generator = (static function (mixed $result): \Generator {
205+
return $result;
206+
yield;
207+
})($result);
184208
$this->finished = true;
185209
} catch (\Throwable $e) {
210+
$this->generator = self::getDummyGenerator();
186211
$this->handleException($e);
187212
} finally {
188213
unset($this->handler, $this->values);
189214
}
190215
}
191216

192-
private function fill(): void
217+
private function handleException(\Throwable $e): never
193218
{
194-
$this->key = $this->generator->key();
195-
$this->value = $this->generator->current();
196-
$this->finished = !$this->generator->valid() and $this->result = $this->generator->getReturn();
197-
}
198-
199-
private function handleException(\Throwable $e): void
200-
{
201-
$this->key = null;
202-
$this->value = null;
219+
$this->finished and throw $e;
203220
$this->finished = true;
204221
foreach ($this->catchers as $catch) {
205-
$catch($e);
222+
try {
223+
$catch($e);
224+
} catch (\Throwable) {
225+
// Do nothing.
226+
}
206227
}
228+
229+
$this->catchers = [];
230+
throw $e;
207231
}
208232
}

src/Internal/Workflow/Process/Scope.php

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,8 @@ public function getContext(): WorkflowContext
131131
public function start(\Closure $handler, ?ValuesInterface $values, bool $deferred): void
132132
{
133133
// Create a coroutine generator
134-
$this->coroutine = $this->call($handler, $values ?? EncodedValues::empty());
134+
$this->coroutine = DeferredGenerator::fromHandler($handler, $values ?? EncodedValues::empty())
135+
->catch($this->onException(...));
135136

136137
$deferred
137138
? $this->services->loop->once($this->layer, $this->next(...))
@@ -330,14 +331,6 @@ function () use ($cancelID): void {
330331
return $scope;
331332
}
332333

333-
/**
334-
* @param \Closure(ValuesInterface): mixed $handler
335-
*/
336-
protected function call(\Closure $handler, ValuesInterface $values): DeferredGenerator
337-
{
338-
return DeferredGenerator::fromHandler($handler, $values)->catch($this->onException(...));
339-
}
340-
341334
/**
342335
* Call a Signal or Update method. In this case deserialization errors are skipped.
343336
*
@@ -397,7 +390,11 @@ protected function next(): void
397390
$this->context->resolveConditions();
398391

399392
if (!$this->coroutine->valid()) {
400-
$this->onResult($this->coroutine->getReturn());
393+
try {
394+
$this->onResult($this->coroutine->getReturn());
395+
} catch (\Throwable) {
396+
$this->onResult(null);
397+
}
401398

402399
return;
403400
}
@@ -428,7 +425,11 @@ protected function next(): void
428425
break;
429426

430427
default:
431-
$this->coroutine->send($current);
428+
try {
429+
$this->coroutine->send($current);
430+
} catch (\Throwable) {
431+
// Ignore
432+
}
432433
goto begin;
433434
}
434435
}

tests/Acceptance/Harness/ChildWorkflow/ThrowOnExecuteTest.php

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,6 @@ public function run()
5151
{
5252
return yield Workflow::newChildWorkflowStub(
5353
ChildWorkflow::class,
54-
// TODO: remove after https://github.com/temporalio/sdk-php/issues/451 is fixed
55-
Workflow\ChildWorkflowOptions::new()->withTaskQueue(Workflow::getInfo()->taskQueue),
5654
)->run();
5755
}
5856
}
@@ -63,6 +61,7 @@ class ChildWorkflow
6361
#[WorkflowMethod('Harness_ChildWorkflow_ThrowsOnExecute_Child')]
6462
public function run()
6563
{
64+
yield 1;
6665
throw new ApplicationFailure('Test message', 'TestError', true, EncodedValues::fromValues([['foo' => 'bar']]));
6766
}
6867
}

tests/Fixtures/src/Workflow/GeneratorWorkflow.php

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
namespace Temporal\Tests\Workflow;
1313

1414
use Temporal\Activity\ActivityOptions;
15+
use Temporal\Common\RetryOptions;
1516
use Temporal\Internal\Workflow\ActivityProxy;
1617
use Temporal\Tests\Activity\SimpleActivity;
1718
use Temporal\Workflow;
@@ -27,7 +28,9 @@ public function handler(
2728
// typed stub
2829
$simple = Workflow::newActivityStub(
2930
SimpleActivity::class,
30-
ActivityOptions::new()->withStartToCloseTimeout(5)
31+
ActivityOptions::new()->withStartToCloseTimeout(5)->withRetryOptions(
32+
RetryOptions::new()->withMaximumAttempts(1)
33+
)
3134
);
3235

3336
return [
@@ -38,11 +41,16 @@ public function handler(
3841

3942
/**
4043
* @param ActivityProxy|SimpleActivity $simple
41-
* @param string $input
42-
* @return \Generator
4344
*/
4445
private function doSomething(ActivityProxy $simple, string $input): \Generator
4546
{
47+
$input === 'error' and throw new \Exception('error from generator');
48+
49+
if ($input === 'failure') {
50+
yield $simple->fail();
51+
throw new \Exception('Unreachable statement');
52+
}
53+
4654
$result = [];
4755
$result[] = yield $simple->echo($input);
4856
$result[] = yield $simple->echo($input);

tests/Functional/Client/TypedStubTestCase.php

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@
1111

1212
namespace Temporal\Tests\Functional\Client;
1313

14+
use Temporal\Exception\Client\WorkflowFailedException;
1415
use Temporal\Exception\Client\WorkflowQueryException;
16+
use Temporal\Exception\Failure\ActivityFailure;
1517
use Temporal\Exception\InvalidArgumentException;
1618
use Temporal\Tests\DTO\Message;
1719
use Temporal\Tests\DTO\User;
@@ -151,6 +153,34 @@ public function testGeneratorCoroutines()
151153
);
152154
}
153155

156+
public function testGeneratorErrorCoroutines()
157+
{
158+
$client = $this->createClient();
159+
$simple = $client->newWorkflowStub(GeneratorWorkflow::class);
160+
161+
try {
162+
$simple->handler('error');
163+
$this->fail('Expected exception to be thrown');
164+
} catch (WorkflowFailedException $e) {
165+
$this->assertStringContainsString('error from generator', $e->getPrevious()->getMessage());
166+
}
167+
}
168+
169+
public function testGeneratorErrorInNestedActionCoroutines()
170+
{
171+
$client = $this->createClient();
172+
$simple = $client->newWorkflowStub(GeneratorWorkflow::class);
173+
174+
try {
175+
$simple->handler('failure');
176+
$this->fail('Expected exception to be thrown');
177+
} catch (WorkflowFailedException $e) {
178+
$this->assertStringNotContainsString('Unreachable statement', $e->getPrevious()->getMessage());
179+
$this->assertInstanceOf(ActivityFailure::class, $e->getPrevious());
180+
$this->assertStringContainsString('failed activity', $e->getPrevious()->getPrevious()->getMessage());
181+
}
182+
}
183+
154184
/**
155185
* @group skip-on-test-server
156186
*/

0 commit comments

Comments
 (0)