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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# Changelog

## 2.11.3 - 2026-10-23

- Refuse dependency injection that would silently discard recorded activity
input before queued or local activity code runs. Record a typed attempt
failure and preserve the authored retry policy, original input and identity.
- Record activity entry dependency-resolution failures through the normal
outcome path instead of leaving the attempt leased without an outcome.

## 2.11.2 - 2026-10-23

- Deliver unpinned activity tasks to eligible versioned workers during rolling
Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@
"dev-main": "2.0.x-dev"
},
"durable-workflow": {
"product-train": "2.11.2",
"product-train": "2.11.3",
"laravel-embedded-upgrade-contract": "resources/laravel-embedded-upgrade-contract.json",
"laravel-dependency-security-policy": "resources/laravel-dependency-security-policy.json"
},
Expand Down
19 changes: 19 additions & 0 deletions src/V2/Exceptions/ActivityEntryArgumentsMismatchException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

declare(strict_types=1);

namespace Workflow\V2\Exceptions;

use LogicException;

final class ActivityEntryArgumentsMismatchException extends LogicException
{
public function __construct(string $activityType, string $entryMethod)
{
parent::__construct(sprintf(
'Activity [%s] entry [%s] cannot bind its recorded arguments: recorded_argument_would_be_discarded. Restore compatible activity code; this attempt follows the recorded retry policy.',
$activityType,
$entryMethod,
));
}
}
12 changes: 9 additions & 3 deletions src/V2/Jobs/RunActivityTask.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use Throwable;
use Workflow\V2\Models\ActivityExecution;
use Workflow\V2\Models\WorkflowTask;
use Workflow\V2\Support\ActivityEntryArguments;
use Workflow\V2\Support\ActivityOutcomeRecorder;
use Workflow\V2\Support\ActivityRetryPolicy;
use Workflow\V2\Support\ActivityTaskClaim;
Expand Down Expand Up @@ -122,17 +123,22 @@ private function executeMaintainedClaim(ActivityTaskClaim $claim, ?EmbeddedActiv
$activityClass = TypeRegistry::resolveActivityClass($execution->activity_class, $execution->activity_type);
$activity = RuntimeObjectFactory::activity($activityClass, $execution, $execution->run, $this->taskId);
$entryMethod = EntryMethod::forActivity($activity);
$arguments = $activity->resolveMethodDependencies($execution->activityArguments(), $entryMethod);
$activityArguments = $execution->activityArguments();

$result = null;
$throwable = null;
try {
$arguments = ActivityEntryArguments::resolve($activity, $activityArguments, $entryMethod);
} catch (Throwable $error) {
$arguments = [];
$throwable = $error;
}

if (WorkflowStub::faked()) {
WorkflowStub::recordDispatched($execution->activity_class, $activityArguments);
}

if (WorkflowStub::hasMock($execution->activity_class)) {
if ($throwable === null && WorkflowStub::hasMock($execution->activity_class)) {
$mocked = WorkflowStub::mockedResult(
$execution->activity_class,
new ActivityFakeContext(
Expand All @@ -147,7 +153,7 @@ private function executeMaintainedClaim(ActivityTaskClaim $claim, ?EmbeddedActiv

$result = $mocked['result'];
$throwable = $mocked['throwable'];
} else {
} elseif ($throwable === null) {
try {
$result = $activity->{$entryMethod->getName()}(...$arguments);
} catch (Throwable $error) {
Expand Down
31 changes: 31 additions & 0 deletions src/V2/Support/ActivityEntryArguments.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

declare(strict_types=1);

namespace Workflow\V2\Support;

use ReflectionMethod;
use Workflow\V2\Activity;
use Workflow\V2\Exceptions\ActivityEntryArgumentsMismatchException;

/** @internal Preserves recorded input across method dependency injection. */
final class ActivityEntryArguments
{
public static function resolve(Activity $activity, array $recorded, ReflectionMethod $entry): array
{
$resolved = $activity->resolveMethodDependencies($recorded, $entry);

// A new injected value must not push recorded input past the last
// parameter, where PHP would silently discard it. Existing variadic
// handlers can consume those values and keep their ordinary behavior.
if (! $entry->isVariadic() && count($resolved) > $entry->getNumberOfParameters()
&& count($resolved) > count($recorded)) {
throw new ActivityEntryArgumentsMismatchException(
$activity->execution->activity_type ?? $activity::class,
$entry->getName(),
);
}

return $resolved;
}
}
12 changes: 8 additions & 4 deletions src/V2/Support/LocalActivityExecutor.php
Original file line number Diff line number Diff line change
Expand Up @@ -389,16 +389,20 @@ private function invokeCallback(WorkflowRun $run, WorkflowTask $task, ActivityEx
$activity = RuntimeObjectFactory::activity($activityClass, $execution, $run, $task->id);
$entryMethod = EntryMethod::forActivity($activity);
$activityArguments = $execution->activityArguments();
$arguments = $activity->resolveMethodDependencies($activityArguments, $entryMethod);

$result = null;
$throwable = null;
try {
$arguments = ActivityEntryArguments::resolve($activity, $activityArguments, $entryMethod);
} catch (Throwable $error) {
$arguments = [];
$throwable = $error;
}

if (WorkflowStub::faked()) {
WorkflowStub::recordDispatched($execution->activity_class, $activityArguments);
}

if (WorkflowStub::hasMock($execution->activity_class)) {
if ($throwable === null && WorkflowStub::hasMock($execution->activity_class)) {
$mocked = WorkflowStub::mockedResult(
$execution->activity_class,
new ActivityFakeContext(
Expand All @@ -413,7 +417,7 @@ private function invokeCallback(WorkflowRun $run, WorkflowTask $task, ActivityEx

$result = $mocked['result'];
$throwable = $mocked['throwable'];
} else {
} elseif ($throwable === null) {
try {
$result = $activity->{$entryMethod->getName()}(...$arguments);
} catch (Throwable $error) {
Expand Down
Loading
Loading