Git Product home page Git Product logo

laravel-workflow / laravel-workflow Goto Github PK

View Code? Open in Web Editor NEW
787.0 19.0 34.0 235 KB

Durable workflow engine that allows users to track job status, orchestrate microservices and write long running persistent distributed workflows in PHP powered by Laravel Queues. Inspired by Temporal and Azure Durable Functions.

Home Page: https://laravel-workflow.com

License: MIT License

PHP 99.94% Dockerfile 0.06%
laravel php background-jobs durable-functions orchestration queueing workflows jobs status microservices

laravel-workflow's Introduction

logo

GitHub Workflow Status Scrutinizer code quality (GitHub/Bitbucket) Scrutinizer coverage (GitHub/BitBucket) Packagist Downloads (custom server) Docs Packagist License

Laravel Workflow is a package for the Laravel web framework that provides tools for defining and managing workflows and activities. A workflow is a series of interconnected activities that are executed in a specific order to achieve a desired result. Activities are individual tasks or pieces of logic that are executed as part of a workflow.

Laravel Workflow can be used to automate and manage complex processes, such as financial transactions, data analysis, data pipelines, microservices, job tracking, user signup flows, sagas and other business processes. By using Laravel Workflow, developers can break down large, complex processes into smaller, modular units that can be easily maintained and updated.

Some key features and benefits of Laravel Workflow include:

  • Support for defining workflows and activities using simple, declarative PHP classes.
  • Tools for starting, monitoring, and managing workflows, including support for queuing and parallel execution.
  • Built-in support for handling errors and retries, ensuring that workflows are executed reliably and consistently.
  • Integration with Laravel's queue and event systems, allowing workflows to be executed asynchronously on worker servers.
  • Extensive documentation and a growing community of developers who use and contribute to Laravel Workflow.

Documentation

Documentation for Laravel Workflow can be found on the Laravel Workflow website.

Community

You can find us in the GitHub discussions and also on our Discord channel.

Sample App

There's also a sample application that you can run directly from GitHub in your browser.

Usage

1. Create a workflow.

use Workflow\ActivityStub;
use Workflow\Workflow;

class MyWorkflow extends Workflow
{
    public function execute($name)
    {
        $result = yield ActivityStub::make(MyActivity::class, $name);
        return $result;
    }
}

2. Create an activity.

use Workflow\Activity;

class MyActivity extends Activity
{
    public function execute($name)
    {
        return "Hello, {$name}!";
    }
}

3. Run the workflow.

use Workflow\WorkflowStub;

$workflow = WorkflowStub::make(MyWorkflow::class);
$workflow->start('world');
while ($workflow->running());
$workflow->output();
=> 'Hello, world!'

Monitoring

Waterline is a separate UI that works nicely alongside Horizon. Think of Waterline as being to workflows what Horizon is to queues.

Dashboard View

waterline_dashboard

Workflow View

workflow

Refer to https://github.com/laravel-workflow/waterline for installation and configuration instructions.

"Laravel" is a registered trademark of Taylor Otwell. This project is not affiliated, associated, endorsed, or sponsored by Taylor Otwell, nor has it been reviewed, tested, or certified by Taylor Otwell. The use of the trademark "Laravel" is for informational and descriptive purposes only. Laravel Workflow is not officially related to the Laravel trademark or Taylor Otwell.

laravel-workflow's People

Contributors

asanovr avatar hrabbit avatar jbardnz avatar justinelst avatar martio avatar naugrimm avatar rmcdaniel avatar tonypartridge avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

laravel-workflow's Issues

Exception("Activity timed out.")

We have this exception every time on each successfull Activity finish
image
Here is our Workflow:

class SimpleWorkflow extends Workflow
{
    public function execute()
    {
        $result = yield ActivityStub::make(SimpleActivity::class);
        $result2 = yield ActivityStub::make(SimpleActivity::class);
        
        return 'workflow_' . $result . '_'. $result2;
    }
}

Here is our Activity:

class SimpleActivity extends Activity
{

    public function execute()
    {
        // Perform some work...
        return 123;
    }
}

We use Laravel 10 & Laravel Horizon with Redis Queue

Cannot use finally method of Laravel Batch Job inside an Activity

I have a batch job that is dispatched in an Activity. When the batch job is finished, I would like to trigger the Workflow to fire the next Activity. However, I got an error when using finally method.

CleanShot 2023-05-25 at 16 47 42@2x

Here is my Activity code:

Bus::batch($jobs)
->allowFailures()
->finally(function () {
    $this->storedWorkflow->toWorkflow()->setReady(true);
})
->dispatch();

Here is my Workflow code:

yield ActivityStub::make(ActivityOne::class);

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

yield ActivityStub::make(ActivityTwo::class);

Updated: Seems this is an error of Laravel, not this package. If I use $this inside the finally method, it will throw that error. So here is the walkaround:

$id = $this->workflowId();

Bus::batch($jobs)
->allowFailures()
->finally(function () use ($id) {
    $workflow = StoredWorkflow::find($id);
    WorkflowStub::fromStoredWorkflow($workflow)->setReady(true);
})
->dispatch();

However, there is something wrong with serializing the options column in job_batches table. When I run Bus::findBatch($batchID), it throw this error.

CleanShot 2023-05-25 at 22 57 09@2x

Probably, the Larvel serializing was conflicted with the Workflow serializing.

Activity doesn't execute

The workflow's execute() function is called, but not its first activity's execute() function.

The workflow class method starts with:

    public function execute($email_address = '')
    {
        dump(__CLASS__ . '::execute()', $email_address);

        yield ActivityStub::make(SendEmailAddressVerificationEmailActivity::class, $email_address);

and prints:

"App\Workflows\Tests\VerifyEmailAddress\VerifyEmailAddressWorkflow::execute()"
"[email protected]"

But the activity class method doesn't print or log anything:

    public function execute($email_address)
    {
        dump(__CLASS__ . '::execute()', $email_address);

        Log::info('Sending to: ' . $email_address);

In the workflows database table all started workflows have status waiting.
I'm using Laravel 10.

Package state & plans?

Hello @rmcdaniel!

I love the idea of implementing the workflow pattern in Laravel! Tried to use this package in my sandbox environment and had a quick look at the code as well.

A few things I've discovered:

If I decide to use this package in production, I'm looking to become a stable contributor to improve the solution.

Let me know what you think,
Sincerely, Michael


Call to a member function valid() on null {"exception":"[object] (Error(code: 0)

I am trying the laravel workflow tutorial very basic example and getting the below error i have added inside details

<?php

namespace App\Workflows;

use Workflow\ActivityStub;
use Workflow\Workflow;

class MyWorkflow extends Workflow
{
    public function execute()
    {
        $result = yield ActivityStub::make(MyActivity::class);

        return $result;

    }
}
<?php

namespace App\Workflows;

use Workflow\Activity;

class MyActivity extends Activity
{
    public function execute()
    {
        return "done";
    }
}

Error in laravel log

```

[2023-10-12 05:22:29] local.ERROR: Call to a member function valid() on null {"exception":"[object] (Error(code: 0): Call to a member function valid() on null at /Users/khanakia/D1/www/shopify/shopify_bulkupdate_laravel/vendor/laravel-workflow/laravel-workflow/src/Workflow.php:154)
[stacktrace]
#0 /Users/khanakia/D1/www/shopify/shopify_bulkupdate_laravel/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(36): Workflow\Workflow->handle()
#1 /Users/khanakia/D1/www/shopify/shopify_bulkupdate_laravel/vendor/laravel/framework/src/Illuminate/Container/Util.php(41): Illuminate\Container\BoundMethod::Illuminate\Container\{closure}()
#2 /Users/khanakia/D1/www/shopify/shopify_bulkupdate_laravel/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(93): Illuminate\Container\Util::unwrapIfClosure(Object(Closure))
#3 /Users/khanakia/D1/www/shopify/shopify_bulkupdate_laravel/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(35): Illuminate\Container\BoundMethod::callBoundMethod(Object(Illuminate\Foundation\Application), Array, Object(Closure))
#4 /Users/khanakia/D1/www/shopify/shopify_bulkupdate_laravel/vendor/laravel/framework/src/Illuminate/Container/Container.php(662): Illuminate\Container\BoundMethod::call(Object(Illuminate\Foundation\Application), Array, Array, NULL)
#5 /Users/khanakia/D1/www/shopify/shopify_bulkupdate_laravel/vendor/laravel/framework/src/Illuminate/Bus/Dispatcher.php(128): Illuminate\Container\Container->call(Array)
#6 /Users/khanakia/D1/www/shopify/shopify_bulkupdate_laravel/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(141): Illuminate\Bus\Dispatcher->Illuminate\Bus\{closure}(Object(App\Workflows\MyWorkflow))
#7 /Users/khanakia/D1/www/shopify/shopify_bulkupdate_laravel/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(116): Illuminate\Pipeline\Pipeline->Illuminate\Pipeline\{closure}(Object(App\Workflows\MyWorkflow))
#8 /Users/khanakia/D1/www/shopify/shopify_bulkupdate_laravel/vendor/laravel/framework/src/Illuminate/Bus/Dispatcher.php(132): Illuminate\Pipeline\Pipeline->then(Object(Closure))
#9 /Users/khanakia/D1/www/shopify/shopify_bulkupdate_laravel/vendor/laravel/framework/src/Illuminate/Queue/CallQueuedHandler.php(123): Illuminate\Bus\Dispatcher->dispatchNow(Object(App\Workflows\MyWorkflow), false)
#10 /Users/khanakia/D1/www/shopify/shopify_bulkupdate_laravel/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(141): Illuminate\Queue\CallQueuedHandler->Illuminate\Queue\{closure}(Object(App\Workflows\MyWorkflow))
#11 /Users/khanakia/D1/www/shopify/shopify_bulkupdate_laravel/vendor/laravel-workflow/laravel-workflow/src/Middleware/WithoutOverlappingMiddleware.php(56): Illuminate\Pipeline\Pipeline->Illuminate\Pipeline\{closure}(Object(App\Workflows\MyWorkflow))
#12 /Users/khanakia/D1/www/shopify/shopify_bulkupdate_laravel/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(180): Workflow\Middleware\WithoutOverlappingMiddleware->handle(Object(App\Workflows\MyWorkflow), Object(Closure))
#13 /Users/khanakia/D1/www/shopify/shopify_bulkupdate_laravel/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(116): Illuminate\Pipeline\Pipeline->Illuminate\Pipeline\{closure}(Object(App\Workflows\MyWorkflow))
#14 /Users/khanakia/D1/www/shopify/shopify_bulkupdate_laravel/vendor/laravel/framework/src/Illuminate/Queue/CallQueuedHandler.php(122): Illuminate\Pipeline\Pipeline->then(Object(Closure))
#15 /Users/khanakia/D1/www/shopify/shopify_bulkupdate_laravel/vendor/laravel/framework/src/Illuminate/Queue/CallQueuedHandler.php(70): Illuminate\Queue\CallQueuedHandler->dispatchThroughMiddleware(Object(Illuminate\Queue\Jobs\DatabaseJob), Object(App\Workflows\MyWorkflow))
#16 /Users/khanakia/D1/www/shopify/shopify_bulkupdate_laravel/vendor/laravel/framework/src/Illuminate/Queue/Jobs/Job.php(98): Illuminate\Queue\CallQueuedHandler->call(Object(Illuminate\Queue\Jobs\DatabaseJob), Array)
#17 /Users/khanakia/D1/www/shopify/shopify_bulkupdate_laravel/vendor/laravel/framework/src/Illuminate/Queue/Worker.php(439): Illuminate\Queue\Jobs\Job->fire()
#18 /Users/khanakia/D1/www/shopify/shopify_bulkupdate_laravel/vendor/laravel/framework/src/Illuminate/Queue/Worker.php(389): Illuminate\Queue\Worker->process('database', Object(Illuminate\Queue\Jobs\DatabaseJob), Object(Illuminate\Queue\WorkerOptions))
#19 /Users/khanakia/D1/www/shopify/shopify_bulkupdate_laravel/vendor/laravel/framework/src/Illuminate/Queue/Worker.php(176): Illuminate\Queue\Worker->runJob(Object(Illuminate\Queue\Jobs\DatabaseJob), 'database', Object(Illuminate\Queue\WorkerOptions))
#20 /Users/khanakia/D1/www/shopify/shopify_bulkupdate_laravel/vendor/laravel/framework/src/Illuminate/Queue/Console/WorkCommand.php(137): Illuminate\Queue\Worker->daemon('database', 'default', Object(Illuminate\Queue\WorkerOptions))
#21 /Users/khanakia/D1/www/shopify/shopify_bulkupdate_laravel/vendor/laravel/framework/src/Illuminate/Queue/Console/WorkCommand.php(120): Illuminate\Queue\Console\WorkCommand->runWorker('database', 'default')
#22 /Users/khanakia/D1/www/shopify/shopify_bulkupdate_laravel/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(36): Illuminate\Queue\Console\WorkCommand->handle()
#23 /Users/khanakia/D1/www/shopify/shopify_bulkupdate_laravel/vendor/laravel/framework/src/Illuminate/Container/Util.php(41): Illuminate\Container\BoundMethod::Illuminate\Container\{closure}()
#24 /Users/khanakia/D1/www/shopify/shopify_bulkupdate_laravel/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(93): Illuminate\Container\Util::unwrapIfClosure(Object(Closure))
#25 /Users/khanakia/D1/www/shopify/shopify_bulkupdate_laravel/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(35): Illuminate\Container\BoundMethod::callBoundMethod(Object(Illuminate\Foundation\Application), Array, Object(Closure))
#26 /Users/khanakia/D1/www/shopify/shopify_bulkupdate_laravel/vendor/laravel/framework/src/Illuminate/Container/Container.php(662): Illuminate\Container\BoundMethod::call(Object(Illuminate\Foundation\Application), Array, Array, NULL)
#27 /Users/khanakia/D1/www/shopify/shopify_bulkupdate_laravel/vendor/laravel/framework/src/Illuminate/Console/Command.php(211): Illuminate\Container\Container->call(Array)
#28 /Users/khanakia/D1/www/shopify/shopify_bulkupdate_laravel/vendor/symfony/console/Command/Command.php(326): Illuminate\Console\Command->execute(Object(Symfony\Component\Console\Input\ArgvInput), Object(Illuminate\Console\OutputStyle))
#29 /Users/khanakia/D1/www/shopify/shopify_bulkupdate_laravel/vendor/laravel/framework/src/Illuminate/Console/Command.php(180): Symfony\Component\Console\Command\Command->run(Object(Symfony\Component\Console\Input\ArgvInput), Object(Illuminate\Console\OutputStyle))
#30 /Users/khanakia/D1/www/shopify/shopify_bulkupdate_laravel/vendor/symfony/console/Application.php(1081): Illuminate\Console\Command->run(Object(Symfony\Component\Console\Input\ArgvInput), Object(Symfony\Component\Console\Output\ConsoleOutput))
#31 /Users/khanakia/D1/www/shopify/shopify_bulkupdate_laravel/vendor/symfony/console/Application.php(320): Symfony\Component\Console\Application->doRunCommand(Object(Illuminate\Queue\Console\WorkCommand), Object(Symfony\Component\Console\Input\ArgvInput), Object(Symfony\Component\Console\Output\ConsoleOutput))
#32 /Users/khanakia/D1/www/shopify/shopify_bulkupdate_laravel/vendor/symfony/console/Application.php(174): Symfony\Component\Console\Application->doRun(Object(Symfony\Component\Console\Input\ArgvInput), Object(Symfony\Component\Console\Output\ConsoleOutput))
#33 /Users/khanakia/D1/www/shopify/shopify_bulkupdate_laravel/vendor/laravel/framework/src/Illuminate/Foundation/Console/Kernel.php(201): Symfony\Component\Console\Application->run(Object(Symfony\Component\Console\Input\ArgvInput), Object(Symfony\Component\Console\Output\ConsoleOutput))
#34 /Users/khanakia/D1/www/shopify/shopify_bulkupdate_laravel/artisan(35): Illuminate\Foundation\Console\Kernel->handle(Object(Symfony\Component\Console\Input\ArgvInput), Object(Symfony\Component\Console\Output\ConsoleOutput))
#35 {main}
"}

</details>

Deprecations in `Opis\Closure\SerializableClosure`

  • Opis\Closure\SerializableClosure implements the Serializable interface, which is deprecated. Implement __serialize() and __unserialize() instead (or in addition, if support for old PHP versions is necessary) in /var/www/html/vendor/opis/closure/src/SerializableClosure.php on line 18

Signals not received due to different queue name prefix

Hi,

I have a webapp from which a user can start a workflow, which is then executed on another host with queue workers running under Supervisor.
A workflow can be paused if an issue occurs, and restarted after the issue is fixed using a signal that is sent from the same host running Supervisor and the worker, using a custom Laravel cli command.

The problem is that nothing happened after the signal was sent. After investigating the issue, it appeared that the messages was sent to different Redis queue names, due to different prefixes.

The Redis queue for workflows and activities is named <prefix>_database_queues:<name>.
For the webapp and the workers running with Supervisor, the <prefix> value is the slugified value of the APP_NAME option in the .env file, as expected. But when sending the signal from a cli command on the same host as the workers, the <prefix> value is different.
The messages are then sent to a different queue, and cannot be received by the workers running the workflow.

The attribute [code] either does not exist or was not retrieved for model

Hi! I got an error message in my logs when I try to view failed workflow in Waterline:

[2023-07-05 17:51:15] local.ERROR: The attribute [code] either does not exist or was not retrieved for model [Workflow\Models\StoredWorkflowException]. {"exception":"Illuminate\\Database\\Eloquent\\MissingAttributeException [code: 0]: The attribute [code] either does not exist or was not retrieved for model [Workflow\\Models\\StoredWorkflowException]. at /var/www/html/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php:474"}

And flow preview show infinite loader:

SCR-20230705-pwna

Workflow status doesn't changed to completed when contains signal methods

Hi,

Thanks for creating this simple yet powerful workflow package for Laravel community.

However, I follow the document to setup the Workflow, found the status won't change to "Completed" when contains a signal method.

The steps to re-produce the issue...

Workflow file

<?php

namespace App\Workflows\Demo;

use Workflow\ActivityStub;
use Workflow\SignalMethod;
use Workflow\Workflow;
use WOrkflow\WorkflowStub;

class DemoWorkflow extends Workflow
{
    private bool $isReady = false;

    #[SignalMethod]
    public function ready()
    {
        $this->isReady = true;
    }

    public function execute()
    {
        $resultOne = yield ActivityStub::make(StepOneActivity::class);

        yield WorkflowStub::await(fn () => $this->isReady);
//        yield WorkflowStub::timer(5);
//        $resultApprove = yield WorkflowStub::awaitWithTimeout('5 minutes', fn() => $this->approve());

        $resultTwo = yield ActivityStub::make(StepTwoActivity::class);

        return $resultOne . $resultTwo;
    }
}

Step 1 activity

<?php

namespace App\Workflows\Demo;

use Workflow\Activity;

class StepOneActivity extends Activity
{
    public function execute()
    {
        // Executing step 1
        return 'Step 1 completed';
    }
}

Step 2 activity

<?php

namespace App\Workflows\Demo;

use Workflow\Activity;

class StepTwoActivity extends Activity
{
    public function execute()
    {
        // Executing step 2
        return 'Step 2 completed';
    }
}

Controller

<?php

namespace App\Controllers;

use App\Http\Controllers\Controller;
use App\Workflows\Demo\DemoWorkflow;
use Workflow\WorkflowStub;

class WorkflowController extends Controller
{
    public function index()
    {
        $workflow = WorkflowStub::make(DemoWorkflow::class);
        $workflow->start();

        return $workflow->id();
    }

    public function ready(int $workflow_id)
    {
        $workflow = WorkflowStub::load($workflow_id);
        $workflow->ready();

        return true;
    }

    public function status(int $workflow_id)
    {
        $workflow = WorkflowStub::load($workflow_id);

        return $workflow->status();
    }
}

First I call index method to create the workflow, StepOneActivity executed, workflow pending wait for ready signal, next call ready method from controller to set the isReady property to true, Horizon shows the StepTwoActivity been executed, but from Waterline dashboard, the workflow still on running.

image
image

Here below is the screenshot of related records in database. Although the status not changed to completed, but the updated_at column is being updated.

image
image
image

StepOneActivity and StepTwoActivity are both executed, only the Workflow status not being updated to completed.

If comment the line

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

Then the workflow can changed to completed.
image

Please kindly advise if any steps missed, many thanks!

Workflow stuck. No more job in queue.

Sometime, the workflow stuck and not processing.
Please check this repo https://github.com/zsinryu/laravel-workflow-parallel

  1. Clone .env.example to .env
  2. ./vendor/bin/sail up
  3. Open 3 terminals, 2 for php artisan queue:work for simulate multi workers. And use the last terminal for test command php artisan workflow:test
  4. There are about 30% percent the error will happen. So try to run the test command few times.
image

Querying a workflow DOES advance child's execution

Hello! It says in docs that "Querying a workflow does not advance its execution, unlike signals." but it seems not to be true for child workflows.

image

I try to query parent workflow and everytime I do it it's child workflows are executed again and again (in my case it's yield ChildWorkflowStub::make(RunApprovementProcessWorkflow::class, $contract, $order)).

Parent workflow looks like this:

class ContractWorkflow extends Workflow
{
    private bool $readyToSign = false;
    public bool $approved = false;

    #[QueryMethod]
    public function getApproved(): bool
    {
        return $this->approved;
    }

    #[QueryMethod]
    public function getReadyToSign(): bool
    {
        return $this->readyToSign;
    }

    #[SignalMethod]
    public function contractReadyToSign(): void
    {
        $this->readyToSign = true;
    }

    public function execute(Contract $contract, User $author, array $approvers, array $signers, array $observers, $dueDate)
    {
        $contractApproval = yield ActivityStub::make(CreateContractApprovalActivity::class, $contract, $author, $dueDate);

        yield ActivityStub::make(CreateObserversActivity::class, $contractApproval, $observers);
        yield ActivityStub::make(CreateApproversActivity::class, $contractApproval, $approvers);
        yield ActivityStub::make(CreateSignersActivity::class, $contractApproval, $signers);


        foreach ($contract->getDistinctApproversOrders() as $order) {

            $approved = yield ChildWorkflowStub::make(RunApprovementProcessWorkflow::class, $contract, $order);
            if ($approved === TaskStatus::REJECTED) {
                yield ActivityStub::make(RunCancelProcessActivity::class, $contract, $contractApproval);
                return  -1;
            }
        }
      .......................
}

Proper way to pass arguments?

Hi @rmcdaniel

I was trying the package again and came to the issue of passing parameters to the workflow.
For example, I want to pass validated input from form-request to the workflow and action.

arguments

This isn't documented, and I needed to dig deeper. It looks like you pass arguments in the raw format (serialized). Even more, there are required parameters passed in case of activity (index, now, etc) which is a bit unintuitive from the DX point of view (I tried to type-hint all the stuff in the constructor first).

Maybe there's a way to avoid unserializing arguments manually? Or I'm approaching this wrong?


Errors in WithoutOverlappingMiddleware

Hi there,

We run laravel-workflow in production for all of our workflows.

Recently, we started receiving an error from WithoutOverlappingMiddleware.php.

The error is:

ErrorException: foreach() argument must be of type array|object, string given in /var/www/html/vendor/laravel-workflow/laravel-workflow/src/Middleware/WithoutOverlappingMiddleware.php:85

I can see that this foreach loop was added in one of the recent patch version commits.

The purpose of filing this ticket is three-fold:

  1. To bring to your attention that the change made has broken existing running workflows
  2. To try to understand the best way for us to resolve this issue. We're considering rolling back to the version before this update. However, there were signals that were lost. Is there a best approach to make sure those signals get run? (We're generating invoices with this)
  3. We strongly suggest the adoption of a standard commit message and versioning approach, such as this: https://www.conventionalcommits.org/en/v1.0.0/
  • We use this internally to remove human judgement from versioning. Just don't believe a change like this should be a patch version.
  • Of course, you may disagree (totally fine). If you are interested in adopting conventional commits however, we have a setup using Github Actions that automatically generates the correct tag based on the commit message that goes into the main branch. Would be more than happy to show how this was done!

Argument #1 ($storedWorkflow) must be of type Workflow\Models\StoredWorkflow, int given

Hello, we are attempting to migrate some of our business processes with Laravel Workflows, and while unit-testing the old APIs after turning them into a basic Workflow, this is the error I get.

[2023-04-25 11:30:50] testing.ERROR: Workflow\Workflow::__construct(): Argument #1 ($storedWorkflow) must be of type Workflow\Models\StoredWorkflow, int given, called in /<folder>/vendor/laravel/framework/src/Illuminate/Foundation/Bus/Dispatchable.php on line 19 {"exception":"[object] (TypeError(code: 0): Workflow\\Workflow::__construct(): Argument #1 ($storedWorkflow) must be of type Workflow\\Models\\StoredWorkflow, int given, called in /<folder>/vendor/laravel/framework/src/Illuminate/Foundation/Bus/Dispatchable.php on line 19 at /<folder>/vendor/laravel-workflow/laravel-workflow/src/Workflow.php:46)
[stacktrace]
#0 /<folder>/vendor/laravel/framework/src/Illuminate/Foundation/Bus/Dispatchable.php(19): Workflow\\Workflow->__construct()
#1 /<folder>/vendor/laravel-workflow/laravel-workflow/src/ActivityStub.php(34): Workflow\\Workflow::dispatch()
#2 /<folder>/app/Workflows/DeleteContactWorkflow.php(11): Workflow\\ActivityStub::make()
#3 [internal function]: App\\Workflows\\DeleteContactWorkflow->execute()
#4 /<folder>/vendor/laravel-workflow/laravel-workflow/src/Workflow.php(133): Generator->valid()
#5 /<folder>/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(36): Workflow\\Workflow->handle()
#6 /<folder>/vendor/laravel/framework/src/Illuminate/Container/Util.php(41): Illuminate\\Container\\BoundMethod::Illuminate\\Container\\{closure}()
#7 /<folder>/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(93): Illuminate\\Container\\Util::unwrapIfClosure()
#8 /<folder>/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(35): Illuminate\\Container\\BoundMethod::callBoundMethod()
#9 /<folder>/vendor/laravel/framework/src/Illuminate/Container/Container.php(662): Illuminate\\Container\\BoundMethod::call()
#10 /<folder>/vendor/laravel/framework/src/Illuminate/Bus/Dispatcher.php(128): Illuminate\\Container\\Container->call()
#11 /<folder>/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(141): Illuminate\\Bus\\Dispatcher->Illuminate\\Bus\\{closure}()
#12 /<folder>/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(116): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}()
#13 /<folder>/vendor/laravel/framework/src/Illuminate/Bus/Dispatcher.php(132): Illuminate\\Pipeline\\Pipeline->then()
#14 /<folder>/vendor/laravel/framework/src/Illuminate/Queue/CallQueuedHandler.php(123): Illuminate\\Bus\\Dispatcher->dispatchNow()
#15 /<folder>/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(141): Illuminate\\Queue\\CallQueuedHandler->Illuminate\\Queue\\{closure}()
#16 /<folder>/vendor/laravel-workflow/laravel-workflow/src/Middleware/WithoutOverlappingMiddleware.php(75): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}()
#17 /<folder>/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(180): Workflow\\Middleware\\WithoutOverlappingMiddleware->handle()
#18 /<folder>/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(116): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}()
#19 /<folder>/vendor/laravel/framework/src/Illuminate/Queue/CallQueuedHandler.php(122): Illuminate\\Pipeline\\Pipeline->then()
#20 /<folder>/vendor/laravel/framework/src/Illuminate/Queue/CallQueuedHandler.php(70): Illuminate\\Queue\\CallQueuedHandler->dispatchThroughMiddleware()
#21 /<folder>/vendor/laravel/framework/src/Illuminate/Queue/Jobs/Job.php(98): Illuminate\\Queue\\CallQueuedHandler->call()
#22 /<folder>/vendor/laravel/framework/src/Illuminate/Queue/SyncQueue.php(43): Illuminate\\Queue\\Jobs\\Job->fire()
#23 /<folder>/vendor/laravel/framework/src/Illuminate/Bus/Dispatcher.php(253): Illuminate\\Queue\\SyncQueue->push()
#24 /<folder>/vendor/laravel/framework/src/Illuminate/Bus/Dispatcher.php(229): Illuminate\\Bus\\Dispatcher->pushCommandToQueue()
#25 /<folder>/vendor/laravel/framework/src/Illuminate/Bus/Dispatcher.php(77): Illuminate\\Bus\\Dispatcher->dispatchToQueue()
#26 /<folder>/vendor/laravel/framework/src/Illuminate/Foundation/Bus/PendingDispatch.php(193): Illuminate\\Bus\\Dispatcher->dispatch()
#27 /<folder>/vendor/laravel/framework/src/Illuminate/Foundation/Bus/Dispatchable.php(19): Illuminate\\Foundation\\Bus\\PendingDispatch->__destruct()
#28 /<folder>/vendor/laravel-workflow/laravel-workflow/src/WorkflowStub.php(251): Workflow\\Workflow::dispatch()
#29 /<folder>/vendor/laravel-workflow/laravel-workflow/src/WorkflowStub.php(188): Workflow\\WorkflowStub->dispatch()
#30 /<folder>/app/Http/Controllers/Webhooks/Contacts.php(48): Workflow\\WorkflowStub->start()
#31 /<folder>/vendor/laravel/framework/src/Illuminate/Routing/Controller.php(54): App\\Http\\Controllers\\Webhooks\\Contacts->deleteContact()
#32 /<folder>/vendor/laravel/framework/src/Illuminate/Routing/ControllerDispatcher.php(43): Illuminate\\Routing\\Controller->callAction()
#33 /<folder>/vendor/laravel/framework/src/Illuminate/Routing/Route.php(259): Illuminate\\Routing\\ControllerDispatcher->dispatch()
#34 /<folder>/vendor/laravel/framework/src/Illuminate/Routing/Route.php(205): Illuminate\\Routing\\Route->runController()
#35 /<folder>/vendor/laravel/framework/src/Illuminate/Routing/Router.php(798): Illuminate\\Routing\\Route->run()
#36 /<folder>/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(141): Illuminate\\Routing\\Router->Illuminate\\Routing\\{closure}()
#37 /<folder>/vendor/laravel/framework/src/Illuminate/Routing/Middleware/SubstituteBindings.php(50): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}()
#38 /<folder>/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(180): Illuminate\\Routing\\Middleware\\SubstituteBindings->handle()
#39 /<folder>/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(116): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}()
#40 /<folder>/vendor/laravel/framework/src/Illuminate/Routing/Router.php(797): Illuminate\\Pipeline\\Pipeline->then()
#41 /<folder>/vendor/laravel/framework/src/Illuminate/Routing/Router.php(776): Illuminate\\Routing\\Router->runRouteWithinStack()
#42 /<folder>/vendor/laravel/framework/src/Illuminate/Routing/Router.php(740): Illuminate\\Routing\\Router->runRoute()
#43 /<folder>/vendor/laravel/framework/src/Illuminate/Routing/Router.php(729): Illuminate\\Routing\\Router->dispatchToRoute()
#44 /<folder>/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(200): Illuminate\\Routing\\Router->dispatch()
#45 /<folder>/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(141): Illuminate\\Foundation\\Http\\Kernel->Illuminate\\Foundation\\Http\\{closure}()
#46 /<folder>/vendor/laravel/vapor-core/src/Http/Middleware/ServeStaticAssets.php(21): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}()
#47 /<folder>/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(180): Laravel\\Vapor\\Http\\Middleware\\ServeStaticAssets->handle()
#48 /<folder>/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TransformsRequest.php(21): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}()
#49 /<folder>/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/ConvertEmptyStringsToNull.php(31): Illuminate\\Foundation\\Http\\Middleware\\TransformsRequest->handle()
#50 /<folder>/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(180): Illuminate\\Foundation\\Http\\Middleware\\ConvertEmptyStringsToNull->handle()
#51 /<folder>/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TransformsRequest.php(21): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}()
#52 /<folder>/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TrimStrings.php(40): Illuminate\\Foundation\\Http\\Middleware\\TransformsRequest->handle()
#53 /<folder>/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(180): Illuminate\\Foundation\\Http\\Middleware\\TrimStrings->handle()
#54 /<folder>/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/ValidatePostSize.php(27): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}()
#55 /<folder>/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(180): Illuminate\\Foundation\\Http\\Middleware\\ValidatePostSize->handle()
#56 /<folder>/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/PreventRequestsDuringMaintenance.php(86): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}()
#57 /<folder>/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(180): Illuminate\\Foundation\\Http\\Middleware\\PreventRequestsDuringMaintenance->handle()
#58 /<folder>/vendor/laravel/framework/src/Illuminate/Http/Middleware/HandleCors.php(49): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}()
#59 /<folder>/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(180): Illuminate\\Http\\Middleware\\HandleCors->handle()
#60 /<folder>/vendor/laravel/framework/src/Illuminate/Http/Middleware/TrustProxies.php(39): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}()
#61 /<folder>/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(180): Illuminate\\Http\\Middleware\\TrustProxies->handle()
#62 /<folder>/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(116): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}()
#63 /<folder>/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(175): Illuminate\\Pipeline\\Pipeline->then()
#64 /<folder>/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(144): Illuminate\\Foundation\\Http\\Kernel->sendRequestThroughRouter()
#65 /<folder>/vendor/laravel/framework/src/Illuminate/Foundation/Testing/Concerns/MakesHttpRequests.php(563): Illuminate\\Foundation\\Http\\Kernel->handle()
#66 /<folder>/vendor/laravel/framework/src/Illuminate/Foundation/Testing/Concerns/MakesHttpRequests.php(529): Illuminate\\Foundation\\Testing\\TestCase->call()
#67 /<folder>/vendor/laravel/framework/src/Illuminate/Foundation/Testing/Concerns/MakesHttpRequests.php(457): Illuminate\\Foundation\\Testing\\TestCase->json()
#68 /<folder>/tests/Api/Webhooks/ContactTest.php(17): Illuminate\\Foundation\\Testing\\TestCase->deleteJson()
#69 /<folder>/vendor/phpunit/phpunit/src/Framework/TestCase.php(1065): Tests\\Api\\Webhooks\\ContactTest->test_create_contact()
#70 /<folder>/vendor/laravel/framework/src/Illuminate/Foundation/Testing/TestCase.php(173): PHPUnit\\Framework\\TestCase->runTest()
#71 /<folder>/vendor/phpunit/phpunit/src/Framework/TestCase.php(632): Illuminate\\Foundation\\Testing\\TestCase->runTest()
#72 /<folder>/vendor/phpunit/phpunit/src/Framework/TestRunner.php(101): PHPUnit\\Framework\\TestCase->runBare()
#73 /<folder>/vendor/phpunit/phpunit/src/Framework/TestCase.php(468): PHPUnit\\Framework\\TestRunner->run()
#74 /<folder>/vendor/phpunit/phpunit/src/Framework/TestSuite.php(357): PHPUnit\\Framework\\TestCase->run()
#75 /<folder>/vendor/phpunit/phpunit/src/Framework/TestSuite.php(357): PHPUnit\\Framework\\TestSuite->run()
#76 /<folder>/vendor/phpunit/phpunit/src/TextUI/TestRunner.php(63): PHPUnit\\Framework\\TestSuite->run()
#77 /<folder>/vendor/phpunit/phpunit/src/TextUI/Application.php(168): PHPUnit\\TextUI\\TestRunner->run()
#78 /<folder>/vendor/phpunit/phpunit/phpunit(99): PHPUnit\\TextUI\\Application->run()
#79 {main}
"} 

The relevant activity and workflow are:

class DeleteContactActivity extends Workflow
{

    public function execute($email) 
    {
        ContactApi::contacts()->deleteContact($email);
        return true;
    }

}

...

class DeleteContactWorkflow extends \Workflow\Workflow
{
    public function execute($data) 
    {
        return yield ActivityStub::make(DeleteContactActivity::class, $data);
    }
}

...

public function deleteContact(string $email, ContactRequest $request)
{
    $workflow = WorkflowStub::make(DeleteContactWorkflow::class);
    $workflow->start($email);

    return ['job_id' => $workflow->id(), 'status' => $workflow->status()];
}

Workflows stuck in waiting when running multiple horizon processes

Hey there,

we have been using the package for a couple of months now on a relatively large production app (processed >600,000 flows so far). The flows work almost flawlessly, however, we noticed some strange behavior that we can't explain for now:

Setup:

  • We have various workflow classes with both sub-workflows and, obviously, activities
  • This was our production horizon config, important part is the maxProcesses => 10 for production env:
   'defaults' => [
        'supervisor-1' => [
            'connection' => 'redis',
            'queue' => ['default'],
            'balance' => 'auto',
            'autoScalingStrategy' => 'time',
            'maxProcesses' => 1,
            'maxTime' => 0,
            'maxJobs' => 0,
            'memory' => 128,
            'tries' => 1,
            'timeout' => 60,
            'nice' => 0,
        ],
    ],

    'environments' => [
        'production' => [
            'supervisor-1' => [
                'maxProcesses' => 10,
                'balanceMaxShift' => 1,
                'balanceCooldown' => 3,
            ],
        ],
    ],`

Observed behavior:

  • Every other day we have a workflow stuck in the waiting status
  • As far as I can tell, the workflows get stuck when one activity returns and tries to dispatch the parent workflow
  • It seems to happen with all workflows at different activities
  • The following exception is thrown, when the workflow gets stuck
Spatie\ModelStates\Exceptions\TransitionNotFound: Transition from `running` to `pending` on model `Workflow\Models\StoredWorkflow` was not found, did you forget to register it in `Workflow\Models\StoredWorkflow::registerStates()`?
#41 /vendor/spatie/laravel-model-states/src/Exceptions/TransitionNotFound.php(19): Spatie\ModelStates\Exceptions\TransitionNotFound::make
#40 /vendor/spatie/laravel-model-states/src/Exceptions/CouldNotPerformTransition.php(33): Spatie\ModelStates\Exceptions\CouldNotPerformTransition::notFound
#39 /vendor/spatie/laravel-model-states/src/State.php(158): Spatie\ModelStates\State::transitionTo
#38 /vendor/laravel-workflow/laravel-workflow/src/WorkflowStub.php(235): Workflow\WorkflowStub::dispatch
#37 /vendor/laravel-workflow/laravel-workflow/src/WorkflowStub.php(230): Workflow\WorkflowStub::next
#36 /vendor/laravel-workflow/laravel-workflow/src/Workflow.php(190): Workflow\Workflow::handle
#35 /vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(36): Illuminate\Container\BoundMethod::Illuminate\Container\{closure}
#34 /vendor/laravel/framework/src/Illuminate/Container/Util.php(41): Illuminate\Container\Util::unwrapIfClosure
#33 /vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(93): Illuminate\Container\BoundMethod::callBoundMethod
#32 /vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(35): Illuminate\Container\BoundMethod::call
#31 /vendor/laravel/framework/src/Illuminate/Container/Container.php(662): Illuminate\Container\Container::call
#30 /vendor/laravel/framework/src/Illuminate/Bus/Dispatcher.php(128): Illuminate\Bus\Dispatcher::Illuminate\Bus\{closure}
#29 /vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(141): Illuminate\Pipeline\Pipeline::Illuminate\Pipeline\{closure}
#28 /vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(116): Illuminate\Pipeline\Pipeline::then
#27 /vendor/laravel/framework/src/Illuminate/Bus/Dispatcher.php(132): Illuminate\Bus\Dispatcher::dispatchNow
#26 /vendor/laravel/framework/src/Illuminate/Queue/CallQueuedHandler.php(123): Illuminate\Queue\CallQueuedHandler::Illuminate\Queue\{closure}
#25 /vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(141): Illuminate\Pipeline\Pipeline::Illuminate\Pipeline\{closure}
#24 /vendor/laravel-workflow/laravel-workflow/src/Middleware/WithoutOverlappingMiddleware.php(75): Workflow\Middleware\WithoutOverlappingMiddleware::handle
#23 /vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(180): Illuminate\Pipeline\Pipeline::Illuminate\Pipeline\{closure}
#22 /vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(116): Illuminate\Pipeline\Pipeline::then
#21 /vendor/laravel/framework/src/Illuminate/Queue/CallQueuedHandler.php(122): Illuminate\Queue\CallQueuedHandler::dispatchThroughMiddleware
#20 /vendor/laravel/framework/src/Illuminate/Queue/CallQueuedHandler.php(70): Illuminate\Queue\CallQueuedHandler::call
#19 /vendor/laravel/framework/src/Illuminate/Queue/Jobs/Job.php(98): Illuminate\Queue\Jobs\Job::fire
#18 /vendor/laravel/framework/src/Illuminate/Queue/Worker.php(434): Illuminate\Queue\Worker::process
#17 /vendor/laravel/framework/src/Illuminate/Queue/Worker.php(384): Illuminate\Queue\Worker::runJob
#16 /vendor/laravel/framework/src/Illuminate/Queue/Worker.php(175): Illuminate\Queue\Worker::daemon
#15 /vendor/laravel/framework/src/Illuminate/Queue/Console/WorkCommand.php(136): Illuminate\Queue\Console\WorkCommand::runWorker
#14 /vendor/laravel/framework/src/Illuminate/Queue/Console/WorkCommand.php(119): Illuminate\Queue\Console\WorkCommand::handle
#13 /vendor/laravel/horizon/src/Console/WorkCommand.php(51): Laravel\Horizon\Console\WorkCommand::handle
#12 /vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(36): Illuminate\Container\BoundMethod::Illuminate\Container\{closure}
#11 /vendor/laravel/framework/src/Illuminate/Container/Util.php(41): Illuminate\Container\Util::unwrapIfClosure
#10 /vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(93): Illuminate\Container\BoundMethod::callBoundMethod
#9 /vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(35): Illuminate\Container\BoundMethod::call
#8 /vendor/laravel/framework/src/Illuminate/Container/Container.php(662): Illuminate\Container\Container::call
#7 /vendor/laravel/framework/src/Illuminate/Console/Command.php(194): Illuminate\Console\Command::execute
#6 /vendor/symfony/console/Command/Command.php(312): Symfony\Component\Console\Command\Command::run
#5 /vendor/laravel/framework/src/Illuminate/Console/Command.php(163): Illuminate\Console\Command::run
#4 /vendor/symfony/console/Application.php(1040): Symfony\Component\Console\Application::doRunCommand
#3 /vendor/symfony/console/Application.php(314): Symfony\Component\Console\Application::doRun
#2 /vendor/symfony/console/Application.php(168): Symfony\Component\Console\Application::run
#1 /vendor/laravel/framework/src/Illuminate/Foundation/Console/Kernel.php(200): Illuminate\Foundation\Console\Kernel::handle
#0 /artisan(35): null

Our Workaround:
We spent quite some time trying to figure out what the issue is, however, we could not identify a bug or anything. We had the gut feeling, that this might be something like a race condition where another queue worker picked up the workflow so it was already in state running. So we ended up reducing horizon's maxProcesses to just one, and the app has been running for almost a month without workflows getting stuck.

I am honestly not sure if this is an issue with the library or if we did something weird with our workflows. So first, my first question is: has something similar been observed already?

Default Queue Not Respected

Recently we deployed a feature, utilizing this package. We ran into an error specifying that we were unable to add a message to a queue named "default" on SQS. Our environment variable already has this value changed - the rest of our platform relies on this so we know that works.

The issue with the package is a little interesting, because it doesn't occur at all times, there's a specific procedure to reproduce. I'll explain the specific procedure that reproduced the problem. Keep in mind this all works in an environment where "default" is actually the queue name.

  1. In our Workflow, we yield an Activity, named ScheduleDelayedWorkflowSignal
  2. This Activity executes successfully (consumed via SQS). What it does is it dispatches a job, named CallWorkflowSignal. It dispatches it on the database queue connection, with a delay of 24 hours
  3. 24 hours later, CallWorkflowSignal executes (consumed from database queue). What it does is it loads a workflow via WorkflowStub::load, and then invokes a signal method
  4. It's during the invocation of this signal method that we run into the error. Not having dived into the code myself yet (super urgent for us to fix so I'm gonna dive in but also reporting here), my suspicion is that when a Workflow is loaded via load, the default queue configurations are not properly loaded in and/or respected.

You can see the code for CallWorkflowSignal here, it's pretty simple:

class CallWorkflowSignal implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public function __construct(public $workflowId, public string $methodName, public $arguments)
    {
    }

    public function handle() : void
    {
        $workflow = WorkflowStub::load($this->workflowId);
        $workflow->{$this->methodName}(...$this->arguments);
    }
}

ScheduleDelayedWorkflowSignal:

class ScheduleDelayedWorkflowSignal extends Activity
{
    public function execute(string $methodName, int $delayMinutes, ...$arguments)
    {
        CallWorkflowSignal::dispatch($this->workflowId(), $methodName, $arguments)
            ->onConnection('database')
            ->delay(now()->addMinutes($delayMinutes));
    }
}

And the code from the workflow itself (well, a method in the workflow that we yield from):

yield ActivityStub::make(
            ScheduleDelayedWorkflowSignal::class,
            $signalMethodName,
            $delayInMinutes,
            true
        );

        yield WorkflowStub::await(fn() => $this->{$signalPropertyName}); 

Race condition when starting a workflow within a database transaction

When you create and start a workflow from within a database transaction, there is a chance hat this workflow will never be executed.

Please see the following test: Naugrimm/laravel-workflow: failing test

Our current workaround is placing all workflows in a special queue that has 'after_commit' => true enabled in config/queue.php.

If this is your preferred way, I would like to request an addition to the documentation.

Another (potentially BC breaking change) would be to set the $afterCommit property in the Activity/Workflow classes, similar to the connection and queue properties.

What do you think?

Workflow loop on fail

We have a workflow with 5 actions in it, when an action fails at a certain point and exhausts the retries, if we run $workflow->resume() the workflow goes into a loop and the attempt counter gets up into the thousands until i manually stop it.

I can fix the issue by disabling the overlap middleware, but didn't dig into it anymore than that.

Has anyone else experienced this? I'll dig into it more if needed

image

Duplicate Exception

I use PostgreSQL as the database and PHP 8.2.

The PostgreSQL duplicate key exception differs from the MySQL SQL error, which leads to an exception in the code because it uses the case-sensitive str_contains function. See below:

    public function next($index, $now, $class, $result): void
    {
        try {
            $this->storedWorkflow->logs()
                ->create([
                    'index' => $index,
                    'now' => $now,
                    'class' => $class,
                    'result' => serialize($result),
                ]);
        } catch (QueryException $exception) {
            if (! str_contains($exception->getMessage(), 'Duplicate')) { // <---- HERE
                throw $exception;
            }
        }

        $this->dispatch();
    }

The SQL error from a PostgreSQL looks like this:

SQLSTATE[23505]: Unique violation: 7 ERROR:  duplicate key value violates unique constraint 

And here, as an example, the output of str_contains using the uppercase and lowercase variation.

> echo var_dump(str_contains('Unique violation: 7 ERROR:  duplicate key', 'Duplicate'));
bool(false)
> echo var_dump(str_contains('Unique violation: 7 ERROR:  duplicate key', 'duplicate'));
bool(true) 

Advice on laravel-workflow

  1. The documentation provided by the project should emphasize how the provided methods should be used and how they should be used without an execution plan.
  2. Is it necessary to use Finite for the flow of states? Does laravel-workflow not have its own state changes?
  3. There is no clear description in the document on how to obtain data in the approval flow. It is suggested to provide a method to obtain data in the workflow

Serialization of 'Closure' is not allowed

[2023-01-02 15:18:32] local.ERROR: Serialization of 'Closure' is not allowed {"exception":"[object] (Exception(code: 0): Serialization of 'Closure' is not allowed at /Users/rabol/code/web/wftest/vendor/laravel/serializable-closure/src/Serializers/Signed.php:68)

This looks like a regression from a previous issue with storing exception messages.

Support MongoDB

Hi team,

Is there any chance to support mongodb?
Can we remove strict type (on constructor and parameter) so we can overwrite model with custom model?

Thanks.

Error("Call to undefined method React\Promise\Promise::valid()")

Hi, Nice work with the package!

I am running Laravel using sail and Redis (queue).

When I try to run a workflow I get an error:

Error("Call to undefined method React\Promise\Promise::valid()")
in /var/www/html/vendor/laravel-workflow/laravel-workflow/src/Workflow.php (line 153)

            'execute'
        ));

        while ($this->coroutine->valid()) {
            $this->index = WorkflowStub::getContext()->index;

            $nextLog = $this->storedWorkflow->logs()

line 153 is: while ($this->coroutine->valid()) {

It is a very simple workflow:

$workflow = WorkflowStub::make(PostmarkEventworkflow::class);
$workflow->start($event);

PostmarkEventworkflow::class :

<?php

namespace App\Workflows;

use Workflow\ActivityStub;
use Workflow\Workflow;

class PostmarkEventworkflow extends Workflow
{
    public function execute()
    {
        // return yield ActivityStub::make(GetPostmarkEventActivity::class, $event);
        return ActivityStub::make(TestActivity::class);
    }
}

TestActivity:

<?php

namespace App\Workflows;

use Workflow\Activity;

class TestActivity extends Activity
{
    public function execute()
    {
        return 'hello';
    }
}

Memory leaks / high memory consumption

Environment

OS: Microsoft Windows 11 Home [10.0.22631 Build 22631]
Laravel version: 9.52.16
PHP version: 8.3.3
ext-redis version: 6.0.3-dev
Redis version: 5.0.14.1
laravel-workflow version: 1.0.23

Most likely, environment has no effect on the high memory usage observed when using the package.

Observed behavior

Using workers listening on a Redis queue connection, ever-increasing and high memory consumption is observed, as much as 7 times when compared with chained jobs and batches.

Steps to reproduce

See instructions in https://github.com/pelcro-inc/laravel-workflows-memory-leak

workflow_exceptions table missing?

Is there a reason that exceptions table is only built if workflow_timers doesn't exist? It's run after the timers_table so it will never exist unless it's a legacy table and if it was I would have expected it to drop if it does exist..

Or is it just a typo from copying the timers migration?

Duplicated query while running jobs

image

Hi guys. I have implemented just example of workflow and activity that explained in description,
But when i logging my request queries i see duplicated queries. Why is it like this?
@

Problems using timer from child workflow

Environment:
PHP: 8.2
Laravel 10

Timers within the child workflow do not run

It is not possible for me to run a timer within a child workflow.
Consider a parent workflow with the following code:

yield ChildWorkflowStub::make(LongTimerWorkflow::class);

And the child workflow with this code:

yield  WorkflowStub::timer(100);

An error of "Call to a member function copy() on string" will occur on line 41 of the Timer trait.
I can resolve this issue with the following snippet, however I'm struggling to understand why the Carbon date instance became a string in the first place:

 if ($timer === null) {
            if (is_string(self::$context->now)) {
                self::$context->now = \Carbon\Carbon::parse(self::$context->now);
            }

            $when = self::$context->now->copy()
                ->addSeconds($seconds);

Timers within the child workflow do not respect the queue connection specified

I was testing out behaviour of having my parent workflow use the default queue driver (redis), and the child workflow using the database queue driver. Upon monitoring these queues, I noticed that while the child workflow starts up in the database queue, any timer it creates (only managed to create a timer with my hotfix above) uses the queue connection of the parent - redis in this case.

Why would I do this? We're using SQS in production, it's non-trivial to switchover and/or consume from redis, however we want workflows that can wait 24 hours. My workaround for this was to have a child workflow use a database queue driver (which has a worker already) and a timer in order to provide this mechanism. If there's a separate way to accomplish this, would appreciate any tips.

workflow run on browser

i have install laravel-workflow package on laravel 10 ,.after how can i run workflow on the browser

Workflow stuck at waiting status

Thanks for your great work.
My issue is that I can't seem to do a simple successful workflow. All my workflows get stuck at waiting status. I'm creating a very simple order workflow:

class OrderWorkflow extends Workflow
{
    public function execute($order) 
    {
        $charge = yield ActivityStub::make(ChargeCardActivity::class, $order);
        
        $ship = yield ActivityStub::make(ShipOrderActivity::class, $order);
        
        return $ship;
    }
}

Activities

class ChargeCardActivity extends Activity
{
    public function execute($order)
    {
        // Charge the card for the order total...
        return true;
    }
}
class ShipOrderActivity extends Activity 
{
    public function execute($order)
    {
        // Ship the order...
        return true;
    }
}

And I'm starting the workflow after I store the order in the OrderController

    public function store(Request $request)
    {
        $order = Order::create($request->all());

        // Start the OrderWorkflow, passing the order
        WorkflowStub::make(OrderWorkflow::class)->start($order);
        return redirect()->route('orders.show', $order->id);
    }

P.S. Could you provide a folder in your repo for examples of workflow projects? that would much appreciated.

Value object serialization

I've tried to wrap a complex process into the workflow. The package indeed has a hard time restoring activities with DTO, Value Objects, or Enums inside.

It's of course possible to create all the code using only primitive types, but in enterprise scenarios that would be a massive productivity killer. It would be nice to make it work at least as native jobs as they can hold objects with no problem even though that is not a best practice for performance.

Example stacktrace of failing activity (Eloquent Enum Cast):

Cannot instantiate enum App\Enums\TransactionType {"exception":"[object] (Error(code: 0): Cannot instantiate enum App\\Enums\\TransactionType at /var/www/html/vendor/opis/closure/src/SerializableClosure.php:411)
[stacktrace]
#0 /var/www/html/vendor/opis/closure/src/SerializableClosure.php(411): ReflectionClass->newInstanceWithoutConstructor()
#1 /var/www/html/vendor/opis/closure/src/SerializableClosure.php(427): Opis\\Closure\\SerializableClosure::wrapClosures()
#2 /var/www/html/vendor/opis/closure/src/SerializableClosure.php(427): Opis\\Closure\\SerializableClosure::wrapClosures()
#3 /var/www/html/vendor/opis/closure/functions.php(19): Opis\\Closure\\SerializableClosure::wrapClosures()
#4 /var/www/html/vendor/laravel-workflow/laravel-workflow/src/Serializers/Y.php(45): Opis\\Closure\\serialize()
#5 /var/www/html/vendor/laravel-workflow/laravel-workflow/src/WorkflowStub.php(200): Workflow\\Serializers\\Y::serialize()
#6 /var/www/html/vendor/laravel-workflow/laravel-workflow/src/Middleware/WorkflowMiddleware.php(15): Workflow\\WorkflowStub->next()
#7 /var/www/html/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(180): Workflow\\Middleware\\WorkflowMiddleware->handle()
#8 /var/www/html/vendor/laravel-workflow/laravel-workflow/src/Middleware/WithoutOverlappingMiddleware.php(75): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}()
#9 /var/www/html/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(180): Workflow\\Middleware\\WithoutOverlappingMiddleware->handle()
#10 /var/www/html/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(116): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}()
#11 /var/www/html/vendor/laravel/framework/src/Illuminate/Queue/CallQueuedHandler.php(126): Illuminate\\Pipeline\\Pipeline->then()
#12 /var/www/html/vendor/laravel/framework/src/Illuminate/Queue/CallQueuedHandler.php(70): Illuminate\\Queue\\CallQueuedHandler->dispatchThroughMiddleware()
#13 /var/www/html/vendor/laravel/framework/src/Illuminate/Queue/Jobs/Job.php(98): Illuminate\\Queue\\CallQueuedHandler->call()
#14 /var/www/html/vendor/laravel/framework/src/Illuminate/Queue/Worker.php(425): Illuminate\\Queue\\Jobs\\Job->fire()
#15 /var/www/html/vendor/laravel/framework/src/Illuminate/Queue/Worker.php(375): Illuminate\\Queue\\Worker->process()
#16 /var/www/html/vendor/laravel/framework/src/Illuminate/Queue/Worker.php(173): Illuminate\\Queue\\Worker->runJob()
#17 /var/www/html/vendor/laravel/framework/src/Illuminate/Queue/Console/WorkCommand.php(147): Illuminate\\Queue\\Worker->daemon()
#18 /var/www/html/vendor/laravel/framework/src/Illuminate/Queue/Console/WorkCommand.php(130): Illuminate\\Queue\\Console\\WorkCommand->runWorker()
#19 /var/www/html/vendor/laravel/horizon/src/Console/WorkCommand.php(51): Illuminate\\Queue\\Console\\WorkCommand->handle()

No publishable resources for tag [migrations].

Hi

in a clean Laravel 10 app, with Laravel Breeze installed this command returns No publishable resources for tag [migrations].

php artisan vendor:publish --provider="Workflow\Providers\WorkflowServiceProvider" --tag="migrations"

Invalid default value for 'created_at'

hi richard,

found this:

image

SQLSTATE[42000]: Syntax error or access violation: 1067 Invalid default value for 'created_at' (SQL: create table `workflow_logs` (`id` bigint unsigned not null auto_increment primary key, `stored_workflow_id` bigint unsigned not null, `index` bigint unsigned not null, `now` timestamp not null, `class` text not null, `result` text null, `created_at` timestamp(6) not null) default character set utf8mb4 collate 'utf8mb4_unicode_ci')

version: 0.0.10
SQL server: 10.7.4-MariaDB-1:10.7.4+maria~focal

Serialization of models returned from Activities

Hi.

I have a question regarding serialization of results (and arguments) of Activites.

Right now models returned from Activities are serialized to Model Identifiers. While at first it seems as a proper solution, that reduces amount of data stored, it brings one major problem.

Unfortunatelly, it means that any model returned from Activity cannot be deleted later in time or Workflow will throw an error during unserialization. For example, such Activities cannot be viewed in Waterline.

After quick look into the code it seems that all serializations are made by the same Y class. But serialized Workflow arguments/results are serialized fully (I don't know if thats proper term), but Activity arguments/results are serialized to Model Identifiers.

Is this something that can be changed or maybe my understanding of Workflows is incorrect and I should write Activities in different way?

Thanks in advance for any help.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.