Skip to content

Commit 4847f98

Browse files
committed
feat(config): optional per-request async scope; release 0.7.2
HttpServerConfig::setRequestScope(bool) / isRequestScope() — opt out of the per-request child async scope (default on, behaviour unchanged). When off, the H1/H2/H3 handler coroutine reuses the connection scope directly, saving two emalloc/efree per request; Async\request_context() then returns null (use ?->). Wired through the per-worker view AND the cross-thread shared-config snapshot (freeze + populate_from_shared), so the knob is honoured under setWorkers(N>1) — not just single-worker. Setter is chainable and locks after start(). Tests: core/049 (default/toggle/chainable/locked/scope-off null context), core/050 (multi-worker propagation). Bumps the stale version #define to 0.7.2; CHANGELOG gains 0.7.2 + the previously-undocumented 0.7.1 (H3 stream credit).
1 parent 44729ad commit 4847f98

12 files changed

Lines changed: 240 additions & 9 deletions

CHANGELOG.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [0.7.2] - 2026-06-02
11+
12+
### Added
13+
14+
- `HttpServerConfig::setRequestScope(bool)` / `isRequestScope()` — opt out of the
15+
per-request child async scope (default on). Off reuses the connection scope,
16+
saving two allocations per request; `Async\request_context()` then returns null
17+
(use `?->`). Propagates across `setWorkers(N>1)`.
18+
19+
## [0.7.1] - 2026-06-01
20+
21+
### Fixed
22+
23+
- HTTP/3: replenish the connection's bidi stream credit on stream close
24+
(`extend_max_streams_bidi`), so long-lived connections no longer stall at the
25+
`initial_max_streams_bidi` cap (#79).
26+
1027
## [0.7.0] - 2026-06-01
1128

1229
Headline release: **HTTP/3 over QUIC**. Folds in everything tagged but not yet

include/php_http_server.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040
extern zend_module_entry http_server_module_entry;
4141
#define phpext_http_server_ptr &http_server_module_entry
4242

43-
#define PHP_HTTP_SERVER_VERSION "0.6.7"
43+
#define PHP_HTTP_SERVER_VERSION "0.7.2"
4444

4545
/*
4646
* ==========================================================================
@@ -240,6 +240,7 @@ struct _http_server_config_t {
240240
uint32_t tls_buffer_bytes; /* CT-out BIO ring size, record-aligned (#29) */
241241
bool http3_alt_svc_enabled;
242242
bool http3_pacing; /* QUIC send pacing — opt-in (#59 Phase 2) */
243+
bool request_scope; /* Per-request child scope (default on) */
243244

244245
/* HTTP body compression (issues #8, #9). Phase 1 ships gzip via zlib-ng;
245246
* phase 2 adds Brotli + zstd through the same vtable.
@@ -780,6 +781,7 @@ typedef struct {
780781
uint32_t tls_buffer_bytes; /* CT-out BIO ring size (#29) */
781782
bool http3_alt_svc_enabled;
782783
bool http3_pacing; /* QUIC send pacing — opt-in (#59) */
784+
bool request_scope; /* Per-request child scope (default on) */
783785
bool telemetry_enabled; /* W3C trace context ingestion */
784786
bool body_streaming_enabled; /* Issue #26 */
785787
/* Hot-path gate: true iff per-request hrtime stamps (enqueue_ns /

src/core/http_connection.c

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1982,7 +1982,8 @@ static void h1_static_on_passthrough_to_php(void *user)
19821982
* http_request_handler_coroutine_new). */
19831983
zend_coroutine_t *coroutine = http_request_handler_coroutine_new(
19841984
conn->scope, http_handler_coroutine_entry, ctx,
1985-
http_handler_coroutine_dispose);
1985+
http_handler_coroutine_dispose,
1986+
conn->view != NULL ? conn->view->request_scope : true);
19861987

19871988
if (UNEXPECTED(coroutine == NULL)) {
19881989
zval_ptr_dtor(&ctx->request_zv);
@@ -2200,7 +2201,8 @@ static void http_connection_dispatch_request(http_connection_t *conn, http_reque
22002201
* (and its spawns) finish, or on spawn failure inside the helper. */
22012202
zend_coroutine_t *coroutine = http_request_handler_coroutine_new(
22022203
conn->scope, http_handler_coroutine_entry, ctx,
2203-
http_handler_coroutine_dispose);
2204+
http_handler_coroutine_dispose,
2205+
conn->view != NULL ? conn->view->request_scope : true);
22042206

22052207
if (coroutine == NULL) {
22062208
/* Spawn failed. The usual cause is a stale EG(exception) — a

src/core/http_connection.h

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -449,20 +449,28 @@ zend_async_scope_t *http_request_scope_new(zend_async_scope_t *server_scope);
449449
* (not yet enqueued) on success; on failure disposes the scope and
450450
* returns NULL, leaving protocol-specific zval/conn cleanup to the
451451
* caller. Shared by the H1/H2/H3 dispatch paths — static inline as it
452-
* sits on the per-request hot path. */
452+
* sits on the per-request hot path.
453+
*
454+
* own_scope (HttpServerConfig::setRequestScope): true mints a per-request
455+
* child scope (isolated request_context()); false reuses server_scope —
456+
* two fewer allocations, but request_context() is then null. */
453457
static zend_always_inline zend_coroutine_t *http_request_handler_coroutine_new(
454458
zend_async_scope_t *server_scope,
455459
zend_coroutine_entry_t entry, void *extended_data,
456-
zend_async_coroutine_dispose dispose)
460+
zend_async_coroutine_dispose dispose,
461+
const bool own_scope)
457462
{
458-
zend_async_scope_t *req_scope = http_request_scope_new(server_scope);
463+
zend_async_scope_t *req_scope = own_scope ? http_request_scope_new(server_scope) : server_scope;
459464
if (req_scope == NULL) {
460465
return NULL;
461466
}
462467

463468
zend_coroutine_t *coroutine = ZEND_ASYNC_NEW_COROUTINE(req_scope);
464469
if (coroutine == NULL) {
465-
req_scope->try_to_dispose(req_scope);
470+
if (own_scope) {
471+
req_scope->try_to_dispose(req_scope);
472+
}
473+
466474
return NULL;
467475
}
468476

src/http2/http2_strategy.c

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -300,7 +300,8 @@ static void http2_strategy_dispatch(struct http_request_t *request,
300300
* its own request_context() subtree, isolated from sibling streams. */
301301
zend_coroutine_t *co = http_request_handler_coroutine_new(
302302
self->conn->scope, http2_handler_coroutine_entry, stream,
303-
http2_handler_coroutine_dispose);
303+
http2_handler_coroutine_dispose,
304+
self->conn->view != NULL ? self->conn->view->request_scope : true);
304305

305306
if (co == NULL) {
306307
zval_ptr_dtor(&stream->request_zv);

src/http3/http3_dispatch.c

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -251,7 +251,8 @@ void http3_stream_dispatch(http3_connection_t *c, http3_stream_t *s)
251251
* http_request_handler_coroutine_new — each multiplexed stream gets
252252
* its own request_context() subtree, isolated from sibling streams. */
253253
zend_coroutine_t *co = http_request_handler_coroutine_new(
254-
scope, h3_handler_coroutine_entry, s, h3_handler_coroutine_dispose);
254+
scope, h3_handler_coroutine_entry, s, h3_handler_coroutine_dispose,
255+
s->conn->view != NULL ? s->conn->view->request_scope : true);
255256

256257
if (co == NULL) {
257258
zval_ptr_dtor(&s->request_zv); ZVAL_UNDEF(&s->request_zv);

src/http_server_class.c

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -956,6 +956,7 @@ const http_server_view_t http_server_view_default = {
956956
.tls_buffer_bytes = 0,
957957
.http3_alt_svc_enabled = true,
958958
.http3_pacing = false,
959+
.request_scope = true,
959960
};
960961

961962
/* Counters / view accessors. Always return a non-NULL pointer; callers
@@ -2346,6 +2347,9 @@ ZEND_METHOD(TrueAsync_HttpServer, start)
23462347
zend_call_method_with_0_params(Z_OBJ(server->config), NULL, NULL, "isHttp3Pacing", &retval);
23472348
server->view.http3_pacing = (Z_TYPE(retval) == IS_TRUE);
23482349

2350+
zend_call_method_with_0_params(Z_OBJ(server->config), NULL, NULL, "isRequestScope", &retval);
2351+
server->view.request_scope = (Z_TYPE(retval) == IS_TRUE);
2352+
23492353
zend_call_method_with_0_params(Z_OBJ(server->config), NULL, NULL, "isTelemetryEnabled", &retval);
23502354
server->view.telemetry_enabled = (Z_TYPE(retval) == IS_TRUE);
23512355

src/http_server_config.c

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ struct _http_server_shared_config_t {
112112
* honoured at start() time as an escape hatch. */
113113
bool http3_alt_svc_enabled;
114114
bool http3_pacing;
115+
bool request_scope;
115116
};
116117

117118
/* Forward declarations for shared-config lifecycle helpers */
@@ -402,6 +403,7 @@ ZEND_METHOD(TrueAsync_HttpServerConfig, __construct)
402403
config->tls_buffer_bytes = DEFAULT_TLS_BUFFER_BYTES;
403404
config->http3_alt_svc_enabled = true; /* RFC 7838 advertise on by default */
404405
config->http3_pacing = false; /* QUIC send pacing — opt-in (#59) */
406+
config->request_scope = true; /* Per-request child scope on by default */
405407
config->write_buffer_size = DEFAULT_WRITE_BUFFER_SIZE;
406408
config->auto_await_body = true; /* Default: wait for body on non-multipart */
407409
ZVAL_UNDEF(&config->bootloader);
@@ -1617,6 +1619,40 @@ ZEND_METHOD(TrueAsync_HttpServerConfig, isHttp3Pacing)
16171619
RETURN_BOOL(config->http3_pacing);
16181620
}
16191621

1622+
/* {{{ proto HttpServerConfig::setRequestScope(bool $enable): static
1623+
*
1624+
* Per-request child scope (on by default). When on, every request (and
1625+
* every multiplexed H2/H3 stream) runs in its own async scope, giving it
1626+
* an isolated request_context() subtree and a clean drain boundary.
1627+
*
1628+
* Turning it off reuses the connection scope directly, saving two
1629+
* allocations per request — but Async\request_context() then returns
1630+
* null (use the ?-> operator), so only disable it for handlers that
1631+
* never rely on per-request context. */
1632+
ZEND_METHOD(TrueAsync_HttpServerConfig, setRequestScope)
1633+
{
1634+
bool enable;
1635+
ZEND_PARSE_PARAMETERS_START(1, 1)
1636+
Z_PARAM_BOOL(enable)
1637+
ZEND_PARSE_PARAMETERS_END();
1638+
1639+
http_server_config_t *config = Z_HTTP_SERVER_CONFIG_P(ZEND_THIS);
1640+
1641+
if (config_check_locked(config)) {
1642+
return;
1643+
}
1644+
1645+
config->request_scope = enable;
1646+
RETURN_OBJ_COPY(Z_OBJ_P(ZEND_THIS));
1647+
}
1648+
1649+
ZEND_METHOD(TrueAsync_HttpServerConfig, isRequestScope)
1650+
{
1651+
ZEND_PARSE_PARAMETERS_NONE();
1652+
http_server_config_t *config = Z_HTTP_SERVER_CONFIG_P(ZEND_THIS);
1653+
RETURN_BOOL(config->request_scope);
1654+
}
1655+
16201656
/* ==========================================================================
16211657
* HTTP body compression knobs (issue #8). Editable until the config is
16221658
* locked — see HttpServer::__construct. The MIME whitelist setter
@@ -2500,6 +2536,7 @@ static zend_object *http_server_config_create(zend_class_entry *ce)
25002536
config->tls_buffer_bytes = 0;
25012537
config->http3_alt_svc_enabled = true;
25022538
config->http3_pacing = false;
2539+
config->request_scope = true;
25032540
config->write_buffer_size = 0;
25042541
config->http2_enabled = false;
25052542
config->websocket_enabled = false;
@@ -2637,6 +2674,7 @@ static http_server_shared_config_t *http_server_shared_config_freeze(
26372674
shared->tls_buffer_bytes = src->tls_buffer_bytes;
26382675
shared->http3_alt_svc_enabled = src->http3_alt_svc_enabled;
26392676
shared->http3_pacing = src->http3_pacing;
2677+
shared->request_scope = src->request_scope;
26402678
shared->write_buffer_size = src->write_buffer_size;
26412679

26422680
shared->http2_enabled = src->http2_enabled;
@@ -2789,6 +2827,7 @@ static void http_server_config_populate_from_shared(
27892827
dst->tls_buffer_bytes = src->tls_buffer_bytes;
27902828
dst->http3_alt_svc_enabled = src->http3_alt_svc_enabled;
27912829
dst->http3_pacing = src->http3_pacing;
2830+
dst->request_scope = src->request_scope;
27922831
dst->write_buffer_size = src->write_buffer_size;
27932832

27942833
dst->http2_enabled = src->http2_enabled;

stubs/HttpServerConfig.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -529,6 +529,19 @@ public function setHttp3Pacing(bool $enable): static {}
529529
/** @return bool */
530530
public function isHttp3Pacing(): bool {}
531531

532+
/**
533+
* Per-request child scope (on by default). Off reuses the connection
534+
* scope directly — saves two allocations per request, but
535+
* Async\request_context() then returns null (use ?->).
536+
*
537+
* @param bool $enable
538+
* @return static
539+
*/
540+
public function setRequestScope(bool $enable): static {}
541+
542+
/** @return bool */
543+
public function isRequestScope(): bool {}
544+
532545
// === HTTP body compression (issue #8) ===
533546

534547
/**

stubs/HttpServerConfig.php_arginfo.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,10 @@ ZEND_END_ARG_INFO()
153153

154154
#define arginfo_class_TrueAsync_HttpServerConfig_isHttp3Pacing arginfo_class_TrueAsync_HttpServerConfig_isHttp3AltSvcEnabled
155155

156+
#define arginfo_class_TrueAsync_HttpServerConfig_setRequestScope arginfo_class_TrueAsync_HttpServerConfig_setHttp3AltSvcEnabled
157+
158+
#define arginfo_class_TrueAsync_HttpServerConfig_isRequestScope arginfo_class_TrueAsync_HttpServerConfig_isHttp3AltSvcEnabled
159+
156160
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_TrueAsync_HttpServerConfig_setCompressionLevel, 0, 1, IS_STATIC, 0)
157161
ZEND_ARG_TYPE_INFO(0, level, IS_LONG, 0)
158162
ZEND_END_ARG_INFO()
@@ -307,6 +311,8 @@ ZEND_METHOD(TrueAsync_HttpServerConfig, setHttp3AltSvcEnabled);
307311
ZEND_METHOD(TrueAsync_HttpServerConfig, isHttp3AltSvcEnabled);
308312
ZEND_METHOD(TrueAsync_HttpServerConfig, setHttp3Pacing);
309313
ZEND_METHOD(TrueAsync_HttpServerConfig, isHttp3Pacing);
314+
ZEND_METHOD(TrueAsync_HttpServerConfig, setRequestScope);
315+
ZEND_METHOD(TrueAsync_HttpServerConfig, isRequestScope);
310316
ZEND_METHOD(TrueAsync_HttpServerConfig, setCompressionEnabled);
311317
ZEND_METHOD(TrueAsync_HttpServerConfig, isCompressionEnabled);
312318
ZEND_METHOD(TrueAsync_HttpServerConfig, setCompressionLevel);
@@ -408,6 +414,8 @@ static const zend_function_entry class_TrueAsync_HttpServerConfig_methods[] = {
408414
ZEND_ME(TrueAsync_HttpServerConfig, isHttp3AltSvcEnabled, arginfo_class_TrueAsync_HttpServerConfig_isHttp3AltSvcEnabled, ZEND_ACC_PUBLIC)
409415
ZEND_ME(TrueAsync_HttpServerConfig, setHttp3Pacing, arginfo_class_TrueAsync_HttpServerConfig_setHttp3Pacing, ZEND_ACC_PUBLIC)
410416
ZEND_ME(TrueAsync_HttpServerConfig, isHttp3Pacing, arginfo_class_TrueAsync_HttpServerConfig_isHttp3Pacing, ZEND_ACC_PUBLIC)
417+
ZEND_ME(TrueAsync_HttpServerConfig, setRequestScope, arginfo_class_TrueAsync_HttpServerConfig_setRequestScope, ZEND_ACC_PUBLIC)
418+
ZEND_ME(TrueAsync_HttpServerConfig, isRequestScope, arginfo_class_TrueAsync_HttpServerConfig_isRequestScope, ZEND_ACC_PUBLIC)
411419
ZEND_ME(TrueAsync_HttpServerConfig, setCompressionEnabled, arginfo_class_TrueAsync_HttpServerConfig_setCompressionEnabled, ZEND_ACC_PUBLIC)
412420
ZEND_ME(TrueAsync_HttpServerConfig, isCompressionEnabled, arginfo_class_TrueAsync_HttpServerConfig_isCompressionEnabled, ZEND_ACC_PUBLIC)
413421
ZEND_ME(TrueAsync_HttpServerConfig, setCompressionLevel, arginfo_class_TrueAsync_HttpServerConfig_setCompressionLevel, ZEND_ACC_PUBLIC)

0 commit comments

Comments
 (0)