Skip to content

Add signals #1

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

Merged
merged 8 commits into from
Oct 22, 2022
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
36 changes: 35 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ php artisan vendor:publish --provider="Workflow\Providers\WorkflowServiceProvide

## Requirements

You can use any queue driver that Laravel supports but this is heavily tested against Redis.
You can use any queue driver that Laravel supports but this is heavily tested against Redis. Your cache driver must support locks. (Read: [Laravel Queues](https://laravel.com/docs/9.x/queues#unique-jobs))

## Usage

Expand Down Expand Up @@ -49,6 +49,40 @@ $workflow->output();
=> 'activity'
```

## Signals

Using `WorkflowStub::await()` along with signal methods allows a workflow to wait for an external event.

```
class MyWorkflow extends Workflow
{
private bool $isReady = false;

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

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

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

$otherResult = yield ActivityStub::make(MyOtherActivity::class);

return $result . $otherResult;
}
}
```

The workflow will reach the call to `WorkflowStub::await()` and then hibernate until some external code signals the workflow like this.

```
$workflow->ready();
```

## Failed Workflows

If a workflow fails or crashes at any point then it can be resumed from that point. Any activities that were successfully completed during the previous execution of the workflow will not be ran again.
Expand Down
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@
}
],
"require": {
"spatie/laravel-model-states": "^2.1"
"spatie/laravel-model-states": "^2.1",
"react/promise": "^2.9"
},
"require-dev": {
"orchestra/testbench": "^7.1"
Expand Down
8 changes: 7 additions & 1 deletion src/Activity.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use BadMethodCallException;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
Expand All @@ -13,7 +14,7 @@
use Workflow\Middleware\WorkflowMiddleware;
use Workflow\Models\StoredWorkflow;

class Activity implements ShouldBeEncrypted, ShouldQueue
class Activity implements ShouldBeEncrypted, ShouldQueue, ShouldBeUnique
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

Expand All @@ -34,6 +35,11 @@ public function __construct(int $index, StoredWorkflow $model, ...$arguments)
$this->arguments = $arguments;
}

public function uniqueId()
{
return $this->model->id;
}

public function handle()
{
if (! method_exists($this, 'execute')) {
Expand Down
5 changes: 5 additions & 0 deletions src/Models/StoredWorkflow.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,9 @@ public function logs()
{
return $this->hasMany(StoredWorkflowLog::class);
}

public function signals()
{
return $this->hasMany(StoredWorkflowSignal::class);
}
}
17 changes: 17 additions & 0 deletions src/Models/StoredWorkflowSignal.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

namespace Workflow\Models;

use Illuminate\Database\Eloquent\Model;

class StoredWorkflowSignal extends Model
{
protected $table = 'workflow_signals';

protected $guarded = [];

public function workflow()
{
return $this->belongsTo(StoredWorkflow::class);
}
}
40 changes: 40 additions & 0 deletions src/Signal.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php

namespace Workflow;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Workflow\Models\StoredWorkflow;

class Signal implements ShouldBeEncrypted, ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

public $tries = 3;

public $maxExceptions = 3;

public $model;

public function __construct(StoredWorkflow $model)
{
$this->model = $model;
}

public function handle()
{
$workflow = $this->model->toWorkflow();

if ($workflow->running()) {
try {
$workflow->resume();
} catch (\Spatie\ModelStates\Exceptions\TransitionNotFound) {
$this->release();
}
}
}
}
9 changes: 9 additions & 0 deletions src/SignalMethod.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

namespace Workflow;

use Spiral\Attributes\NamedArgumentConstructorAttribute;

final class SignalMethod implements NamedArgumentConstructorAttribute
{
}
65 changes: 56 additions & 9 deletions src/Workflow.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,10 @@
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use React\Promise\PromiseInterface;
use Throwable;
use Workflow\Exceptions\WorkflowFailedException;
use Workflow\Models\StoredWorkflow;
use Workflow\States\WorkflowCompletedStatus;
use Workflow\States\WorkflowFailedStatus;
use Workflow\States\WorkflowRunningStatus;
use Workflow\States\WorkflowWaitingStatus;

Expand Down Expand Up @@ -50,24 +49,72 @@ public function handle()
{
$this->model->status->transitionTo(WorkflowRunningStatus::class);

$log = $this->model->logs()->whereIndex($this->index)->first();

$this->model
->signals()
->when($log, function($query, $log) {
$query->where('created_at', '<=', $log->created_at);
})
->each(function ($signal) {
$this->{$signal->method}(...unserialize($signal->arguments));
});

$this->coroutine = $this->execute(...$this->arguments);

while ($this->coroutine->valid()) {
$index = $this->index++;
$nextLog = $this->model->logs()->whereIndex($this->index + 1)->first();

$this->model
->signals()
->when($nextLog, function($query, $nextLog) {
$query->where('created_at', '<=', $nextLog->created_at);
})
->when($log, function($query, $log) {
$query->where('created_at', '>', $log->created_at);
})
->each(function ($signal) {
$this->{$signal->method}(...unserialize($signal->arguments));
});

$current = $this->coroutine->current();

$log = $this->model->logs()->whereIndex($index)->first();
if ($current instanceof PromiseInterface) {
$resolved = false;

$current->then(function ($value) use (&$resolved) {
$resolved = true;

$this->model->logs()->create([
'index' => $this->index,
'result' => serialize(true),
]);

$log = $this->model->logs()->whereIndex($this->index)->first();

if ($log) {
$this->coroutine->send(unserialize($log->result));
$this->coroutine->send(unserialize($log->result));
});

if (!$resolved) {
$this->model->status->transitionTo(WorkflowWaitingStatus::class);

return;
}
} else {
$this->model->status->transitionTo(WorkflowWaitingStatus::class);
$log = $this->model->logs()->whereIndex($this->index)->first();

$current->activity()::dispatch($index, $this->model, ...$current->arguments());
if ($log) {
$this->coroutine->send(unserialize($log->result));
} else {
$this->model->status->transitionTo(WorkflowWaitingStatus::class);

return;
$current->activity()::dispatch($this->index, $this->model, ...$current->arguments());

return;
}
}

$this->index++;
}

$this->model->output = serialize($this->coroutine->getReturn());
Expand Down
42 changes: 40 additions & 2 deletions src/WorkflowStub.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@

namespace Workflow;

use React\Promise\Deferred;
use React\Promise\PromiseInterface;
use ReflectionClass;
use Workflow\Models\StoredWorkflow;

use Workflow\States\WorkflowCompletedStatus;
use Workflow\States\WorkflowFailedStatus;
use Workflow\States\WorkflowPendingStatus;
use function React\Promise\resolve;

class WorkflowStub
{
Expand Down Expand Up @@ -36,6 +39,19 @@ public static function fromStoredWorkflow(StoredWorkflow $model)
return new static($model);
}

public static function await($condition): PromiseInterface
{
$result = $condition();

if ($result === true) {
return resolve(true);
}

$deferred = new Deferred();

return $deferred->promise();
}

public function id()
{
return $this->model->id;
Expand All @@ -48,7 +64,7 @@ public function output()

public function running()
{
return ! in_array($this->status(), [
return !in_array($this->status(), [
WorkflowCompletedStatus::class,
WorkflowFailedStatus::class,
]);
Expand Down Expand Up @@ -110,4 +126,26 @@ private function dispatch()

$this->model->class::dispatch($this->model, ...unserialize($this->model->arguments));
}

public function __call($method, $arguments)
{
if (collect((new ReflectionClass($this->model->class))->getMethods())
->filter(function ($method) {
return collect($method->getAttributes())
->contains(function ($attribute) {
return $attribute->getName() === SignalMethod::class;
});
})
->map(function ($method) {
return $method->getName();
})->contains($method)
) {
$this->model->signals()->create([
'method' => $method,
'arguments' => serialize($arguments),
]);

Signal::dispatch($this->model);
}
}
}
38 changes: 38 additions & 0 deletions src/migrations/2022_01_01_000002_create_workflow_signals_table.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateWorkflowSignalsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('workflow_signals', function (Blueprint $table) {
$table->id('id');
$table->foreignId('stored_workflow_id')->index();
$table->text('method');
$table->text('arguments')->nullable();
$table->timestamps();

$table->index(['stored_workflow_id', 'created_at']);

$table->foreign('stored_workflow_id')->references('id')->on('workflows')->onDelete('cascade');
});
}

/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('workflow_signals');
}
}
Loading
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy