Skip to main content

2 posts tagged with "determinism"

View All Tags

· 4 min read
Richard

When it comes to building web applications, managing complex processes and activities can be a daunting task. Laravel Workflow simplifies this process by providing tools for defining and managing workflows and activities. In addition, integrating a state machine library can offer more explicit control over the transitions between states or activities, resulting in a more structured and visual representation of the workflow. In this blog post, we will explore the benefits of using Laravel Workflow along with a state machine and walk through an example of integrating Laravel Workflow with Finite, a simple state machine library.

Benefits of Combining Laravel Workflow and State Machines

Using Laravel Workflow and a state machine together provides several advantages:

  1. Flexibility and modularity: Laravel Workflow allows developers to break down complex processes into smaller, modular units that are easy to maintain and update.
  2. Explicit control over transitions: State machines provide a clear visualization of workflow states, activities, and transitions, making it easier to understand and maintain.
  3. Robust error handling and retries: Laravel Workflow offers built-in support for handling errors and retries, ensuring that workflows are executed reliably and consistently.
  4. Scalability: Laravel Workflow supports queuing and parallel execution, allowing workflows to be executed asynchronously on worker servers.
  5. Integration with Laravel’s queue and event systems: This allows for seamless integration with other Laravel features and packages.

Installation Guide

To get started with Laravel Workflow and Finite, you will need to install them in your Laravel project:

For Laravel Workflow, run the following command:

composer require laravel-workflow/laravel-workflow

For Finite, run the following command:

composer require yohang/finite

Loan Application Workflow Example

The following code demonstrates how to create a LoanApplicationWorkflow using Laravel Workflow and Finite:

use Finite\StatefulInterface;  
use Finite\StateMachine\StateMachine;
use Finite\State\State;
use Finite\State\StateInterface;
use Workflow\Models\StoredWorkflow;
use Workflow\SignalMethod;
use Workflow\WorkflowStub;
use Workflow\Workflow;

class LoanApplicationWorkflow extends Workflow implements StatefulInterface
{
private $state;
private $stateMachine;

public function setFiniteState($state)
{
$this->state = $state;
}

public function getFiniteState()
{
return $this->state;
}

#[SignalMethod]
public function submit()
{
$this->stateMachine->apply('submit');
}

#[SignalMethod]
public function approve()
{
$this->stateMachine->apply('approve');
}

#[SignalMethod]
public function deny()
{
$this->stateMachine->apply('deny');
}

public function isSubmitted()
{
return $this->stateMachine->getCurrentState()->getName() === 'submitted';
}

public function isApproved()
{
return $this->stateMachine->getCurrentState()->getName() === 'approved';
}

public function isDenied()
{
return $this->stateMachine->getCurrentState()->getName() === 'denied';
}

public function __construct(
public StoredWorkflow $storedWorkflow,
...$arguments
) {
parent::__construct($storedWorkflow, $arguments);

$this->stateMachine = new StateMachine();

$this->stateMachine->addState(new State('created', StateInterface::TYPE\_INITIAL));
$this->stateMachine->addState('submitted');
$this->stateMachine->addState(new State('approved', StateInterface::TYPE\_FINAL));
$this->stateMachine->addState(new State('denied', StateInterface::TYPE\_FINAL));

$this->stateMachine->addTransition('submit', 'created', 'submitted');
$this->stateMachine->addTransition('approve', 'submitted', 'approved');
$this->stateMachine->addTransition('deny', 'submitted', 'denied');

$this->stateMachine->setObject($this);
$this->stateMachine->initialize();
}

public function execute()
{
// loan created

yield WorkflowStub::await(fn () => $this->isSubmitted());

// loan submitted

yield WorkflowStub::await(fn () => $this->isApproved() || $this->isDenied());

// loan approved/denied

return $this->stateMachine->getCurrentState()->getName();
}
}

In this example, we define a LoanApplicationWorkflow class that extends Workflow and implements StatefulInterface. The workflow has four states: created, submitted, approved or denied. The workflow transitions between these states by externally calling the submit(), approve(), and deny() signal methods.

To use the LoanApplicationWorkflow, you can create a new instance of it, start the workflow, submit the loan application, approve it, and get the output as follows:

// create workflow  
$workflow = WorkflowStub::make(LoanApplicationWorkflow::class);

// start workflow
$workflow->start();

sleep(1);

// submit signal
$workflow->submit();

sleep(1);

// approve signal
$workflow->approve();

sleep(1);

$workflow->output();
// "approved"

This is the view from Waterline.

timeline

Conclusion

Although Laravel Workflow offers a way to define and manage workflows and activities, some developers might still prefer to use a state machine to have more explicit control over the transitions between states or activities.

A state machine can provide a more structured and visual representation of the workflow, making it easier to understand and maintain. In such cases, a state machine library can be integrated with Laravel Workflow. This allows developers to define their workflow states, activities, and transitions using the state machine library while still leveraging Laravel Workflow’s features, such as queuing, parallel execution, error handling, retries, and integration with Laravel’s queue and event systems.

The Laravel developer community has created several state machine packages that can be integrated with Laravel Workflow, such as the following:

By integrating a state machine library with Laravel Workflow, developers can get the best of both worlds: the flexibility and modularity of Laravel Workflow and the explicit control and visualization of a state machine. This can help to create more maintainable, robust, and scalable workflows for complex business processes.

· 3 min read
Richard

effects

Workflows provide a more organized and structured approach to managing distributed processes, making it easier for developers to understand and work with complex logic.

Laravel Workflow is a powerful package for the Laravel web framework that provides tools for defining and managing workflows.

One of the key features of any workflow engine is the ability to track the history of a workflow as it is executed which allows a workflow to be retried if it fails or encounters an error. However, this also means that your workflow code must be deterministic and any non-deterministic code has to be carefully managed.

Recently, Laravel Workflow added support for side effects, which are closures containing non-deterministic code that is only executed once and the result saved. Side effects are a useful way to introduce non-deterministic behavior into a workflow, such as generating a random number or UUID.

Here is an example workflow that demonstrates side effects.

class SideEffectWorkflow extends Workflow  
{
public function execute()
{
$sideEffect = yield WorkflowStub::sideEffect(
fn () => random\_int(PHP\_INT\_MIN, PHP\_INT\_MAX)
);

$badSideEffect = random\_int(PHP\_INT\_MIN, PHP\_INT\_MAX);

$result1 = yield ActivityStub::make(SimpleActivity::class, $sideEffect);

$result2 = yield ActivityStub::make(SimpleActivity::class, $badSideEffect);

if ($sideEffect !== $result1) {
throw new Exception(
'These side effects should match because it was properly wrapped in WorkflowStub::sideEffect().'
);
}

if ($badSideEffect === $result2) {
throw new Exception(
'These side effects should not match because it was not wrapped in WorkflowStub::sideEffect().'
);
}
}
}

The activity doesn’t actually do anything. It just takes the input and passes it back out unmodified, so that we can compare the result to what we generated inside of the workflow.

class SimpleActivity extends Activity  
{
public function execute($input)
{
return $input;
}
}

In this example, the workflow generates two random integers: one using a side effect and the other using a local variable. The values of these integers are then passed to two different activities.

The first activity receives the value of the side effect, which has been saved. As a result, the value of the side effect should remain constant throughout the execution of the workflow.

The second activity receives the value of the local variable, which is not saved and will be regenerated. This means that the value of the local variable will change between executions of the workflow.

As a result, it is not expected that the value of the local variable will match the value returned from the second activity. The odds of two random integers generated using random_int(PHP_INT_MIN, PHP_INT_MAX) being equal are extremely low, since there are a very large number of possible integers in this range.

dice

It’s important to use side effects appropriately in your workflow to ensure that your workflow is reliable and can recover from failures. Only use side effects for short pieces of code that cannot fail, and make sure to use activities to perform long-running work that may fail and need to be retried, such as API requests or external processes.

Overall, side effects are a powerful tool for introducing non-deterministic behavior into your workflows. When used correctly, they can help you to add more flexibility and complexity to your application’s logic.

Laravel Workflow is a powerful tool for managing workflows in your Laravel applications, and the addition of support for side effects makes it even more powerful!