Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Fault-tolerant workflow orchestration in PHP @ ...

Fault-tolerant workflow orchestration in PHP @ PHPCon Poland 2024

Developing your application on the Temporal platform gives you a secret weapon – durable execution – which guarantees that your code runs to completion no matter what. The result? Bulletproof applications that are faster to develop and easier to support. Because the full running state of a workflow is durable and fault tolerant by default, your business logic can be recovered, replayed or paused from any arbitrary point.

Sebastian Grodzicki

October 26, 2024
Tweet

More Decks by Sebastian Grodzicki

Other Decks in Programming

Transcript

  1. Sebastian Grodzicki • CTO @ • ex-Google/Elastic/SHOWROOM/GoldenLine • PHP developer

    for 20+ years • PHPCon speaker/attendee for 10+ years •  @sebgrodzicki phpinfo();
  2. The Need for Fault Tolerance • Application failures and crashes

    • Network outages • Flaky endpoints • Long-running processes • …and more.
  3. Why Temporal? • Reliable distributed applications • Productive development paradigms

    and code structure • Visible distributed application state
  4. Why Temporal? Temporal works with your preexisting choices for: •

    runtime, • test framework, • deployment, • continuous integration, • web frameworks, • …and more.
  5. Why Temporal? Temporal allows you to develop with durable execution

    in di ff erent languages and multiple languages can be used to build single services, enabling polyglot development.
  6. Why Temporal? Temporal is built in the open and released

    under the MIT license. It has been endorsed by some of the world's best companies and is loved by a growing, vibrant community.
  7. – Rob Zienert, Senior Software Engineer @ Net fl ix

    "Net fl ix engineers spend less time writing logic to maintain application consistency or guard against failures because Temporal does it for them. The Temporal platform is easy to operate and fi ts naturally in our development work fl ow."
  8. – Mitchell Hashimoto, Co-founder @ Hashicorp "Temporal's technology satis fi

    ed all of these requirements out of the box and allowed our developers to focus on business logic. Without Temporal's technology, we would've spent a signi fi cant amount of time rebuilding Temporal and would've very likely done a worse job."
  9. – Sebastian Grodzicki, CTO @ Levity "Temporal has been a

    game-changer for our team. By abstracting away the complexities of building scalable distributed systems, it has allowed us to focus on delivering value to our customers at a much faster pace."
  10. The Building Blocks • Work fl ow: your business logic,

    de fi ned in code, outlining each step in your process. • Activities: individual units of work in your Work fl ow. • SDK: everything needed to build Work fl ows, Activities, and various other Temporal features in a speci fi c programming language. • Temporal Service: a set of services and components on your own infrastructure (or Temporal Cloud).
  11. Building Work fl ows in PHP • PHP SDK: $

    composer require temporal/sdk • Temporal CLI: $ brew install temporal (on macOS) $ temporal server start-dev
  12. Demo: Money Transfer #[WorkflowInterface] interface AccountTransferWorkflowInterface { #[WorkflowMethod(name: "MoneyTransfer")] public

    function transfer( string $fromAccountId, string $toAccountId, string $referenceId, int $amountCents ); }
  13. Demo: Money Transfer #[ActivityInterface(prefix: "MoneyTransfer.")] interface AccountInterface { public function

    deposit(string $accountId, string $referenceId, int $amountCents): void; public function withdraw(string $accountId, string $referenceId, int $amountCents): void; }
  14. Demo: Money Transfer class AccountTransferWorkflow implements AccountTransferWorkflowInterface { private AccountInterface

    $account; public function __construct() { $this->account = Workflow::newActivityStub( AccountInterface::class, ActivityOptions::new() ->withStartToCloseTimeout(CarbonInterval::seconds(5)) ->withRetryOptions(RetryOptions::new()->withMaximumAttempts(10)) ); } public function transfer(string $fromAccountId, string $toAccountId, string $referenceId, int $amountCents) { yield $this->account->withdraw($fromAccountId, $referenceId, $amountCents); yield $this->account->deposit($toAccountId, $referenceId, $amountCents); } }
  15. Demo: Money Transfer class AccountActivity implements AccountInterface { public function

    deposit(string $accountId, string $referenceId, int $amountCents): void { $this->log( "Withdraw to %s of %d cents requested. ReferenceId=%s\n", $accountId, $amountCents, $referenceId ); // throw new \RuntimeException("simulated"); // Uncomment to simulate failure } public function withdraw(string $accountId, string $referenceId, int $amountCents): void { $this->log( "Deposit to %s of %d cents requested. ReferenceId=%s\n", $accountId, $amountCents, $referenceId ); } }
  16. Demo: Money Transfer class ExecuteCommand extends Command { protected const

    NAME = 'money-transfer'; protected const DESCRIPTION = 'Execute MoneyTransferWorkflow'; public function execute(InputInterface $input, OutputInterface $output): int { $workflow = $this->workflowClient->newWorkflowStub(AccountTransferWorkflowInterface::class); $output->writeln("Starting <comment>MoneyTransferWorkflow</comment>... "); // runs in blocking mode $workflow->transfer( 'fromID', 'toID', 'refID', 1000 ); $output->writeln("<info>Workflow complete</info>"); return self::SUCCESS; } }
  17. Demo: Subscription #[ActivityInterface(prefix: "Subscription.")] interface AccountActivityInterface { public function sendWelcomeEmail(string

    $userID): void; public function chargeMonthlyFee(string $userID): void; public function sendEndOfTrialEmail(string $userID): void; public function sendMonthlyChargeEmail(string $userID): void; public function sendSorryToSeeYouGoEmail(string $userID): void; public function processSubscriptionCancellation(string $userID): void; }
  18. Demo: Subscription class SubscriptionWorkflow implements SubscriptionWorkflowInterface { private $account; public

    function __construct() { $this->account = Workflow::newActivityStub( AccountActivityInterface::class, ActivityOptions::new() ->withScheduleToCloseTimeout(CarbonInterval::seconds(2)) ); } public function subscribe(string $userID){...} }
  19. Demo: Subscription class SubscriptionWorkflow implements SubscriptionWorkflowInterface { public function subscribe(string

    $userID) { yield $this->account->sendWelcomeEmail($userID); try { $trialPeriod = true; while (true) { // Lower period duration to observe workflow behaviour yield Workflow::timer(CarbonInterval::days(30)); if ($trialPeriod) { yield $this->account->sendEndOfTrialEmail($userID); $trialPeriod = false; continue; } yield $this->account->chargeMonthlyFee($userID); yield $this->account->sendMonthlyChargeEmail($userID); } } catch (CanceledFailure $e) { yield Workflow::asyncDetached( function () use ($userID) { yield $this->account->processSubscriptionCancellation($userID); yield $this->account->sendSorryToSeeYouGoEmail($userID); } ); } } }
  20. Demo: Subscription class SubscribeCommand extends Command { protected const NAME

    = 'subscribe:start'; protected const DESCRIPTION = 'Execute Subscription\SubscriptionWorkflow with custom user ID'; protected const ARGUMENTS = [ ['userID', InputArgument::REQUIRED, 'Unique user ID'] ]; public function execute(InputInterface $input, OutputInterface $output): int{...} }
  21. Demo: Subscription class SubscribeCommand extends Command { public function execute(InputInterface

    $input, OutputInterface $output): int { $userID = $input->getArgument('userID'); $workflow = $this->workflowClient->newWorkflowStub( SubscriptionWorkflowInterface::class, WorkflowOptions::new() ->withWorkflowId('subscription:' . $userID) ->withWorkflowIdReusePolicy(IdReusePolicy::POLICY_ALLOW_DUPLICATE) ); $output->writeln("Start <comment>SubscriptionWorkflow</comment>... "); try { $run = $this->workflowClient->start($workflow, $userID); } catch (WorkflowExecutionAlreadyStartedException $e) { $output->writeln('<fg=red>Already running</fg=red>'); return self::SUCCESS; } $output->writeln( sprintf( 'Started: WorkflowID=<fg=magenta>%s</fg=magenta>, RunID=<fg=magenta>%s</fg=magenta>', $run->getExecution()->getID(), $run->getExecution()->getRunID(), ) ); return self::SUCCESS; } }
  22. Demo: Subscription class CancelCommand extends Command { protected const NAME

    = 'subscribe:cancel'; protected const DESCRIPTION = 'Cancel Subscription\SubscriptionWorkflow for user ID'; protected const ARGUMENTS = [ ['userID', InputArgument::REQUIRED, 'Unique user ID'] ]; public function execute(InputInterface $input, OutputInterface $output): int { $userID = $input->getArgument('userID'); $workflow = $this->workflowClient->newUntypedRunningWorkflowStub('subscription:' . $userID); try { $workflow->cancel(); $output->writeln('Cancelled'); } catch (WorkflowNotFoundException $e) { $output->writeln('<fg=red>Already stopped</fg=red>'); } return self::SUCCESS; } }