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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]

### Fixed
- **`root_context()` was not the main Scope's context and could not be seen by `find()`.** It lived in a global of its own, outside the Scope tree, so `find()` — which walks the Scope chain — never reached it. It is now the main Scope's context: `root_context() === current_context()` at top level, and values set on it are visible from every coroutine that inherits from the main Scope.

- **`iterate()` ignored `concurrency` for every `Traversable`.** Generators, `Iterator` and `IteratorAggregate` ran strictly one callback at a time (arrays were fine): advancing a `zend_iterator` cancelled the spawning microtask and only uncancelled it if the move had suspended, so after the first non-suspending move no worker was ever spawned again.

- **`current_context()` silently discarded everything written before the first coroutine.** With no scope to anchor to, every top-level call returned a fresh detached context, so `set()` wrote where nobody could read it. The scheduler is now started on demand, as `spawn()` already does.
Expand Down
27 changes: 17 additions & 10 deletions async.c
Original file line number Diff line number Diff line change
Expand Up @@ -880,11 +880,25 @@ PHP_FUNCTION(Async_root_context)
THROW_IF_ASYNC_OFF;
THROW_IF_SCHEDULER_CONTEXT;

if (ASYNC_G(root_context) == NULL) {
ASYNC_G(root_context) = (zend_async_context_t *) async_context_new();
// The root context is the context of the main scope. Holding it in a global of its own put it outside
// the scope tree, where find() -- which walks scope->parent_scope -- could never reach it.
SCHEDULER_LAUNCH;

zend_async_scope_t *scope = ZEND_ASYNC_MAIN_SCOPE;

if (UNEXPECTED(scope == NULL)) {
async_throw_error("The main scope is not defined");
RETURN_THROWS();
}

async_context_t *context = (async_context_t *) ASYNC_G(root_context);
if (scope->context == NULL) {
async_context_t *context = async_context_new();
context->scope = scope;
scope->context = &context->base;
RETURN_OBJ_COPY(&context->std);
}

async_context_t *context = (async_context_t *) scope->context;
RETURN_OBJ_COPY(&context->std);
}

Expand Down Expand Up @@ -1695,7 +1709,6 @@ static PHP_GINIT_FUNCTION(async)
async_globals->signal_handlers = NULL;
async_globals->signal_events = NULL;
async_globals->process_events = NULL;
async_globals->root_context = NULL;
/* Maximum number of coroutines in the concurrent iterator */
async_globals->default_concurrency = 32;

Expand Down Expand Up @@ -1813,12 +1826,6 @@ PHP_MINFO_FUNCTION(async)

PHP_RSHUTDOWN_FUNCTION(async)
{
if (ASYNC_G(root_context) != NULL) {
async_context_t *root_context = (async_context_t *) ASYNC_G(root_context);
ASYNC_G(root_context) = NULL;
OBJ_RELEASE(&root_context->std);
}

async_cpu_usage_reset_state();

return SUCCESS;
Expand Down
17 changes: 16 additions & 1 deletion async.stub.php
Original file line number Diff line number Diff line change
Expand Up @@ -192,8 +192,19 @@ function loadavg(): ?array {}
*/
function timeout(int $ms): Awaitable {}

/**
* Returns the context of the current Scope.
*
* Values set here are visible to every coroutine below this Scope, because find() walks up the Scope
* chain. At top level this is the main Scope, so it returns the same object as root_context().
*/
function current_context(): Context {}

/**
* Returns the context of the current coroutine.
*
* Unlike current_context(), this one belongs to the coroutine alone and is not inherited by anybody.
*/
function coroutine_context(): Context {}

/**
Expand All @@ -207,7 +218,11 @@ function current_coroutine(): Coroutine {}
//function finally(\Closure $callback): void {}

/**
* Returns the root Scope.
* Returns the context of the main Scope, the root of the Scope tree.
*
* Values set here are reachable through find() from every coroutine that inherits from the main Scope,
* which is all of them except those in a deliberately detached `new Scope()`. Those still reach it by
* calling root_context() directly.
*/
function root_context(): Context {}

Expand Down
6 changes: 0 additions & 6 deletions async_API.c
Original file line number Diff line number Diff line change
Expand Up @@ -221,12 +221,6 @@ static bool engine_shutdown(void)
zend_hash_destroy(&ASYNC_G(deadlock_channels));
zend_hash_destroy(&ASYNC_G(thread_channels));

if (ASYNC_G(root_context) != NULL) {
async_context_t *root_context = (async_context_t *) ASYNC_G(root_context);
ASYNC_G(root_context) = NULL;
OBJ_RELEASE(&root_context->std);
}

// async_host_name_list_dtor();
return true;
}
Expand Down
2 changes: 1 addition & 1 deletion async_arginfo.h
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/* This is a generated file, edit async.stub.php instead.
* Stub hash: a18e5e8e1cbda7838bb830c323d38bf040cda9cb */
* Stub hash: f55fed172505a7cb1745b8080b2597e5cb3ec825 */

ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_Async_spawn, 0, 1, Async\\Coroutine, 0)
ZEND_ARG_TYPE_INFO(0, task, IS_CALLABLE, 0)
Expand Down
2 changes: 0 additions & 2 deletions php_async.h
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,6 @@ HashTable coroutines;
zend_fiber_transfer *main_transfer;
/* The main flow stack */
zend_vm_stack main_vm_stack;
/* System root context */
zend_async_context_t *root_context;
/* The default concurrency */
int default_concurrency;

Expand Down
32 changes: 32 additions & 0 deletions tests/context/010-root_context_is_main_scope.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
--TEST--
root_context() - is the main Scope's context and is reachable through find()
--FILE--
<?php

use function Async\spawn;
use function Async\await;
use function Async\root_context;
use function Async\current_context;

// root_context() used to live in a global of its own, outside the Scope tree, so find() -- which walks
// scope->parent_scope -- could never reach it. It is the main Scope's context.
root_context()->set('cfg', 'X');

echo "root is current at top level: ", var_export(root_context() === current_context(), true), "\n";
echo "plain coroutine: ", var_export(await(spawn(fn() => current_context()->find('cfg'))), true), "\n";

$inherited = Async\Scope::inherit();
echo "Scope::inherit(): ", var_export(await($inherited->spawn(fn() => current_context()->find('cfg'))), true), "\n";

// A plain `new Scope()` is a detached root on purpose, so it does not inherit the context either --
// but root_context() still reaches it directly.
$detached = new Async\Scope();
echo "new Scope(): ", var_export(await($detached->spawn(fn() => current_context()->find('cfg'))), true), "\n";
echo "new Scope() direct: ", var_export(await($detached->spawn(fn() => root_context()->find('cfg'))), true), "\n";
?>
--EXPECT--
root is current at top level: true
plain coroutine: 'X'
Scope::inherit(): 'X'
new Scope(): NULL
new Scope() direct: 'X'
Loading