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
1 change: 1 addition & 0 deletions async_API.c
Original file line number Diff line number Diff line change
Expand Up @@ -1056,6 +1056,7 @@ void async_await_futures(

// If the await on futures has completed and
// the automatic cancellation mode for pending coroutines is active.
// !Note! that at this point we are finally awaiting the completion of all cancelled Futures.
if (await_context->cancel_on_exit) {
async_cancel_awaited_futures(await_context, futures);
}
Expand Down
9 changes: 7 additions & 2 deletions scheduler.c
Original file line number Diff line number Diff line change
Expand Up @@ -523,6 +523,11 @@ void async_scheduler_launch(void)
return;
}

if (EG(active_fiber)) {
async_throw_error("The True Async Scheduler cannot be started from within a Fiber");
return;
}

if (false == zend_async_reactor_is_enabled()) {
async_throw_error("The scheduler cannot be started without the Reactor");
return;
Expand Down Expand Up @@ -886,7 +891,7 @@ void async_scheduler_coroutine_suspend(zend_fiber_transfer *transfer)
if (UNEXPECTED(
false == has_handles
&& false == is_next_coroutine
&& ZEND_ASYNC_ACTIVE_COROUTINE_COUNT > 0
&& zend_hash_num_elements(&ASYNC_G(coroutines)) > 0
&& circular_buffer_is_empty(&ASYNC_G(microtasks))
&& resolve_deadlocks()
)) {
Expand Down Expand Up @@ -950,7 +955,7 @@ void async_scheduler_main_loop(void)
if (UNEXPECTED(
false == has_handles
&& false == was_executed
&& ZEND_ASYNC_ACTIVE_COROUTINE_COUNT > 0
&& zend_hash_num_elements(&ASYNC_G(coroutines)) > 0
&& circular_buffer_is_empty(&ASYNC_G(coroutine_queue))
&& circular_buffer_is_empty(&ASYNC_G(microtasks))
&& resolve_deadlocks()
Expand Down
42 changes: 35 additions & 7 deletions scope.c
Original file line number Diff line number Diff line change
Expand Up @@ -63,12 +63,10 @@ async_scope_remove_coroutine(async_scope_t *scope, async_coroutine_t *coroutine)
for (uint32_t i = 0; i < vector->length; ++i) {
if (vector->data[i] == coroutine) {
// Decrement active coroutines count if coroutine was active
if (false == ZEND_COROUTINE_IS_ZOMBIE(&coroutine->coroutine)) {
if (scope->active_coroutines_count > 0) {
scope->active_coroutines_count--;
} else if (scope->zombie_coroutines_count > 0) {
scope->zombie_coroutines_count--;
}
if (false == ZEND_COROUTINE_IS_ZOMBIE(&coroutine->coroutine) && scope->active_coroutines_count > 0) {
scope->active_coroutines_count--;
} else if (scope->zombie_coroutines_count > 0) {
scope->zombie_coroutines_count--;
}

vector->data[i] = vector->data[--vector->length];
Expand Down Expand Up @@ -1087,6 +1085,12 @@ static zend_string* scope_info(zend_async_event_t *event)

static void scope_dispose(zend_async_event_t *scope_event)
{
async_scope_t *scope = (async_scope_t *) scope_event;

if (ZEND_ASYNC_SCOPE_IS_DISPOSING(&scope->scope)) {
return;
}

if (ZEND_ASYNC_EVENT_REF(scope_event) > 1) {
ZEND_ASYNC_EVENT_DEL_REF(scope_event);
return;
Expand All @@ -1096,16 +1100,31 @@ static void scope_dispose(zend_async_event_t *scope_event)
ZEND_ASYNC_EVENT_DEL_REF(scope_event);
}

async_scope_t *scope = (async_scope_t *) scope_event;
ZEND_ASYNC_SCOPE_SET_DISPOSING(&scope->scope);

ZEND_ASSERT(scope->coroutines.length == 0 && scope->scope.scopes.length == 0
&& "Scope should be empty before disposal");

zend_object *critical_exception = NULL;

//
// Notifying subscribers one last time that the scope has been definitively completed.
//
ZEND_ASYNC_CALLBACKS_NOTIFY(scope_event, NULL, NULL);
zend_async_callbacks_free(&scope->scope.event);
if (UNEXPECTED(EG(exception))) {
critical_exception = zend_exception_merge(critical_exception, true, true);
}

if (scope->finally_handlers != NULL
&& zend_hash_num_elements(scope->finally_handlers) > 0
&& async_scope_call_finally_handlers(scope)) {
// If finally handlers were called, we don't dispose the scope yet
ZEND_ASYNC_EVENT_ADD_REF(&scope->scope.event);
if (critical_exception) {
async_spawn_and_throw(critical_exception, &scope->scope, 0);
}
ZEND_ASYNC_SCOPE_CLR_DISPOSING(&scope->scope);
return;
}

Expand Down Expand Up @@ -1153,6 +1172,10 @@ static void scope_dispose(zend_async_event_t *scope_event)
async_scope_free_coroutines(scope);
zend_async_scope_free_children(&scope->scope);
efree(scope);

if (critical_exception != NULL) {
async_rethrow_exception(critical_exception);
}
}

zend_async_scope_t * async_new_scope(zend_async_scope_t * parent_scope, const bool with_zend_object)
Expand Down Expand Up @@ -1181,6 +1204,11 @@ zend_async_scope_t * async_new_scope(zend_async_scope_t * parent_scope, const bo
scope->scope.parent_scope = parent_scope;
zend_async_event_t *event = &scope->scope.event;

// Inherit safely disposal flag from parent scope or set it to true if parent scope is NULL
if (parent_scope == NULL || ZEND_ASYNC_SCOPE_IS_DISPOSE_SAFELY(parent_scope)) {
ZEND_ASYNC_SCOPE_SET_DISPOSE_SAFELY(&scope->scope);
}

event->ref_count = 1; // Initialize reference count

scope->scope.before_coroutine_enqueue = scope_before_coroutine_enqueue;
Expand Down
50 changes: 50 additions & 0 deletions tests/edge_cases/007-fiber_first_then_spawn.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
--TEST--
Fiber created first, then spawn operation - should detect incompatible context
--FILE--
<?php

use function Async\spawn;
use function Async\suspend;

echo "Test: Fiber first, then spawn\n";

try {
$fiber = new Fiber(function() {
echo "Inside Fiber\n";

// This should cause issues - spawning from within a Fiber context
$coroutine = spawn(function() {
echo "Inside spawned coroutine from Fiber\n";
suspend();
echo "Coroutine completed\n";
});

echo "Fiber attempting to continue after spawn\n";
Fiber::suspend("fiber suspended");
echo "Fiber resumed\n";

return "fiber done";
});

echo "Starting Fiber\n";
$result = $fiber->start();
echo "Fiber suspended with: " . $result . "\n";

echo "Resuming Fiber\n";
$result = $fiber->resume("resume value");
echo "Fiber returned: " . $result . "\n";

} catch (Async\AsyncException $e) {
echo "Async exception caught: " . $e->getMessage() . "\n";
} catch (Exception $e) {
echo "Exception caught: " . $e->getMessage() . "\n";
}

echo "Test completed\n";
?>
--EXPECTF--
Test: Fiber first, then spawn
Starting Fiber
Inside Fiber
Async exception caught: Cannot spawn a coroutine when async is disabled
Test completed
61 changes: 61 additions & 0 deletions tests/edge_cases/008-spawn_first_then_fiber.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
--TEST--
Spawn coroutine first, then create Fiber - should detect context conflicts
--FILE--
<?php

use function Async\spawn;
use function Async\suspend;
use function Async\await;

echo "Test: Spawn first, then Fiber\n";

try {
// First spawn a coroutine that will suspend and wait
$coroutine = spawn(function() {
echo "Coroutine started\n";
suspend(); // This activates the async scheduler
echo "Coroutine resumed\n";
return "coroutine result";
});

echo "Coroutine spawned, now creating Fiber\n";

// Now try to create and use a Fiber while async scheduler is active
$fiber = new Fiber(function() {
echo "Inside Fiber - this should conflict with active scheduler\n";

// Try to interact with the active coroutine from within Fiber
// This creates a context conflict
Fiber::suspend("fiber suspended");

echo "Fiber resumed\n";
return "fiber done";
});

echo "Starting Fiber\n";
$fiberResult = $fiber->start();
echo "Fiber suspended with: " . $fiberResult . "\n";

echo "Resuming Fiber\n";
$fiberResult = $fiber->resume("resume data");
echo "Fiber completed with: " . $fiberResult . "\n";

echo "Getting coroutine result\n";
$coroutineResult = await($coroutine);
echo "Coroutine completed with: " . $coroutineResult . "\n";

} catch (Error $e) {
echo "Error caught: " . $e->getMessage() . "\n";
} catch (Exception $e) {
echo "Exception caught: " . $e->getMessage() . "\n";
}

echo "Test completed\n";
?>
--EXPECTF--
Test: Spawn first, then Fiber
Coroutine spawned, now creating Fiber
Error caught: Cannot create a fiber while an True Async is active
Test completed
Coroutine started
Coroutine resumed
116 changes: 116 additions & 0 deletions tests/edge_cases/009-fiber_spawn_destructor.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
--TEST--
Fiber and spawn operations in destructors - memory management conflicts
--FILE--
<?php

use function Async\spawn;
use function Async\suspend;
use function Async\await;

echo "Test: Fiber and spawn in destructors\n";

class FiberSpawner {
private $name;

public function __construct($name) {
$this->name = $name;
echo "Created: {$this->name}\n";
}

public function __destruct() {
echo "Destructing: {$this->name}\n";

try {
if ($this->name === 'FiberInDestructor') {
// Create Fiber in destructor
$fiber = new Fiber(function() {
echo "Fiber running in destructor\n";
Fiber::suspend("destructor fiber");
echo "Fiber resumed in destructor\n";
return "destructor done";
});

echo "Starting fiber in destructor\n";
$result = $fiber->start();
echo "Fiber suspended with: " . $result . "\n";

$result = $fiber->resume("resume in destructor");
echo "Fiber completed with: " . $result . "\n";

} elseif ($this->name === 'SpawnInDestructor') {
// Spawn coroutine in destructor
echo "Spawning coroutine in destructor\n";
$coroutine = spawn(function() {
echo "Coroutine running in destructor\n";
suspend();
echo "Coroutine resumed in destructor\n";
return "destructor coroutine done";
});

echo "Waiting for coroutine in destructor\n";
$result = await($coroutine);
echo "Coroutine completed with: " . $result . "\n";
}
} catch (Error $e) {
echo "Error in destructor: " . $e->getMessage() . "\n";
} catch (Exception $e) {
echo "Exception in destructor: " . $e->getMessage() . "\n";
}

echo "Destructor finished: {$this->name}\n";
}
}

try {
echo "Creating objects that will spawn/fiber in destructors\n";

$obj1 = new FiberSpawner('FiberInDestructor');
$obj2 = new FiberSpawner('SpawnInDestructor');

echo "Starting some async operations\n";
$mainCoroutine = spawn(function() {
echo "Main coroutine running\n";
suspend();
echo "Main coroutine resumed\n";
return "main done";
});

// Force destruction by unsetting
echo "Unsetting objects to trigger destructors\n";
unset($obj1);
unset($obj2);

echo "Completing main coroutine\n";
$result = await($mainCoroutine);
echo "Main coroutine result: " . $result . "\n";

} catch (Error $e) {
echo "Error caught: " . $e->getMessage() . "\n";
} catch (Exception $e) {
echo "Exception caught: " . $e->getMessage() . "\n";
}

echo "Test completed\n";
?>
--EXPECTF--
Test: Fiber and spawn in destructors
Creating objects that will spawn/fiber in destructors
Created: FiberInDestructor
Created: SpawnInDestructor
Starting some async operations
Unsetting objects to trigger destructors
Destructing: FiberInDestructor
Error in destructor: Cannot create a fiber while an True Async is active
Destructor finished: FiberInDestructor
Destructing: SpawnInDestructor
Spawning coroutine in destructor
Waiting for coroutine in destructor
Main coroutine running
Coroutine running in destructor
Main coroutine resumed
Coroutine resumed in destructor
Coroutine completed with: destructor coroutine done
Destructor finished: SpawnInDestructor
Completing main coroutine
Main coroutine result: main done
Test completed
Loading