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

## [Unreleased]

### Changed
- **`Context::get()` and `getLocal()` now throw `Async\ContextException` when the key is missing, instead of returning `null`.** They were exact duplicates of `find()`/`findLocal()`, which made them pointless. `get()` is the mandatory-value form; `find()` remains the one that answers `null`.

### Fixed
- **`foreach` over a `Channel` broke on the ordinary producer/consumer pattern.** Closing a channel while a consumer was parked in the loop left the wake-up `ChannelException` pending, so it escaped `foreach` uncaught — and surfaced against the producer's `close()`, which had done nothing wrong. An explicit `close()` now ends the iteration cleanly, while a deadlock or a disposal still propagates.

Expand Down
3 changes: 2 additions & 1 deletion async.c
Original file line number Diff line number Diff line change
Expand Up @@ -1779,8 +1779,9 @@ ZEND_MINIT_FUNCTION(async)
async_register_timeout_ce();
async_register_scope_ce();
async_register_coroutine_ce();
async_register_context_ce();
// Must follow the exceptions: ContextException derives from AsyncException.
async_register_exceptions_ce();
async_register_context_ce();
async_register_channel_ce();
async_register_thread_channel_ce();
async_register_thread_pool_ce();
Expand Down
22 changes: 20 additions & 2 deletions context.c
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

#include "exceptions.h"
#include "context_arginfo.h"
#include "zend_exceptions.h"

//////////////////////////////////////////////////////////////////////
/// Context API Implementation
Expand Down Expand Up @@ -197,6 +198,7 @@ void async_context_dispose(async_context_t *context)
//////////////////////////////////////////////////////////////////////

zend_class_entry *async_ce_context = NULL;
zend_class_entry *async_ce_context_exception = NULL;

#define METHOD(name) ZEND_METHOD(Async_Context, name)
#define ZEND_OBJECT_TO_CONTEXT(obj) ((async_context_t *) ((char *) (obj) - (obj)->handlers->offset))
Expand All @@ -210,6 +212,19 @@ zend_class_entry *async_ce_context = NULL;
} \
} while (0)

/**
* get()/getLocal() are the mandatory-value forms; find()/findLocal() are the ones that answer null.
*/
static void context_throw_missing_key(const zval *key)
{
if (Z_TYPE_P(key) == IS_STRING) {
zend_throw_exception_ex(async_ce_context_exception, 0, "Context key \"%s\" not found", Z_STRVAL_P(key));
} else {
zend_throw_exception_ex(
async_ce_context_exception, 0, "Context key of type %s not found", ZSTR_VAL(Z_OBJCE_P(key)->name));
}
}

METHOD(find)
{
zval *key;
Expand Down Expand Up @@ -239,7 +254,8 @@ METHOD(get)
return;
}

RETURN_NULL();
context_throw_missing_key(key);
RETURN_THROWS();
}

METHOD(has)
Expand Down Expand Up @@ -283,7 +299,8 @@ METHOD(getLocal)
return;
}

RETURN_NULL();
context_throw_missing_key(key);
RETURN_THROWS();
}

METHOD(hasLocal)
Expand Down Expand Up @@ -362,6 +379,7 @@ static void context_free(zend_object *object)

void async_register_context_ce(void)
{
async_ce_context_exception = register_class_Async_ContextException(async_ce_async_exception);
async_ce_context = register_class_Async_Context();

async_ce_context->create_object = context_object_create;
Expand Down
1 change: 1 addition & 0 deletions context.h
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ void async_context_dispose(async_context_t *context);

// Class entry
PHP_ASYNC_API extern zend_class_entry *async_ce_context;
PHP_ASYNC_API extern zend_class_entry *async_ce_context_exception;
void async_register_context_ce(void);


Expand Down
19 changes: 18 additions & 1 deletion context.stub.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@

namespace Async;

/**
* Thrown when a mandatory Context key is missing.
*/
class ContextException extends AsyncException {}

/**
* @strict-properties
* @not-serializable
Expand All @@ -12,11 +17,17 @@ final class Context
{
/**
* Find a value by key in the current or parent Context.
*
* Returns null when the key is absent. Use get() when the value is mandatory.
*/
public function find(string|object $key): mixed {}

/**
* Get a value by key in the current Context.
* Get a value by key in the current or parent Context.
*
* Unlike find(), a missing key is an error rather than a null.
*
* @throws ContextException If the key is not found at any level.
*/
public function get(string|object $key): mixed {}

Expand All @@ -27,11 +38,17 @@ public function has(string|object $key): bool {}

/**
* Find a value by key only in the local Context.
*
* Returns null when the key is absent. Use getLocal() when the value is mandatory.
*/
public function findLocal(string|object $key): mixed {}

/**
* Get a value by key only in the local Context.
*
* Unlike findLocal(), a missing key is an error rather than a null.
*
* @throws ContextException If the key is not found in the local Context.
*/
public function getLocal(string|object $key): mixed {}

Expand Down
47 changes: 28 additions & 19 deletions context_arginfo.h
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
/* This is a generated file, edit the .stub.php file instead.
* Stub hash: bb8f3ff840be0001815129aa778f2c91b5701873 */
/* This is a generated file, edit context.stub.php instead.
* Stub hash: 9cee158f24f5c73feaca9c17d8b82fba15e506de */

ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Async_Context_find, 0, 1, IS_MIXED, 0)
ZEND_ARG_TYPE_MASK(0, key, MAY_BE_STRING | MAY_BE_OBJECT, NULL)
ZEND_ARG_TYPE_MASK(0, key, MAY_BE_STRING|MAY_BE_OBJECT, NULL)
ZEND_END_ARG_INFO()

#define arginfo_class_Async_Context_get arginfo_class_Async_Context_find

ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Async_Context_has, 0, 1, _IS_BOOL, 0)
ZEND_ARG_TYPE_MASK(0, key, MAY_BE_STRING | MAY_BE_OBJECT, NULL)
ZEND_ARG_TYPE_MASK(0, key, MAY_BE_STRING|MAY_BE_OBJECT, NULL)
ZEND_END_ARG_INFO()

#define arginfo_class_Async_Context_findLocal arginfo_class_Async_Context_find
Expand All @@ -18,13 +18,13 @@ ZEND_END_ARG_INFO()
#define arginfo_class_Async_Context_hasLocal arginfo_class_Async_Context_has

ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_Async_Context_set, 0, 2, Async\\Context, 0)
ZEND_ARG_TYPE_MASK(0, key, MAY_BE_STRING | MAY_BE_OBJECT, NULL)
ZEND_ARG_TYPE_INFO(0, value, IS_MIXED, 0)
ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, replace, _IS_BOOL, 0, "false")
ZEND_ARG_TYPE_MASK(0, key, MAY_BE_STRING|MAY_BE_OBJECT, NULL)
ZEND_ARG_TYPE_INFO(0, value, IS_MIXED, 0)
ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, replace, _IS_BOOL, 0, "false")
ZEND_END_ARG_INFO()

ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_Async_Context_unset, 0, 1, Async\\Context, 0)
ZEND_ARG_TYPE_MASK(0, key, MAY_BE_STRING | MAY_BE_OBJECT, NULL)
ZEND_ARG_TYPE_MASK(0, key, MAY_BE_STRING|MAY_BE_OBJECT, NULL)
ZEND_END_ARG_INFO()

ZEND_METHOD(Async_Context, find);
Expand All @@ -37,24 +37,33 @@ ZEND_METHOD(Async_Context, set);
ZEND_METHOD(Async_Context, unset);

static const zend_function_entry class_Async_Context_methods[] = {
ZEND_ME(Async_Context, find, arginfo_class_Async_Context_find, ZEND_ACC_PUBLIC) ZEND_ME(
Async_Context, get, arginfo_class_Async_Context_get, ZEND_ACC_PUBLIC)
ZEND_ME(Async_Context, has, arginfo_class_Async_Context_has, ZEND_ACC_PUBLIC) ZEND_ME(
Async_Context, findLocal, arginfo_class_Async_Context_findLocal, ZEND_ACC_PUBLIC)
ZEND_ME(Async_Context, getLocal, arginfo_class_Async_Context_getLocal, ZEND_ACC_PUBLIC) ZEND_ME(
Async_Context, hasLocal, arginfo_class_Async_Context_hasLocal, ZEND_ACC_PUBLIC)
ZEND_ME(Async_Context, set, arginfo_class_Async_Context_set, ZEND_ACC_PUBLIC)
ZEND_ME(Async_Context, unset, arginfo_class_Async_Context_unset, ZEND_ACC_PUBLIC)
ZEND_FE_END
ZEND_ME(Async_Context, find, arginfo_class_Async_Context_find, ZEND_ACC_PUBLIC)
ZEND_ME(Async_Context, get, arginfo_class_Async_Context_get, ZEND_ACC_PUBLIC)
ZEND_ME(Async_Context, has, arginfo_class_Async_Context_has, ZEND_ACC_PUBLIC)
ZEND_ME(Async_Context, findLocal, arginfo_class_Async_Context_findLocal, ZEND_ACC_PUBLIC)
ZEND_ME(Async_Context, getLocal, arginfo_class_Async_Context_getLocal, ZEND_ACC_PUBLIC)
ZEND_ME(Async_Context, hasLocal, arginfo_class_Async_Context_hasLocal, ZEND_ACC_PUBLIC)
ZEND_ME(Async_Context, set, arginfo_class_Async_Context_set, ZEND_ACC_PUBLIC)
ZEND_ME(Async_Context, unset, arginfo_class_Async_Context_unset, ZEND_ACC_PUBLIC)
ZEND_FE_END
};

static zend_class_entry *register_class_Async_ContextException(zend_class_entry *class_entry_Async_AsyncException)
{
zend_class_entry ce, *class_entry;

INIT_NS_CLASS_ENTRY(ce, "Async", "ContextException", NULL);
class_entry = zend_register_internal_class_with_flags(&ce, class_entry_Async_AsyncException, 0);

return class_entry;
}

static zend_class_entry *register_class_Async_Context(void)
{
zend_class_entry ce, *class_entry;

INIT_NS_CLASS_ENTRY(ce, "Async", "Context", class_Async_Context_methods);
class_entry = zend_register_internal_class_with_flags(
&ce, NULL, ZEND_ACC_FINAL | ZEND_ACC_NO_DYNAMIC_PROPERTIES | ZEND_ACC_NOT_SERIALIZABLE);
class_entry = zend_register_internal_class_with_flags(&ce, NULL, ZEND_ACC_FINAL|ZEND_ACC_NO_DYNAMIC_PROPERTIES|ZEND_ACC_NOT_SERIALIZABLE);

return class_entry;
}
12 changes: 10 additions & 2 deletions fuzzy-tests/_harness/Steps.php
Original file line number Diff line number Diff line change
Expand Up @@ -3526,8 +3526,12 @@ function(Context $ctx, string $coro, string $key, string $value) {
&& $cc->get($key) === $value
&& $cc->has($key) === true;
$ctx->inc($inheritOk ? "inherit_hit_$coro" : "inherit_miss_$coro");
// getLocal() throws on a missing key; findLocal() is the null-returning form.
$getLocalThrew = false;
try { $cc->getLocal($key); }
catch (\Async\ContextException $e) { $getLocalThrew = true; }
$localAbsent = $cc->findLocal($key) === null
&& $cc->getLocal($key) === null
&& $getLocalThrew
&& $cc->hasLocal($key) === false;
$ctx->inc($localAbsent ? "local_absent_$coro" : "local_present_$coro");
});
Expand Down Expand Up @@ -3576,7 +3580,11 @@ function(Context $ctx, string $coro, string $key) {
$cc->unset($key);
if ($cc->has($key) !== false) $pass = false;
if ($cc->hasLocal($key) !== false) $pass = false;
if ($cc->get($key) !== null) $pass = false;
// After unset the key is gone, so get() must throw rather than answer null.
$getThrew = false;
try { $cc->get($key); }
catch (\Async\ContextException $e) { $getThrew = true; }
if (!$getThrew) $pass = false;
} catch (\Throwable $e) {
$pass = false;
}
Expand Down
19 changes: 14 additions & 5 deletions tests/context/002-context_inheritance.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@ use function Async\spawn;
use function Async\current_context;
use function Async\await;

function get_local_or_throws(\Async\Context $context, string $key): string
{
try {
return (string) $context->getLocal($key);
} catch (\Async\ContextException) {
return 'throws';
}
}

echo "start\n";

// Create parent scope
Expand Down Expand Up @@ -53,9 +62,9 @@ $child_coroutine = $child_scope->spawn(function() {
echo "findLocal parent_key: " . ($context->findLocal('parent_key') ?: 'null') . "\n";
echo "findLocal shared_key: " . ($context->findLocal('shared_key') ?: 'null') . "\n";

// Test getLocal() - should NOT find parent values
echo "getLocal parent_key: " . ($context->getLocal('parent_key') ?: 'null') . "\n";
echo "getLocal shared_key: " . ($context->getLocal('shared_key') ?: 'null') . "\n";
// Test getLocal() - should NOT find parent values, and a miss is an error rather than a null
echo "getLocal parent_key: " . get_local_or_throws($context, 'parent_key') . "\n";
echo "getLocal shared_key: " . get_local_or_throws($context, 'shared_key') . "\n";

// Test hasLocal() - should NOT find parent values
echo "hasLocal parent_key: " . ($context->hasLocal('parent_key') ? 'true' : 'false') . "\n";
Expand Down Expand Up @@ -100,8 +109,8 @@ has parent_key: true
has shared_key: true
findLocal parent_key: null
findLocal shared_key: null
getLocal parent_key: null
getLocal shared_key: null
getLocal parent_key: throws
getLocal shared_key: throws
hasLocal parent_key: false
hasLocal shared_key: false
child context values set
Expand Down
18 changes: 13 additions & 5 deletions tests/context/008-context_get_missing.phpt
Original file line number Diff line number Diff line change
@@ -1,21 +1,29 @@
--TEST--
Context: get() returns null for missing keys
Context: get() throws for missing keys
--FILE--
<?php

use Async\Context;
use function Async\current_context;

// Covers context.c METHOD(get) L242 RETURN_NULL fallback when
// async_context_find() reports the key as missing.
// get() is the mandatory-value form: a missing key is an error, not a null.
// find() is the one that answers null.

$ctx = current_context();
$ctx->set('present', 'value');

var_dump($ctx->get('present'));
var_dump($ctx->get('missing'));

try {
$ctx->get('missing');
echo "NOT THROWN\n";
} catch (Async\ContextException $exception) {
echo $exception->getMessage(), "\n";
}

var_dump($ctx->find('missing'));

?>
--EXPECT--
string(5) "value"
Context key "missing" not found
NULL
39 changes: 39 additions & 0 deletions tests/context/011-context_get_throws_on_missing_key.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
--TEST--
Context: get()/getLocal() throw on a missing key, find()/findLocal() answer null
--FILE--
<?php

use function Async\current_context;

// get() used to be an exact duplicate of find(): both walked the parents and both answered
// null. It is the mandatory-value form -- a missing key is an error, not a null.
$context = current_context();
$context->set('present', 'V');

echo "find(missing): ", var_export($context->find('missing'), true), "\n";
echo "findLocal(missing): ", var_export($context->findLocal('missing'), true), "\n";

try {
$context->get('missing');
echo "get(missing): NOT THROWN\n";
} catch (Async\ContextException $exception) {
echo "get(missing): ", $exception->getMessage(), "\n";
}

try {
$context->getLocal('missing');
echo "getLocal(missing): NOT THROWN\n";
} catch (Async\ContextException $exception) {
echo "getLocal(missing): ", $exception->getMessage(), "\n";
}

echo "get(present): ", var_export($context->get('present'), true), "\n";
echo "extends AsyncException: ", var_export(is_subclass_of(Async\ContextException::class, Async\AsyncException::class), true), "\n";
?>
--EXPECT--
find(missing): NULL
findLocal(missing): NULL
get(missing): Context key "missing" not found
getLocal(missing): Context key "missing" not found
get(present): 'V'
extends AsyncException: true
Loading
Loading