Skip to content

Commit 8a42213

Browse files
authored
Merge pull request #620: Activity pause
2 parents fadb187 + 95d82a7 commit 8a42213

File tree

12 files changed

+244
-82
lines changed

12 files changed

+244
-82
lines changed

psalm-baseline.xml

Lines changed: 2 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<?xml version="1.0" encoding="UTF-8"?>
2-
<files psalm-version="6.12.0@cf420941d061a57050b6c468ef2c778faf40aee2">
2+
<files psalm-version="6.12.1@e71404b0465be25cf7f8a631b298c01c5ddd864f">
33
<file src="src/Activity.php">
44
<ImplicitToStringCast>
55
<code><![CDATA[$type]]></code>
@@ -352,33 +352,11 @@
352352
</file>
353353
<file src="src/Exception/Client/ActivityCompletionException.php">
354354
<PossiblyInvalidArgument>
355-
<code><![CDATA[$e === null ? 0 : $e->getCode()]]></code>
356-
<code><![CDATA[$e->getCode()]]></code>
357-
<code><![CDATA[$e->getCode()]]></code>
355+
<code><![CDATA[$code]]></code>
358356
</PossiblyInvalidArgument>
359357
<PossiblyNullReference>
360358
<code><![CDATA[getID]]></code>
361359
</PossiblyNullReference>
362-
<UnsafeInstantiation>
363-
<code><![CDATA[new static(
364-
$e->getMessage(),
365-
$e->getCode(),
366-
$e,
367-
)]]></code>
368-
<code><![CDATA[new static(
369-
self::buildMessage(
370-
[
371-
'workflowId' => $info->workflowExecution->getID(),
372-
'runId' => $info->workflowExecution->getRunID(),
373-
'activityId' => $info->id,
374-
'activityType' => $info->type->name,
375-
],
376-
),
377-
$e === null ? 0 : $e->getCode(),
378-
$e,
379-
)]]></code>
380-
<code><![CDATA[new static('', $e->getCode(), $e)]]></code>
381-
</UnsafeInstantiation>
382360
</file>
383361
<file src="src/Exception/Client/MultyOperation/OperationStatus.php">
384362
<InvalidArgument>

src/Activity.php

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
namespace Temporal;
1313

14+
use Temporal\Activity\ActivityCancellationDetails;
1415
use Temporal\Activity\ActivityContextInterface;
1516
use Temporal\Activity\ActivityInfo;
1617
use Temporal\DataConverter\Type;
@@ -83,15 +84,27 @@ public static function hasHeartbeatDetails(): bool
8384
* This method retrieves the payload that was passed into the last call of the {@see Activity::heartbeat()} method.
8485
*
8586
* @param Type|string|\ReflectionType|\ReflectionClass|null $type
86-
* @return mixed
8787
* @throws OutOfContextException in the absence of the activity execution context.
8888
*/
89-
public static function getHeartbeatDetails($type = null)
89+
public static function getHeartbeatDetails($type = null): mixed
9090
{
9191
/** @var ActivityContextInterface $context */
9292
$context = self::getCurrentContext();
9393

94-
return $context->getHeartbeatDetails($type);
94+
return $context->getLastHeartbeatDetails($type);
95+
}
96+
97+
/**
98+
* Cancellation details of the current activity, if any.
99+
*
100+
* Once set, cancellation details do not change.
101+
*/
102+
public static function getCancellationDetails(): ?ActivityCancellationDetails
103+
{
104+
/** @var ActivityContextInterface $context */
105+
$context = self::getCurrentContext();
106+
107+
return $context->getCancellationDetails();
95108
}
96109

97110
/**
@@ -131,7 +144,7 @@ public static function doNotCompleteOnReturn(): void
131144
* ```
132145
*
133146
* @param mixed $details In case of activity timeout details are returned
134-
* as a field of the exception thrown.
147+
* as a field of the exception thrown.
135148
*
136149
* @throws OutOfContextException in the absence of the activity execution context.
137150
*/
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Temporal\Activity;
6+
7+
/**
8+
* Provides the reasons for the activity's cancellation.
9+
*/
10+
final class ActivityCancellationDetails
11+
{
12+
/**
13+
* @internal
14+
*/
15+
public function __construct(
16+
public readonly bool $notFound = false,
17+
public readonly bool $cancelRequested = false,
18+
public readonly bool $paused = false,
19+
public readonly bool $timedOut = false,
20+
public readonly bool $workerShutdown = false,
21+
) {}
22+
}

src/Activity/ActivityContextInterface.php

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@
1414
use Temporal\Activity;
1515
use Temporal\DataConverter\Type;
1616
use Temporal\DataConverter\ValuesInterface;
17+
use Temporal\Exception\Client\ActivityCanceledException;
18+
use Temporal\Exception\Client\ActivityCompletionException;
19+
use Temporal\Exception\Client\ActivityPausedException;
1720

1821
interface ActivityContextInterface
1922
{
@@ -44,9 +47,8 @@ public function hasHeartbeatDetails(): bool;
4447
* @see Activity::getHeartbeatDetails()
4548
*
4649
* @param Type|string $type
47-
* @return mixed
4850
*/
49-
public function getHeartbeatDetails($type = null);
51+
public function getLastHeartbeatDetails($type = null): mixed;
5052

5153
/**
5254
* Marks the activity as incomplete for asynchronous completion.
@@ -58,11 +60,23 @@ public function doNotCompleteOnReturn(): void;
5860
/**
5961
* Use to notify workflow that activity execution is alive.
6062
*
63+
* @throws ActivityCompletionException
64+
* @throws ActivityCanceledException
65+
* @throws ActivityPausedException
66+
*
6167
* @see Activity::heartbeat()
6268
*
63-
* @param mixed $details
6469
*/
65-
public function heartbeat($details): void;
70+
public function heartbeat(mixed $details): void;
71+
72+
/**
73+
* Cancellation details of the current activity, if any.
74+
*
75+
* Once set, cancellation details do not change.
76+
*
77+
* @see Activity::getCancellationDetails()
78+
*/
79+
public function getCancellationDetails(): ?ActivityCancellationDetails;
6680

6781
/**
6882
* Get the currently running activity instance.

src/Exception/Client/ActivityCompletionException.php

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,15 @@ class ActivityCompletionException extends TemporalException
2121
private ?string $activityType = null;
2222
private ?string $activityId = null;
2323

24+
final public function __construct(string $message = "", string|int $code = 0, ?\Throwable $previous = null)
25+
{
26+
parent::__construct($message, $code, $previous);
27+
}
28+
2429
/**
25-
* @return static
30+
* @internal
2631
*/
27-
public static function fromPrevious(\Throwable $e): self
32+
public static function fromPrevious(\Throwable $e): static
2833
{
2934
return new static(
3035
$e->getMessage(),
@@ -34,9 +39,9 @@ public static function fromPrevious(\Throwable $e): self
3439
}
3540

3641
/**
37-
* @return static
42+
* @internal
3843
*/
39-
public static function fromPreviousWithActivityId(string $activityId, \Throwable $e): self
44+
public static function fromPreviousWithActivityId(string $activityId, \Throwable $e): static
4045
{
4146
$e = new static('', $e->getCode(), $e);
4247
$e->activityId = $activityId;
@@ -45,9 +50,9 @@ public static function fromPreviousWithActivityId(string $activityId, \Throwable
4550
}
4651

4752
/**
48-
* @return static
53+
* @internal
4954
*/
50-
public static function fromActivityInfo(ActivityInfo $info, ?\Throwable $e = null): self
55+
public static function fromActivityInfo(ActivityInfo $info, ?\Throwable $e = null): static
5156
{
5257
$e = new static(
5358
self::buildMessage(
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
/**
4+
* This file is part of Temporal package.
5+
*
6+
* For the full copyright and license information, please view the LICENSE
7+
* file that was distributed with this source code.
8+
*/
9+
10+
declare(strict_types=1);
11+
12+
namespace Temporal\Exception\Client;
13+
14+
/**
15+
* Indicates that the activity was paused by the user.
16+
*
17+
* Catching this exception directly is discouraged and catching
18+
* the parent class {@link ActivityCompletionException} is recommended instead.
19+
*/
20+
final class ActivityPausedException extends ActivityCompletionException {}

src/Internal/Activity/ActivityContext.php

Lines changed: 29 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
namespace Temporal\Internal\Activity;
1313

14+
use Temporal\Activity\ActivityCancellationDetails;
1415
use Temporal\Activity\ActivityContextInterface;
1516
use Temporal\Activity\ActivityInfo;
1617
use Temporal\DataConverter\DataConverterInterface;
@@ -19,6 +20,7 @@
1920
use Temporal\DataConverter\ValuesInterface;
2021
use Temporal\Exception\Client\ActivityCanceledException;
2122
use Temporal\Exception\Client\ActivityCompletionException;
23+
use Temporal\Exception\Client\ActivityPausedException;
2224
use Temporal\Exception\Client\ServiceClientException;
2325
use Temporal\Interceptor\HeaderInterface;
2426
use Temporal\Internal\Interceptor\HeaderCarrier;
@@ -31,26 +33,17 @@ final class ActivityContext implements ActivityContextInterface, HeaderCarrier
3133
private ActivityInfo $info;
3234

3335
private bool $doNotCompleteOnReturn = false;
34-
private RPCConnectionInterface $rpc;
35-
private DataConverterInterface $converter;
36-
private ?ValuesInterface $heartbeatDetails;
37-
private ValuesInterface $input;
38-
private HeaderInterface $header;
3936
private ?\WeakReference $instance = null;
37+
private ?ActivityCancellationDetails $cancellationDetails = null;
4038

4139
public function __construct(
42-
RPCConnectionInterface $rpc,
43-
DataConverterInterface $converter,
44-
ValuesInterface $input,
45-
HeaderInterface $header,
46-
?ValuesInterface $lastHeartbeatDetails = null,
40+
private readonly RPCConnectionInterface $rpc,
41+
private readonly DataConverterInterface $converter,
42+
private ValuesInterface $input,
43+
private HeaderInterface $header,
44+
private readonly ?ValuesInterface $lastHeartbeatDetails = null,
4745
) {
4846
$this->info = new ActivityInfo();
49-
$this->rpc = $rpc;
50-
$this->converter = $converter;
51-
$this->heartbeatDetails = $lastHeartbeatDetails;
52-
$this->input = $input;
53-
$this->header = $header;
5447
}
5548

5649
public function getInfo(): ActivityInfo
@@ -91,20 +84,19 @@ public function getDataConverter(): DataConverterInterface
9184

9285
public function hasHeartbeatDetails(): bool
9386
{
94-
return $this->heartbeatDetails !== null;
87+
return $this->lastHeartbeatDetails !== null;
9588
}
9689

9790
/**
9891
* @param Type|string $type
99-
* @return mixed
10092
*/
101-
public function getHeartbeatDetails($type = null)
93+
public function getLastHeartbeatDetails($type = null): mixed
10294
{
10395
if (!$this->hasHeartbeatDetails()) {
10496
return null;
10597
}
10698

107-
return $this->heartbeatDetails->getValue(0, $type);
99+
return $this->lastHeartbeatDetails->getValue(0, $type);
108100
}
109101

110102
public function doNotCompleteOnReturn(): void
@@ -117,13 +109,7 @@ public function isDoNotCompleteOnReturn(): bool
117109
return $this->doNotCompleteOnReturn;
118110
}
119111

120-
/**
121-
* @param mixed $details
122-
*
123-
* @throws ActivityCompletionException
124-
* @throws ActivityCanceledException
125-
*/
126-
public function heartbeat($details): void
112+
public function heartbeat(mixed $details): void
127113
{
128114
// we use native host process RPC here to avoid excessive GRPC connections and to handle throttling
129115
// on Golang end
@@ -141,14 +127,29 @@ public function heartbeat($details): void
141127
],
142128
);
143129

144-
if (!empty($response['canceled'])) {
145-
throw ActivityCanceledException::fromActivityInfo($this->info);
130+
$cancelled = (bool) ($response['canceled'] ?? false);
131+
$paused = (bool) ($response['paused'] ?? false);
132+
133+
if ($cancelled || $paused) {
134+
$this->cancellationDetails ??= new ActivityCancellationDetails(
135+
cancelRequested: $cancelled,
136+
paused: $paused,
137+
);
138+
139+
throw $cancelled
140+
? ActivityCanceledException::fromActivityInfo($this->info)
141+
: ActivityPausedException::fromActivityInfo($this->info);
146142
}
147143
} catch (ServiceClientException $e) {
148144
throw ActivityCompletionException::fromActivityInfo($this->info, $e);
149145
}
150146
}
151147

148+
public function getCancellationDetails(): ?ActivityCancellationDetails
149+
{
150+
return $this->cancellationDetails;
151+
}
152+
152153
public function getInstance(): object
153154
{
154155
\assert($this->instance !== null, 'Activity instance is not available');

src/Internal/Declaration/Dispatcher/Dispatcher.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,9 +88,9 @@ private function scopeMatches(int $scope): bool
8888
}
8989

9090
/**
91-
* @psalm-return FunctionExecutor
9291
*
9392
* @return \Closure(object, array): mixed
93+
* @psalm-return FunctionExecutor
9494
*/
9595
private function createExecutorFromMethod(\ReflectionMethod $fun): \Closure
9696
{
@@ -104,9 +104,9 @@ private function createExecutorFromMethod(\ReflectionMethod $fun): \Closure
104104
}
105105

106106
/**
107-
* @psalm-return FunctionExecutor
108107
*
109108
* @return \Closure(object, array): mixed
109+
* @psalm-return FunctionExecutor
110110
*/
111111
private function createExecutorFromFunction(\ReflectionFunction $fun): \Closure
112112
{

src/Promise.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -153,8 +153,8 @@ static function (iterable $array) use ($count, $cancellationQueue, $resolve, $re
153153
* The `$callback` function receives each item as an argument, where the item is
154154
* a fully resolved value of a promise or a value in `$promises`.
155155
*
156-
* @psalm-param PromiseMapCallback $map
157156
* @param iterable<int, PromiseInterface|mixed> $promises
157+
* @psalm-param PromiseMapCallback $map
158158
*/
159159
public static function map(iterable $promises, callable $map): PromiseInterface
160160
{
@@ -203,10 +203,10 @@ static function (mixed $mapped) use ($i, &$values, &$toResolve, $resolve): void
203203
* promises and/or values. The `$reduce` callback may return either a value or a promise,
204204
* and `$initial` may be a promise or a value for the starting value.
205205
*
206-
* @psalm-param PromiseReduceCallback $reduce
207206
* @param iterable<int, PromiseInterface|mixed> $promises
208207
* @param callable(mixed $current, mixed $carry, int $current, positive-int $items): mixed $reduce
209208
* @param mixed $initial
209+
* @psalm-param PromiseReduceCallback $reduce
210210
*/
211211
public static function reduce(iterable $promises, callable $reduce, $initial = null): PromiseInterface
212212
{

testing/src/Environment.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,6 @@ public function startTemporalServer(
9797
'--dynamic-config-value', 'frontend.enableUpdateWorkflowExecution=true',
9898
'--dynamic-config-value', 'frontend.enableUpdateWorkflowExecutionAsyncAccepted=true',
9999
'--dynamic-config-value', 'frontend.enableExecuteMultiOperation=true',
100-
'--dynamic-config-value', 'system.enableEagerWorkflowStart=true',
101100
'--log-level', 'error',
102101
'--headless',
103102
...$parameters,

0 commit comments

Comments
 (0)