Warning
RoadRunner 2025.1.3+ is required.
Abandoned Child Workflow Cancellation
Added a new feature flag FeatureFlags::$cancelAbandonedChildWorkflows to control the cancellation behavior of abandoned Child Workflows.
Previously, when a parent workflow was canceled, all child workflows would be canceled, including those with ParentClosePolicy::Abandon.
This behavior was incorrect - abandoned child workflows should continue running independently when their parent is canceled.
# worker.php
use Temporal\Worker\FeatureFlags;
// Fixed behavior (does NOT cancel abandoned children) - recommended
FeatureFlags::$cancelAbandonedChildWorkflows = false;
// Default behavior (cancels abandoned children - matches previous SDK versions)
FeatureFlags::$cancelAbandonedChildWorkflows = true;Warning
When setting $cancelAbandonedChildWorkflows = false:
- If you start an abandoned child workflow in the main workflow scope, it may miss the cancellation signal if you await only on the child workflow. Use
Promise::race()with a timer to properly handle cancellation. - If you start an abandoned child workflow in an async scope that is later canceled, the child workflow will not be affected by the scope cancellation.
- You can still cancel abandoned child workflows manually by calling
WorkflowStubInterface::cancel().
New Promises
The PHP SDK now supports React Promise v3.
To make this work correctly in the Workflow Worker environment,
the promises have been forked and improved in the internal/promise package.
The fork addresses critical issues for long-running Workflow Workers:
made rejection handler reusable (a v3 feature),
removed exit(255) calls from rejection handling that would terminate the worker process,
added declare(strict_types=1) throughout, and improved type annotations for better static analysis support.
A key improvement is the @yield annotation added to PromiseInterface,
which enables proper type inference when using promises with generators in Workflows.
This annotation is recognized by IDEs (PHPStorm) and static analysis tools (Psalm), significantly improving DX:
interface SomeActivity {
/**
* @return \React\Promise\PromiseInterface<ResultDto>
*/
public function doSomething(int $value): ResultDto;
}
final class Workflow {
public function handle(): \Generator
{
$activity = \Temporal\Workflow::newActivityStub(SomeActivity::class);
$result = yield $activity->doSomething(42); // IDE and Psalm infer $result as ResultDto
}
}The SDK supports both React Promise v2 and v3 - the version used depends on what you require in your composer.json.
Warning
React Promise v3 includes optimizations that may slightly change promise resolution order compared to v2. This could potentially affect Workflow determinism in edge cases.
If you experience issues after upgrading, lock to React Promise v2 in your composer.json:
{
"require": {
"react/promise": "^2.11"
}
}Destroyable Interface
Workflows can now implement the Destroyable interface from the internal/destroy package to explicitly manage resource cleanup when the Workflow instance is evicted from memory.
This is particularly useful when your Workflow contains circular references between objects that prevent PHP's garbage collector from properly cleaning up memory.
While this is not a common scenario,
having explicit control over resource cleanup is critical for long-running Workers handling many workflow executions.
The SDK automatically calls the destroy() method when a Workflow instance needs to be evicted from memory,
allowing you to break circular references and release resources deterministically.
final class Workflow implements Destroyable
{
/** Collection with cross-linked objects that also implements Destroyable */
private LinkedCollection $collection;
// ...
public function destroy(): void
{
// Must be idempotent - safe to call multiple times
$collection = $this->collection ?? null;
unset($this->collection);
$collection?->destroy();
}
}Enhanced Workflow Info
Added new fields to Workflow::getInfo():
- Access the root Workflow execution from any Workflow in the execution chain, including deeply nested child Workflows.
- Access the Workflow's retry policy directly from the Workflow Context.
$rootExecution = Workflow::getInfo()->rootExecution;
$retryOptions = Workflow::getInfo()->retryOptions;RoadRunner PSR Logger
The RoadRunner ecosystem now includes a new roadrunner/psr-logger package that can be used with Temporal SDK.
By default, the SDK uses \Temporal\Worker\Logger\StderrLogger which outputs messages to STDERR.
RoadRunner captures these messages and logs them at the INFO level.
The new \RoadRunner\PsrLogger\RpcLogger sends logs to RoadRunner via RPC with precise log levels and structured context data.
Get Started:
composer require roadrunner/psr-loggeruse RoadRunner\PsrLogger\RpcLogger;
use Spiral\Goridge\RPC\RPC;
use Temporal\WorkerFactory;
$rpc = RPC::create('tcp://127.0.0.1:6001');
$logger = new RpcLogger($rpc);
$factory = WorkerFactory::create(logger: $logger);
$worker = $factory->newWorker('my-task-queue');New Worker Versioning (experimental)
Worker Versioning enables safe deployment of workflow changes by controlling how Workflows move between different worker versions.
Each worker deployment is identified by a unique Build ID, and workflows can be pinned to specific versions or automatically upgrade to the latest version.
Worker Configuration
Configure versioning when creating a worker:
use Temporal\Worker\WorkerOptions;
use Temporal\Worker\WorkerDeploymentOptions;
use Temporal\Common\Versioning\VersioningBehavior;
$worker = $factory->newWorker(
'my-task-queue',
WorkerOptions::new()
->withDeploymentOptions(
WorkerDeploymentOptions::new()
->withUseVersioning(true)
->withVersion('build-v1.2.3')
->withDefaultVersioningBehavior(VersioningBehavior::Pinned)
)
);Workflow Versioning Behavior
Control versioning behavior per workflow using the #[WorkflowVersioningBehavior] attribute:
use Temporal\Workflow;
use Temporal\Common\Versioning\VersioningBehavior;
#[Workflow\WorkflowInterface]
class MyWorkflow
{
#[Workflow\WorkflowMethod]
#[Workflow\WorkflowVersioningBehavior(VersioningBehavior::Pinned)]
public function handle(): \Generator
{
// Workflow will stay pinned to its original deployment version
yield Workflow::timer(3600);
return 'Done';
}
}Versioning Behaviors:
Pinned: Workflow stays on its original deployment version until completionAutoUpgrade: Workflow automatically moves to the current deployment version on the next workflow task
Client Override
Override versioning behavior when starting a workflow:
use Temporal\Client\WorkflowOptions;
use Temporal\Common\Versioning\VersioningOverride;
use Temporal\Common\Versioning\WorkerDeploymentVersion;
// Pin to specific version
$workflow = $client->newWorkflowStub(
MyWorkflow::class,
WorkflowOptions::new()
->withVersioningOverride(
VersioningOverride::pinned(
WorkerDeploymentVersion::fromString('build-v1.2.3')
)
)
);
// Or enable auto-upgrade
$workflow = $client->newWorkflowStub(
MyWorkflow::class,
WorkflowOptions::new()
->withVersioningOverride(VersioningOverride::autoUpgrade())
);Note
This feature is experimental and requires RoadRunner 2025.1.3+.
See the Worker Versioning documentation for deployment strategies and best practices.
Priority Fairness (experimental)
Priority Fairness extends the Task Queue Priority feature with fairness keys and weights,
enabling balanced task processing across multiple tenants or execution groups within a single task queue.
This is particularly valuable for multi-tenant SaaS applications where you need to prevent large tenants from monopolizing worker resources.
Key Concepts:
- Fairness Key: A short string (up to 64 bytes) that groups tasks together, typically representing a tenant ID or priority band (e.g., "premium", "standard", "free")
- Fairness Weight: A float value (0.001 to 1000) that controls the relative processing share for each fairness key. Higher weights receive proportionally more throughput
The fairness mechanism ensures tasks are dispatched in proportion to their weights. For example, with 1000 tenants each having a weight of 1.0, each tenant receives roughly equal task processing throughput regardless of their individual workload size.
Setting Fairness Parameters:
use Temporal\Common\Priority;
use Temporal\Client\WorkflowOptions;
// Start workflow with fairness settings
$workflow = $workflowClient->newWorkflowStub(
OrderWorkflow::class,
WorkflowOptions::new()
->withTaskQueue('task-queue')
->withPriority(
Priority::new()
->withFairnessKey('tenant-123')
->withFairnessWeight(2.5)
),
);In Workflow Context:
use Temporal\Workflow;
use Temporal\Common\Priority;
// Set fairness for an Activity
$activity = Workflow::newActivityStub(
ActivityInterface::class,
ActivityOptions::new()
->withScheduleToCloseTimeout('5 minutes')
->withPriority(
Priority::new()
->withFairnessKey('premium-tenant')
->withFairnessWeight(10.0)
),
);
// Set fairness for a Child Workflow
$childWorkflow = Workflow::newChildWorkflowStub(
ChildWorkflowInterface::class,
ChildWorkflowOptions::new()
->withPriority(
Priority::new()
->withFairnessKey('tenant-456')
->withFairnessWeight(1.0)
),
);Accessing Fairness Information:
// In Workflow
$priority = Workflow::getInfo()->priority;
$fairnessKey = $priority->fairnessKey;
$fairnessWeight = $priority->fairnessWeight;
// In Activity
$priority = Activity::getInfo()->priority;
$fairnessKey = $priority->fairnessKey;
$fairnessWeight = $priority->fairnessWeight;Weight Precedence:
Fairness weights can be configured from multiple sources, with the following precedence (highest to lowest):
- Task queue configuration overrides (set via API)
- Weight attached to workflow/activity in code
- Default weight of 1.0
Note
- Weight values are automatically clamped to the range [0.001, 1000]
- Fairness keys are inherited by child workflows and activities by default
- The fairness mechanism works in conjunction with priority keys for fine-grained control
- Watch the Temporal Task Queue Fairness | Multi-Tenant Workflows Made Easy video for more details
DestructMemorizedInstanceException (experimental)
Added a new feature flag FeatureFlags::$throwDestructMemorizedInstanceException to control an internal memory cleanup mechanism.
When enabled (default), the SDK throws DestructMemorizedInstanceException into pending promises during Workflow eviction.
This exception may occasionally surface in user code where it should be ignored - which is not obvious and adds complexity.
# worker.php
use Temporal\Worker\FeatureFlags;
// Default behavior
FeatureFlags::$throwDestructMemorizedInstanceException = true;
// Experimental - disable exception throwing
FeatureFlags::$throwDestructMemorizedInstanceException = false;Warning
You can experiment with disabling this flag in non-production environments to monitor memory usage. Future SDK versions will move away from this mechanism toward promise implementations that self-cleanup without exceptions.
Pull Requests
- Update Temporal API and protobuf integration by @roxblnfk in #643
- Control Abandoned Child Workflow Cancellation by @roxblnfk in #646
- Replace React Promise v2 by @roxblnfk in #647
- Promises v3 by @roxblnfk in #649
- Expose Root Workflow Execution in Workflow scope by @roxblnfk in #652
- Expose RetryPolicy in Workflow context by @roxblnfk in #653
- Add feature flag
throwDestructMemorizedInstanceExceptionby @roxblnfk in #651 - Use public Destroyable interface by @roxblnfk in #650
- New Worker Versioning API by @roxblnfk in #645
- Fairness Keys & Weights by @roxblnfk in #657
- Prepare to release by @roxblnfk in #658
Full Changelog: v2.15.0...v2.16.0