Skip to content

v2.16.0

Latest

Choose a tag to compare

@roxblnfk roxblnfk released this 06 Oct 17:54
· 14 commits to master since this release
dfa5fb0

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-logger
use 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 completion
  • AutoUpgrade: 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):

  1. Task queue configuration overrides (set via API)
  2. Weight attached to workflow/activity in code
  3. 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

Full Changelog: v2.15.0...v2.16.0