Skip to content
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
3 changes: 1 addition & 2 deletions src/V2/Models/WorkflowRun.php
Original file line number Diff line number Diff line change
Expand Up @@ -217,8 +217,7 @@ public function waits(): HasMany
ConfiguredV2Models::resolve('run_wait_model', WorkflowRunWait::class),
'workflow_run_id',
)
->orderBy('position')
->orderBy('wait_id');
->inDisplayOrder();
}

public function timelineEntries(): HasMany
Expand Down
61 changes: 61 additions & 0 deletions src/V2/Models/WorkflowRunWait.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace Workflow\V2\Models;

use Carbon\CarbonInterface;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Support\Carbon;
Expand Down Expand Up @@ -38,6 +39,53 @@
'resolved_at' => UtcScheduleTimestamp::class,
];

/**
* @param string $key
*/
public function originalIsEquivalent($key): bool

Check warning on line 45 in src/V2/Models/WorkflowRunWait.php

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

This method has 5 returns, which is more than the 3 allowed.

See more on https://sonarcloud.io/project/issues?id=durable-workflow_workflow&issues=AaF4T9VFVHLR8MI8gtFh&open=AaF4T9VFVHLR8MI8gtFh&pullRequest=941
{
if (parent::originalIsEquivalent($key)) {
return true;
}

if (! array_key_exists($key, $this->original)) {
return false;
}

if ($key === 'payload' && ($this->getCasts()[$key] ?? null) === 'array') {
// MySQL normalizes JSON object key order. Lists and scalar types
// still carry meaning, so compare those without coercion.
return self::canonicalPayload($this->getAttribute($key))
=== self::canonicalPayload($this->getOriginal($key));
}

if (in_array($key, ['opened_at', 'deadline_at', 'resolved_at'], true)
&& ($this->getCasts()[$key] ?? null) === UtcScheduleTimestamp::class) {
$current = $this->getAttribute($key);
$original = $this->getOriginal($key);

// PostgreSQL can return a zero-fraction timestamp without its
// trailing digits. Its instant has not changed.
return $current instanceof CarbonInterface
&& $original instanceof CarbonInterface
&& $current->equalTo($original);
}

return false;
}

public function scopeInDisplayOrder(Builder $query): Builder
{
$status = $query->getQuery()
->getGrammar()
->wrap($this->qualifyColumn('status'));

return $query->orderByRaw(
"CASE {$status} WHEN 'open' THEN 0 WHEN 'resolved' THEN 1 WHEN 'cancelled' THEN 2 ELSE 99 END",
)->orderBy($this->qualifyColumn('position'))
->orderBy($this->qualifyColumn('wait_id'));
}

public function run(): BelongsTo
{
return $this->belongsTo(
Expand Down Expand Up @@ -93,4 +141,17 @@
? Carbon::parse($value)
: null;
}

private static function canonicalPayload(mixed $value): mixed
{
if (! is_array($value)) {
return $value;
}

if (! array_is_list($value)) {
ksort($value);
}

return array_map(self::canonicalPayload(...), $value);
}
}
35 changes: 32 additions & 3 deletions src/V2/Support/RunWaitProjector.php
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ public static function project(WorkflowRun $run, ?array $waits = null): array
$seen = [];
$projected = [];
$existing = $waitModel::query()->where('workflow_run_id', $run->id)->get()->keyBy('id');
$positions = self::chronologicalPositions($waits);

foreach (array_values($waits) as $position => $wait) {
$waitId = self::waitId($wait, $position);
Expand All @@ -161,7 +162,7 @@ public static function project(WorkflowRun $run, ?array $waits = null): array
'workflow_run_id' => $run->id,
'workflow_instance_id' => $run->workflow_instance_id,
'wait_id' => $waitId,
'position' => $position,
'position' => $positions[$waitId],
'kind' => self::stringValue($wait['kind'] ?? null) ?? 'unknown',
'sequence' => self::intValue($wait['sequence'] ?? null),
'status' => self::stringValue($wait['status'] ?? null) ?? 'unknown',
Expand Down Expand Up @@ -296,13 +297,41 @@ private static function projectedRows(WorkflowRun $run): EloquentCollection
/** @var EloquentCollection<int, WorkflowRunWait> $waits */
$waits = $waitModel::query()
->where('workflow_run_id', $run->id)
->orderBy('position')
->orderBy('wait_id')
->inDisplayOrder()
->get();

return $waits;
}

/**
* Keep stored order independent of status. Otherwise closing one open
* wait and opening the next rewrites every retained row twice.
*
* @param list<array<string, mixed>> $waits
* @return array<string, int>
*/
private static function chronologicalPositions(array $waits): array
{
$ordered = [];

foreach (array_values($waits) as $position => $wait) {
$ordered[] = [
'id' => self::waitId($wait, $position),
'opened_at' => self::timestamp($wait['opened_at'] ?? null)?->getTimestampMs() ?? PHP_INT_MAX,
];
}

usort($ordered, static fn (array $left, array $right): int =>
($left['opened_at'] <=> $right['opened_at']) ?: ($left['id'] <=> $right['id']));
$positions = [];

foreach ($ordered as $position => $wait) {
$positions[$wait['id']] = $position;
}

return $positions;
}

/**
* @param EloquentCollection<int, WorkflowRunWait> $projected
* @param list<array<string, mixed>> $canonical
Expand Down
24 changes: 24 additions & 0 deletions tests/Fixtures/V2/TestRetainedSignalWritesWorkflow.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

declare(strict_types=1);

namespace Tests\Fixtures\V2;

use Workflow\V2\Attributes\Signal;
use function Workflow\V2\signal;

use Workflow\V2\Workflow;

#[Signal('advance')]
final class TestRetainedSignalWritesWorkflow extends Workflow
{
public function handle(int $count): array
{
$values = [];
for ($index = 0; $index < $count; $index++) {
$values[] = signal('advance');
}

return $values;
}
}
Loading
Loading