Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
de4d062
feat: implement fibers
xepozz Feb 23, 2026
e70ff84
feat: implement fibers
xepozz Feb 23, 2026
19d00a1
test: more tests
xepozz Feb 25, 2026
bbd5f6d
test: clean
xepozz Feb 25, 2026
9b224c4
test: clean
xepozz Feb 25, 2026
7540165
test: more tests
xepozz Feb 26, 2026
8596c9b
test: more tests
xepozz Feb 26, 2026
a96ba40
test: more tests
xepozz Feb 26, 2026
0aa4503
test: more tests
xepozz Feb 26, 2026
4a1e928
test: more tests
xepozz Feb 26, 2026
ded2396
test: correct deferred
xepozz Feb 26, 2026
2ac6566
test: correct deferred
xepozz Feb 26, 2026
003dea7
test: correct deferred
xepozz Feb 26, 2026
5be5c6a
feat: handle correct setFiberMode
xepozz Mar 21, 2026
5fb15f4
Merge branch 'master' into fibers
xepozz Mar 25, 2026
448937e
Merge remote-tracking branch 'origin/master' into fibers
xepozz May 23, 2026
3e3d671
feat: enhance workflow APIs with updated type annotations and new met…
xepozz May 23, 2026
6e80e73
feat: iteration of i,provements
xepozz May 23, 2026
1347fc0
feat: improve context handling in scope and enhance stack trace rende…
xepozz May 23, 2026
730c321
feat: enhance fiber-based workflow and activity stubs, expand test co…
xepozz May 24, 2026
e856879
chore: update psalm-baseline and adjust phpunit exclusions
xepozz May 24, 2026
a5f069f
feat: add test coverage for fiber-based workflows, queries, and signals
xepozz May 24, 2026
90ffe47
feat: introduce FiberScope and update fiber-based workflows, tests, a…
xepozz May 25, 2026
d632963
Merge branch 'master' into fibers
xepozz Jul 30, 2026
ac908be
fix(fibers): workflow-fiber correctness guards
xepozz Jul 30, 2026
105ca1b
fix(fibers): propagate scope cancel through non-cancellable requests;…
xepozz Jul 30, 2026
14e0f7c
chore(fibers): cs-fixer ordering + psalm suppress for teardown isset …
xepozz Jul 30, 2026
1e017b0
test(fibers): facade-parity arch guard; docs: fiber migration guide
xepozz Jul 30, 2026
9b04f71
Merge branch 'master' into feature/fibers-no-bc
xepozz Aug 20, 2026
bddd3b0
feat(fibers): add Awaiter, FiberSuspension and DeferredFiber primitives
xepozz Aug 20, 2026
5744257
feat(workflow)!: replace generator suspension with fibers
xepozz Aug 20, 2026
2c534d5
fix(fibers): restore Cancel response slot, scope await cancel semantics
xepozz Aug 20, 2026
050f327
fix(fibers): scope the loop-reentrancy guard to the query and finally…
xepozz Aug 20, 2026
6ca0ff2
fix(fibers): unwind a cancelled scope before propagating to its children
xepozz Aug 20, 2026
2d035e8
Merge branch 'master' into feature/fibers-no-bc
xepozz Aug 21, 2026
a859d1b
fix: regenerate psalm baseline on the PHP version CI uses
xepozz Aug 21, 2026
94c83ae
fix(fibers): unwind a cancelled scope synchronously
xepozz Aug 21, 2026
9ef4575
fix(fibers): let scope cancellation interrupt a child workflow result…
xepozz Aug 21, 2026
fa253c3
refactor(workflow): keep only executeAsync on the documented stub sur…
xepozz Aug 21, 2026
38aea5e
test(fibers): pin scope flow-control semantics for async and asyncDet…
xepozz Aug 22, 2026
c8ae31c
test(fibers): cover scope combinators, teardown depth and guard ordering
xepozz Aug 22, 2026
7f91f08
test(worker): guard the loop layers withheld inside a managed fiber
xepozz Aug 22, 2026
7b7e18d
test(fibers): close the remaining behavioural gaps around scopes
xepozz Aug 22, 2026
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
37 changes: 0 additions & 37 deletions psalm-baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -952,12 +952,6 @@
<code><![CDATA[$name]]></code>
<code><![CDATA[$name]]></code>
</ArgumentTypeCoercion>
<LessSpecificReturnStatement>
<code><![CDATA[EncodedValues::decodePromise($this->request($request), $returnType)]]></code>
</LessSpecificReturnStatement>
<MoreSpecificReturnType>
<code><![CDATA[PromiseInterface]]></code>
</MoreSpecificReturnType>
</file>
<file src="src/Internal/Workflow/ChildWorkflowProxy.php">
<MoreSpecificImplementedParamType>
Expand All @@ -968,43 +962,12 @@
<ArgumentTypeCoercion>
<code><![CDATA[$this->workflow]]></code>
</ArgumentTypeCoercion>
<LessSpecificReturnStatement>
<code><![CDATA[$this->execution->promise()->then(
function (WorkflowExecution $execution) use ($name, $args) {
$request = new SignalExternalWorkflow(
$this->getOptions()->namespace,
$execution->getID(),
null,
$name,
EncodedValues::fromValues($args),
true,
);

return $this->request($request);
},
)]]></code>
<code><![CDATA[$this->start(...$args)->then(fn() => $this->getResult($returnType))]]></code>
<code><![CDATA[EncodedValues::decodePromise($started)]]></code>
</LessSpecificReturnStatement>
<MoreSpecificReturnType>
<code><![CDATA[PromiseInterface]]></code>
<code><![CDATA[PromiseInterface]]></code>
<code><![CDATA[PromiseInterface]]></code>
</MoreSpecificReturnType>
<PossiblyNullArgument>
<code><![CDATA[$this->result]]></code>
</PossiblyNullArgument>
</file>
<file src="src/Internal/Workflow/Input.php">
<PropertyTypeCoercion>
<code><![CDATA[$header ?? Header::empty()]]></code>
</PropertyTypeCoercion>
</file>
<file src="src/Internal/Workflow/Process/DeferredGenerator.php">
<UnevaluatedCode>
<code><![CDATA[yield;]]></code>
</UnevaluatedCode>
</file>
<file src="src/Internal/Workflow/Process/Process.php">
<PropertyNotSetInConstructor>
<code><![CDATA[Process]]></code>
Expand Down
21 changes: 21 additions & 0 deletions src/Exception/InvalidSuspendException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

/**
* This file is part of Temporal package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace Temporal\Exception;

/**
* Thrown when workflow execution is suspended outside of the workflow suspension protocol.
*
* The usual causes are a non-workflow asynchronous API that runs its own scheduler on
* {@see \Fiber::suspend()}, a suspending Temporal call made from a promise callback or a
* query handler, and a workflow handler that returns a Generator or a Promise.
*/
class InvalidSuspendException extends TemporalException {}
12 changes: 9 additions & 3 deletions src/Internal/Declaration/Dispatcher/Dispatcher.php
Original file line number Diff line number Diff line change
Expand Up @@ -113,9 +113,15 @@ private function createExecutorFromFunction(\ReflectionFunction $fun): \Closure
});


return $fun->isStatic()
? $closure->bindTo(null, $ctx::class)?->__invoke(...$arguments) ?? $closure(...$arguments)
: $closure->call($ctx, ...$arguments);
if (!$fun->isStatic()) {
return $closure->call($ctx, ...$arguments);
}

$bound = $closure->bindTo(null, $ctx::class);

return $bound === null
? $closure(...$arguments)
: $bound(...$arguments);
} finally {
\restore_error_handler();
}
Expand Down
32 changes: 20 additions & 12 deletions src/Internal/Support/StackRenderer.php
Original file line number Diff line number Diff line change
Expand Up @@ -110,14 +110,14 @@ public static function renderProto(array $stackTrace): EnhancedStackTrace

/** @var list<StackTraceFileLocation> $locations */
$locations = [];
$userFrameSeen = false;

foreach ($stackTrace as $line) {
$location = (new StackTraceFileLocation());

$isInternal = false;
$file = $line['file'] ?? null;
if ($file !== null) {
$location->setFilePath($file);
foreach (self::$ignorePaths as $str) {
if (\str_starts_with($file, $str)) {
$isInternal = true;
Expand All @@ -126,7 +126,13 @@ public static function renderProto(array $stackTrace): EnhancedStackTrace
}
}

isset($line['line']) and $location->setLine($line['line']);
$exposeLocation = $isInternal || !$userFrameSeen;
if ($file !== null && $exposeLocation) {
$location->setFilePath($file);
}
if ($exposeLocation && isset($line['line'])) {
$location->setLine($line['line']);
}

if (isset($line['function'])) {
$location->setFunctionName(\sprintf(
Expand All @@ -139,17 +145,19 @@ public static function renderProto(array $stackTrace): EnhancedStackTrace

$locations[] = $location->setInternalCode($isInternal);

// Store source code for non-internal files
if (!$isInternal && $file !== null && !\array_key_exists($file, $sources)) {
try {
$code = @\file_get_contents($file);
} catch (\Throwable $e) {
$code = \sprintf("Cannot access code.\n---\n%s", $e->getMessage());
}
if (!$isInternal && $file !== null) {
if (!\array_key_exists($file, $sources)) {
try {
$code = @\file_get_contents($file);
} catch (\Throwable $e) {
$code = \sprintf("Cannot access code.\n---\n%s", $e->getMessage());
}

$sources[$file] = (new StackTraceFileSlice())
->setLineOffset(0)
->setContent($code === false ? "Failed to read file." : $code);
$sources[$file] = (new StackTraceFileSlice())
->setLineOffset(0)
->setContent($code === false ? "Failed to read file." : $code);
}
$userFrameSeen = true;
}
}
$stacks[] = (new StackTrace())
Expand Down
1 change: 0 additions & 1 deletion src/Internal/Transport/CompletableResultInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
/**
* @template T
* @extends PromiseInterface<T>
* @yield T
*/
interface CompletableResultInterface extends PromiseInterface
{
Expand Down
17 changes: 9 additions & 8 deletions src/Internal/Workflow/ActivityProxy.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
use Temporal\Internal\Declaration\Prototype\ActivityPrototype;
use Temporal\Internal\Interceptor\Pipeline;
use Temporal\Internal\Support\Reflection;
use Temporal\Internal\Transport\CompletableResultInterface;
use Temporal\Internal\Workflow\Process\Awaiter;
use Temporal\Workflow\WorkflowContextInterface;

/**
Expand Down Expand Up @@ -63,11 +63,10 @@ public function __construct(
$this->ctx = $ctx;
}

/**
* @return CompletableResultInterface
*/
public function __call(string $method, array $args = []): PromiseInterface
public function __call(string $method, array $args = []): mixed
{
Awaiter::assertManaged();

$prototype = $this->findPrototypeByHandlerNameOrFail($method);
$type = $prototype->getHandler()->getReturnType();
$options = $this->options->mergeWith($prototype->getMethodRetry());
Expand All @@ -85,12 +84,12 @@ public function __call(string $method, array $args = []): PromiseInterface
);
}

return $prototype->isLocalActivity()
$result = $prototype->isLocalActivity()
// Run local activity through an interceptor pipeline
? $this->callsInterceptor->with(
fn(ExecuteLocalActivityInput $input): PromiseInterface => $this->ctx
->newUntypedActivityStub($input->options)
->execute($input->type, $input->args, $input->returnType, true),
->executeAsync($input->type, $input->args, $input->returnType, true),
/** @see WorkflowOutboundCallsInterceptor::executeLocalActivity() */
'executeLocalActivity',
)(
Expand All @@ -107,7 +106,7 @@ public function __call(string $method, array $args = []): PromiseInterface
: $this->callsInterceptor->with(
fn(ExecuteActivityInput $input): PromiseInterface => $this->ctx
->newUntypedActivityStub($input->options)
->execute($input->type, $input->args, $input->returnType),
->executeAsync($input->type, $input->args, $input->returnType),
/** @see WorkflowOutboundCallsInterceptor::executeActivity() */
'executeActivity',
)(
Expand All @@ -119,6 +118,8 @@ public function __call(string $method, array $args = []): PromiseInterface
$prototype->getHandler(),
)
);

return Awaiter::await($result, interruptOnCancel: false);
}

private function findPrototypeByHandlerNameOrFail(string $name): ActivityPrototype
Expand Down
15 changes: 15 additions & 0 deletions src/Internal/Workflow/ActivityStub.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
use Temporal\Interceptor\Header;
use Temporal\Interceptor\HeaderInterface;
use Temporal\Internal\Marshaller\MarshallerInterface;
use Temporal\Internal\Workflow\Process\Awaiter;
use Temporal\Internal\Transport\Request\ExecuteActivity;
use Temporal\Internal\Transport\Request\ExecuteLocalActivity;
use Temporal\Worker\Transport\Command\RequestInterface;
Expand Down Expand Up @@ -60,6 +61,20 @@ public function execute(
array $args = [],
Type|string|\ReflectionClass|\ReflectionType|null $returnType = null,
bool $isLocalActivity = false,
): mixed {
Awaiter::assertManaged();

return Awaiter::await(
$this->executeAsync($name, $args, $returnType, $isLocalActivity),
interruptOnCancel: false,
);
}

public function executeAsync(
string $name,
array $args = [],
Type|string|\ReflectionClass|\ReflectionType|null $returnType = null,
bool $isLocalActivity = false,
): PromiseInterface {
$request = $isLocalActivity ?
new ExecuteLocalActivity($name, EncodedValues::fromValues($args), $this->getOptionsArray(), $this->header) :
Expand Down
12 changes: 7 additions & 5 deletions src/Internal/Workflow/ChildWorkflowProxy.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,10 @@

namespace Temporal\Internal\Workflow;

use React\Promise\PromiseInterface;
use Temporal\DataConverter\Type;
use Temporal\Internal\Declaration\Prototype\WorkflowPrototype;
use Temporal\Internal\Support\Reflection;
use Temporal\Internal\Transport\CompletableResultInterface;
use Temporal\Internal\Workflow\Process\Awaiter;
use Temporal\Workflow\ChildWorkflowOptions;
use Temporal\Workflow\ChildWorkflowStubInterface;
use Temporal\Workflow\WorkflowContextInterface;
Expand Down Expand Up @@ -49,10 +48,11 @@ public function __construct(

/**
* @param non-empty-string $method
* @return CompletableResultInterface
*/
public function __call(string $method, array $args): PromiseInterface
public function __call(string $method, array $args): mixed
{
Awaiter::assertManaged();

// If the proxy does not contain information about the running workflow,
// then we try to create a new stub from the workflow method and start
// the workflow.
Expand Down Expand Up @@ -90,7 +90,9 @@ public function __call(string $method, array $args): PromiseInterface
if ($definition->method->getName() === $method) {
$args = Reflection::orderArguments($definition->method, $args);

return $this->stub->signal($name, $args);
$this->stub->signal($name, $args);

return null;
}
}

Expand Down
Loading