Skip to content

Commit 76ece8f

Browse files
xepozzgithub-actions
andauthored
feat: expose side effect summary (#700)
* feat: implement side effect summary * style(php-cs-fixer): fix coding standards * fix: pass not null * fix: pass empty options * feat: add support for SideEffectOptions in sideEffect method --------- Co-authored-by: github-actions <github-actions@users.noreply.github.com>
1 parent f00f12c commit 76ece8f

7 files changed

Lines changed: 239 additions & 8 deletions

File tree

src/Common/SideEffectOptions.php

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
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\Common;
13+
14+
use JetBrains\PhpStorm\Pure;
15+
use Temporal\Internal\Marshaller\Meta\Marshal;
16+
use Temporal\Internal\Support\Options;
17+
18+
/**
19+
* SideEffectOptions provides options for side effects.
20+
*
21+
* @psalm-immutable
22+
*/
23+
class SideEffectOptions extends Options
24+
{
25+
/**
26+
* Optional summary of the side effect.
27+
*
28+
* Single-line fixed summary for this side effect that will appear in UI/CLI.
29+
* This can be in single-line Temporal Markdown format.
30+
*
31+
* @experimental This API is experimental and may change in the future.
32+
*
33+
* @since RoadRunner 2025.1.2
34+
*/
35+
#[Marshal(name: 'summary')]
36+
public string $summary = '';
37+
38+
/**
39+
* Optional summary of the side effect.
40+
*
41+
* Single-line fixed summary for this side effect that will appear in UI/CLI.
42+
* This can be in single-line Temporal Markdown format.
43+
*
44+
* @experimental This API is experimental and may change in the future.
45+
*
46+
* @return $this
47+
*/
48+
#[Pure]
49+
public function withSummary(string $summary): self
50+
{
51+
$self = clone $this;
52+
$self->summary = $summary;
53+
return $self;
54+
}
55+
}

src/Interceptor/WorkflowOutboundCalls/SideEffectInput.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111

1212
namespace Temporal\Interceptor\WorkflowOutboundCalls;
1313

14+
use Temporal\Common\SideEffectOptions;
15+
1416
/**
1517
* @psalm-immutable
1618
*/
@@ -22,13 +24,16 @@ final class SideEffectInput
2224
*/
2325
public function __construct(
2426
public readonly \Closure $callable,
27+
public readonly ?SideEffectOptions $options = null,
2528
) {}
2629

2730
public function with(
2831
?\Closure $callable = null,
32+
?SideEffectOptions $options = null,
2933
): self {
3034
return new self(
3135
$callable ?? $this->callable,
36+
$options ?? $this->options,
3237
);
3338
}
3439
}

src/Internal/Transport/Request/SideEffect.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ final class SideEffect extends Request
1818
{
1919
public const NAME = 'SideEffect';
2020

21-
public function __construct(ValuesInterface $values)
21+
public function __construct(ValuesInterface $values, array $options)
2222
{
23-
parent::__construct(self::NAME, [], $values);
23+
parent::__construct(self::NAME, $options, $values);
2424
}
2525
}

src/Internal/Workflow/WorkflowContext.php

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
use Temporal\Api\Sdk\V1\EnhancedStackTrace;
2323
use Temporal\Common\SearchAttributes\SearchAttributeKey;
2424
use Temporal\Common\SearchAttributes\SearchAttributeUpdate;
25+
use Temporal\Common\SideEffectOptions;
2526
use Temporal\Common\Uuid;
2627
use Temporal\DataConverter\EncodedValues;
2728
use Temporal\DataConverter\Type;
@@ -254,7 +255,7 @@ public function getVersion(string $changeId, int $minSupported, int $maxSupporte
254255
)(new GetVersionInput($changeId, $minSupported, $maxSupported));
255256
}
256257

257-
public function sideEffect(callable $context): PromiseInterface
258+
public function sideEffect(callable $context, ?SideEffectOptions $options = null): PromiseInterface
258259
{
259260
$value = null;
260261
$closure = $context(...);
@@ -265,7 +266,7 @@ public function sideEffect(callable $context): PromiseInterface
265266
$closure,
266267
/** @see WorkflowOutboundCallsInterceptor::sideEffect() */
267268
'sideEffect',
268-
)(new SideEffectInput($closure));
269+
)(new SideEffectInput($closure, $options));
269270
}
270271
} catch (\Throwable $e) {
271272
return reject($e);
@@ -279,7 +280,10 @@ public function sideEffect(callable $context): PromiseInterface
279280
}
280281

281282
$last = fn(): PromiseInterface => EncodedValues::decodePromise(
282-
$this->request(new SideEffect(EncodedValues::fromValues([$value]))),
283+
$this->request(new SideEffect(
284+
EncodedValues::fromValues([$value]),
285+
$options === null ? [] : $this->services->marshaller->marshal($options),
286+
)),
283287
$returnType,
284288
);
285289
return $last();

src/Workflow.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
use Temporal\Activity\ActivityOptionsInterface;
1919
use Temporal\Client\WorkflowStubInterface;
2020
use Temporal\Common\SearchAttributes\SearchAttributeUpdate;
21+
use Temporal\Common\SideEffectOptions;
2122
use Temporal\DataConverter\Type;
2223
use Temporal\DataConverter\ValuesInterface;
2324
use Temporal\Exception\Failure\CanceledFailure;
@@ -567,9 +568,10 @@ public static function getVersion(string $changeId, int $minSupported, int $maxS
567568
* @return PromiseInterface<TReturn>
568569
* @throws OutOfContextException in the absence of the workflow execution context.
569570
*/
570-
public static function sideEffect(callable $value): PromiseInterface
571+
public static function sideEffect(callable $value, ?SideEffectOptions $options = null): PromiseInterface
571572
{
572-
return self::getCurrentContext()->sideEffect($value);
573+
/** @psalm-suppress TooManyArguments */
574+
return self::getCurrentContext()->sideEffect($value, $options);
573575
}
574576

575577
/**

src/Workflow/WorkflowContextInterface.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
use Temporal\Activity\ActivityOptions;
1818
use Temporal\Activity\ActivityOptionsInterface;
1919
use Temporal\Common\SearchAttributes\SearchAttributeUpdate;
20+
use Temporal\Common\SideEffectOptions;
2021
use Temporal\DataConverter\Type;
2122
use Temporal\DataConverter\ValuesInterface;
2223
use Temporal\Internal\Support\DateInterval;
@@ -152,7 +153,7 @@ public function getVersion(string $changeId, int $minSupported, int $maxSupporte
152153
* @param callable(): TReturn $context
153154
* @return PromiseInterface<TReturn>
154155
*/
155-
public function sideEffect(callable $context): PromiseInterface;
156+
public function sideEffect(callable $context/*, ?SideEffectOptions $options = null */): PromiseInterface;
156157

157158
/**
158159
* @internal This is an internal method
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Temporal\Tests\Acceptance\Extra\Workflow\SideEffect;
6+
7+
use PHPUnit\Framework\Attributes\Test;
8+
use Temporal\Api\Common\V1\Payload;
9+
use Temporal\Client\WorkflowClientInterface;
10+
use Temporal\Client\WorkflowStubInterface;
11+
use Temporal\Common\SideEffectOptions;
12+
use Temporal\DataConverter\DataConverterInterface;
13+
use Temporal\Tests\Acceptance\App\Attribute\Stub;
14+
use Temporal\Tests\Acceptance\App\TestCase;
15+
use Temporal\Workflow;
16+
use Temporal\Workflow\WorkflowInterface;
17+
use Temporal\Workflow\WorkflowMethod;
18+
19+
class SideEffectTest extends TestCase
20+
{
21+
#[Test]
22+
public static function currentTime(
23+
#[Stub('Extra_Workflow_SideEffect')]
24+
WorkflowStubInterface $stub,
25+
): void {
26+
$result = $stub->getResult(type: 'array');
27+
28+
self::assertEquals($result['system'], $result['current']);
29+
}
30+
31+
#[Test]
32+
public static function summaryRecordedOnMarker(
33+
#[Stub('Extra_Workflow_SideEffect')]
34+
WorkflowStubInterface $stub,
35+
WorkflowClientInterface $client,
36+
DataConverterInterface $dataConverter,
37+
): void {
38+
$stub->getResult();
39+
40+
$summaries = self::collectSideEffectSummaries($client, $stub, $dataConverter);
41+
42+
self::assertSame(['Side Effect Summary'], $summaries);
43+
}
44+
45+
#[Test]
46+
public static function distinctSummariesPerSideEffect(
47+
#[Stub('Extra_Workflow_SideEffect_Multi')]
48+
WorkflowStubInterface $stub,
49+
WorkflowClientInterface $client,
50+
DataConverterInterface $dataConverter,
51+
): void {
52+
$stub->getResult();
53+
54+
$summaries = self::collectSideEffectSummaries($client, $stub, $dataConverter);
55+
56+
self::assertSame(['first summary', 'second summary'], $summaries);
57+
}
58+
59+
#[Test]
60+
public static function noSummaryWhenOptionsOmitted(
61+
#[Stub('Extra_Workflow_SideEffect_NoOptions')]
62+
WorkflowStubInterface $stub,
63+
WorkflowClientInterface $client,
64+
): void {
65+
$stub->getResult();
66+
67+
$markerCount = 0;
68+
foreach ($client->getWorkflowHistory($stub->getExecution()) as $event) {
69+
if (!$event->hasMarkerRecordedEventAttributes()) {
70+
continue;
71+
}
72+
if ($event->getMarkerRecordedEventAttributes()->getMarkerName() !== 'SideEffect') {
73+
continue;
74+
}
75+
76+
++$markerCount;
77+
self::assertNull($event->getUserMetadata()?->getSummary());
78+
}
79+
80+
self::assertSame(1, $markerCount, 'SideEffect marker must exist in the Workflow history');
81+
}
82+
83+
/**
84+
* @return list<string>
85+
*/
86+
private static function collectSideEffectSummaries(
87+
WorkflowClientInterface $client,
88+
WorkflowStubInterface $stub,
89+
DataConverterInterface $dataConverter,
90+
): array {
91+
$summaries = [];
92+
foreach ($client->getWorkflowHistory($stub->getExecution()) as $event) {
93+
if (!$event->hasMarkerRecordedEventAttributes()) {
94+
continue;
95+
}
96+
if ($event->getMarkerRecordedEventAttributes()->getMarkerName() !== 'SideEffect') {
97+
continue;
98+
}
99+
100+
$payload = $event->getUserMetadata()?->getSummary();
101+
self::assertInstanceOf(Payload::class, $payload);
102+
$summaries[] = $dataConverter->fromPayload($payload, 'string');
103+
}
104+
105+
return $summaries;
106+
}
107+
}
108+
109+
#[WorkflowInterface]
110+
class MainWorkflow
111+
{
112+
#[WorkflowMethod('Extra_Workflow_SideEffect')]
113+
public function run()
114+
{
115+
yield Workflow::timer('1 seconds');
116+
117+
/**
118+
* @var \DateTimeImmutable $currentDate
119+
*/
120+
$currentDate = yield Workflow::sideEffect(
121+
static fn(): \DateTimeImmutable => new \DateTimeImmutable(),
122+
SideEffectOptions::new()
123+
->withSummary('Side Effect Summary'),
124+
);
125+
126+
return yield [
127+
'current' => [
128+
'timestamp' => $currentDate->getTimestamp(),
129+
'timezone.offset' => $currentDate->getTimeZone()->getOffset($currentDate),
130+
],
131+
'system' => [
132+
'timestamp' => Workflow::now()->getTimestamp(),
133+
'timezone.offset' => Workflow::now()->getTimezone()->getOffset(Workflow::now()),
134+
],
135+
];
136+
}
137+
}
138+
139+
#[WorkflowInterface]
140+
class MultiSummaryWorkflow
141+
{
142+
#[WorkflowMethod('Extra_Workflow_SideEffect_Multi')]
143+
public function run()
144+
{
145+
yield Workflow::sideEffect(
146+
static fn(): int => 1,
147+
SideEffectOptions::new()->withSummary('first summary'),
148+
);
149+
yield Workflow::sideEffect(
150+
static fn(): int => 2,
151+
SideEffectOptions::new()->withSummary('second summary'),
152+
);
153+
}
154+
}
155+
156+
#[WorkflowInterface]
157+
class NoOptionsWorkflow
158+
{
159+
#[WorkflowMethod('Extra_Workflow_SideEffect_NoOptions')]
160+
public function run()
161+
{
162+
yield Workflow::sideEffect(static fn(): int => 42);
163+
}
164+
}

0 commit comments

Comments
 (0)