Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add interceptors examples #40

Merged
merged 4 commits into from
Nov 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,4 +161,6 @@ The following samples demonstrate some of the more complex aspects associated wi

- **[Subscription](https://github.com/temporalio/samples-php/tree/master/app/src/Subscription)**: Demonstrates a long-running process associated with a user ID. The process charges the user once every 30 days after a one month free trial period.

- **[Interceptors](https://github.com/temporalio/samples-php/tree/master/app/src/Interceptors)**: Demonstrates how to use Workflow and Activity interceptors to implement custom logic.

<!-- @@@SNIPEND -->
6 changes: 5 additions & 1 deletion app/.rr.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ temporal:
activities:
num_workers: 4

service:
interceptors:
command: "./rr serve -c ./src/Interceptors/.rr.yaml"

logs:
level: debug
level: info
mode: development
3 changes: 2 additions & 1 deletion app/composer.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
{
"minimum-stability": "beta",
"prefer-stable": true,
"require": {
"temporal/sdk": ">=2.6.0",
"temporal/sdk": ">=2.7.0",
"spiral/tokenizer": ">=2.7"
},
"autoload": {
Expand Down
2 changes: 1 addition & 1 deletion app/src/FileProcessing/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# FileProcessing sample

This sample demonstrates how to how to use Task routing.
This sample demonstrates how to use Task routing.

This Workflow downloads a file, processes it, and uploads the result to a destination.
Any worker can pick up the first Activity.
Expand Down
14 changes: 14 additions & 0 deletions app/src/Interceptors/.rr.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
version: "3"

server:
command: "php worker.php"
env:
- TEMPORAL_CLI_ADDRESS: temporal:7233

temporal:
address: "temporal:7233"
activities:
num_workers: 2

logs:
level: info
36 changes: 36 additions & 0 deletions app/src/Interceptors/Activity/Sleeper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

/**
* This file is part of Temporal package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace Temporal\Samples\Interceptors\Activity;

use Temporal\Activity\ActivityInterface;
use Temporal\Activity\ActivityMethod;
use Temporal\Samples\Interceptors\Attribute\StartToCloseTimeout;

#[StartToCloseTimeout(5)]
#[ActivityInterface(prefix: 'Interceptors.')]
class Sleeper
{
#[ActivityMethod]
public function sleep(int $howToSleep)
{
\sleep($howToSleep);
return 'I am awake!';
}

#[StartToCloseTimeout(2)]
#[ActivityMethod]
public function sleep2(int $howToSleep)
{
\sleep($howToSleep);
return 'I am awake!';
}
}
16 changes: 16 additions & 0 deletions app/src/Interceptors/Attribute/ActivityOption.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

/**
* This file is part of Temporal package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace Temporal\Samples\Interceptors\Attribute;

interface ActivityOption
{
}
23 changes: 23 additions & 0 deletions app/src/Interceptors/Attribute/StartToCloseTimeout.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

/**
* This file is part of Temporal package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace Temporal\Samples\Interceptors\Attribute;

use Attribute;

#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)]
final class StartToCloseTimeout implements ActivityOption
{
public function __construct(
public readonly string|int|float|\DateInterval $timeout,
) {
}
}
51 changes: 51 additions & 0 deletions app/src/Interceptors/ExecuteCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php

/**
* This file is part of Temporal package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace Temporal\Samples\Interceptors;

use Carbon\CarbonInterval;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Temporal\Client\WorkflowOptions;
use Temporal\Samples\Interceptors\Workflow\TestActivityAttributesInterceptor;
use Temporal\SampleUtils\Command;

class ExecuteCommand extends Command
{
protected const NAME = 'interceptors';
protected const DESCRIPTION = 'Execute workflow with interceptors';

public function execute(InputInterface $input, OutputInterface $output)
{
$workflow = $this->workflowClient->newWorkflowStub(
TestActivityAttributesInterceptor::class,
WorkflowOptions::new()
->withTaskQueue('interceptors')
->withWorkflowExecutionTimeout(CarbonInterval::minute())
);

$output->writeln("Starting <comment>Interceptors.TestActivityAttributesInterceptor</comment>... ");

$run = $this->workflowClient->start($workflow);

$output->writeln(
\sprintf(
'Started: WorkflowID=<fg=magenta>%s</fg=magenta>, RunID=<fg=magenta>%s</fg=magenta>',
$run->getExecution()->getID(),
$run->getExecution()->getRunID(),
),
);

$output->writeln(\sprintf("Result:\n<info>%s</info>", \print_r($run->getResult(), true)));

return self::SUCCESS;
}
}
62 changes: 62 additions & 0 deletions app/src/Interceptors/Interceptor/ActivityAttributesInterceptor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<?php

/**
* This file is part of Temporal package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace Temporal\Samples\Interceptors\Interceptor;

use React\Promise\PromiseInterface;
use ReflectionAttribute;
use Temporal\Interceptor\Trait\WorkflowOutboundCallsInterceptorTrait;
use Temporal\Interceptor\WorkflowOutboundCalls\ExecuteActivityInput;
use Temporal\Interceptor\WorkflowOutboundCallsInterceptor;
use Temporal\Samples\Interceptors\Attribute;
use Temporal\Samples\Interceptors\Attribute\ActivityOption;

/**
* The interceptor is used to set activity options based on attributes that are
* implement {@see ActivityOption} interface.
*/
final class ActivityAttributesInterceptor implements WorkflowOutboundCallsInterceptor
{
use WorkflowOutboundCallsInterceptorTrait;

public function executeActivity(ExecuteActivityInput $input, callable $next): PromiseInterface
{
if ($input->method === null) {
return $next($input);
}

$options = $input->options;

foreach ($this->iterateOptions($input->method) as $attribute) {
if ($attribute instanceof Attribute\StartToCloseTimeout) {
\error_log(\sprintf('Redeclare start_to_close timeout of %s to %s', $input->type, $attribute->timeout));
$options = $options->withStartToCloseTimeout($attribute->timeout);
}
}

return $next($input->with(options: $options));
}

/**
* @return iterable<int, ActivityOption>
*/
private function iterateOptions(\ReflectionMethod $method): iterable
{
$class = $method->getDeclaringClass();
foreach ($class->getAttributes(Attribute\ActivityOption::class, ReflectionAttribute::IS_INSTANCEOF) as $attr) {
yield $attr->newInstance();
}

foreach ($method->getAttributes(Attribute\ActivityOption::class, ReflectionAttribute::IS_INSTANCEOF) as $attr) {
yield $attr->newInstance();
}
}
}
38 changes: 38 additions & 0 deletions app/src/Interceptors/Interceptor/RequestLoggerInterceptor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

/**
* This file is part of Temporal package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace Temporal\Samples\Interceptors\Interceptor;

use Psr\Log\LoggerInterface;
use React\Promise\PromiseInterface;
use Temporal\Interceptor\WorkflowOutboundRequestInterceptor;
use Temporal\Worker\Transport\Command\RequestInterface;

final class RequestLoggerInterceptor implements WorkflowOutboundRequestInterceptor
{
public function __construct(
private LoggerInterface $logger,
) {
}

public function handleOutboundRequest(RequestInterface $request, callable $next): PromiseInterface
{
$this->logger->info(
\sprintf('Sending request %s', $request->getName()),
[
'headers' => \iterator_to_array($request->getHeader()),
'options' => $request->getOptions(),
]
);

return $next($request);
}
}
11 changes: 11 additions & 0 deletions app/src/Interceptors/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Interceptors sample

This sample demonstrates how you can use Interceptors.

There are few interceptors:
- RequestLoggerInterceptor - logs all internal requests from the Workflow into the RoadRunner log.
- ActivityAttributesInterceptor - reads [ActivityOption](./Attribute/ActivityOption.php) attributes and applies them to the Activity options.

```bash
php ./app/app.php interceptors
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<?php

/**
* This file is part of Temporal package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace Temporal\Samples\Interceptors\Workflow;

use Temporal\Activity\ActivityOptions;
use Temporal\Common\RetryOptions;
use Temporal\Exception\Failure\ActivityFailure;
use Temporal\Exception\Failure\ApplicationFailure;
use Temporal\Internal\Workflow\ActivityProxy;
use Temporal\Samples\Interceptors\Activity\Sleeper;
use Temporal\Samples\Interceptors\Attribute\StartToCloseTimeout;
use Temporal\Samples\Interceptors\Interceptor\ActivityAttributesInterceptor;
use Temporal\Workflow;

#[Workflow\WorkflowInterface]
class TestActivityAttributesInterceptor
{
private ActivityProxy|Sleeper $sleeper;

public function __construct()
{
$this->sleeper = Workflow::newActivityStub(
Sleeper::class,
ActivityOptions::new()
->withStartToCloseTimeout(10)
->withRetryOptions(
RetryOptions::new()->withMaximumAttempts(1)
)
);
}

#[Workflow\WorkflowMethod('Interceptors.TestActivityAttributesInterceptor')]
public function execute()
{
// Sleep for 1 second.
yield $this->sleeper->sleep(1);

/**
* Sleep for 7 seconds. Should fail if the {@see ActivityAttributesInterceptor} is working because there
* is {@see StartToCloseTimeout} attribute on the {@see Sleeper} class with 5-seconds timeout.
*/
try {
yield $this->sleeper->sleep(7);
throw new ApplicationFailure("ActivityAttributesInterceptor doesn't work on the class", 'TEST', true);
} catch (ActivityFailure) {
// OK
}


/**
* Sleep for 3 seconds. Should fail if the {@see ActivityAttributesInterceptor} is working because there
* is {@see StartToCloseTimeout} attribute on the {@see Sleeper::sleep2()} method with 2-seconds timeout.
*/
try {
yield $this->sleeper->sleep2(3);
throw new ApplicationFailure("ActivityAttributesInterceptor doesn't work on the method", 'TEST', true);
} catch (ActivityFailure) {
// OK
}

return "OK";
}
}
Loading