Skip to content

Commit 41ea1dd

Browse files
feat(agent): add per-fiber globals (#1208)
## Summary Each PHP Fiber now gets its own snapshot of the request-scoped globals the agent relies on (`ctx`), so instrumentation running inside a fiber no longer trips over state owned by the main context (or another fiber). The fiber observer hooks already in place from the parent branch are wired up to populate, swap, and tear down these snapshots; the `NRPRG_CTX` accessor transparently reads from the active fiber's snapshot when one is set. All new behavior is gated on PHP 8.1+. ## What changed **Per-fiber globals (`agent/php_fibers.{c,h}`, new)** - New `fiber_globals_t` = `{ ctx_globals_t* }`. - `nr_fiber_copy_ctx_globals` deep-copy the parts a fiber should own. - A per-request `fiber_globals_map` (keyed by `"%p"` of the `zend_fiber_context*`) holds these snapshots; it has a destructor so removed entries are fully cleaned up. Init is lazy on first fiber; teardown happens in `nr_php_post_deactivate`. `php_rshutdown.c` clears `NRPRG(fiber_globals)` before normal global teardown so the existing free paths don't reach into a snapshot. **OAPI fiber observer wiring (`agent/php_observer.c`)** - `nr_fiber_init_observe`: lazy-init `fiber_globals_map`, add a snapshot for the new fiber under its `"%p"` key. - `nr_fiber_destroy_observe`: remove the snapshot for that fiber. - `nr_fiber_switch_observe`: at the end of the existing switch logic, swap `NRPRG(fiber_globals)` to the destination context's snapshot via `nr_fiber_switch_global_context`. **Globals accessors (`agent/php_newrelic.h`)** - New `fiber_globals_t` struct; `NRPRG(fiber_globals_map)` and `NRPRG(fiber_globals)` added to module globals. - On PHP 8.1+, `NRPRG_CTX(Y)` resolves through `NRPRG(fiber_globals)` when it's non-NULL, otherwise read the main-context globals exactly as before. Callers don't have to know which context they're in. PHP < 8.1 macros are unchanged. --------- Co-authored-by: Amber Sistla <asistla@newrelic.com>
1 parent 935c52a commit 41ea1dd

19 files changed

Lines changed: 1631 additions & 20 deletions

agent/Makefile.frag

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ TEST_BINARIES = \
8585
tests/test_curl_md \
8686
tests/test_datastore \
8787
tests/test_environment \
88+
tests/test_fibers \
8889
tests/test_fw_codeigniter \
8990
tests/test_fw_drupal \
9091
tests/test_fw_laravel_queue \

agent/config.m4

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,7 @@ if test "$PHP_NEWRELIC" = "yes"; then
213213
php_api_distributed_trace.c php_api_internal.c php_autorum.c \
214214
php_call.c php_curl.c php_curl_md.c php_datastore.c php_environment.c \
215215
php_error.c php_execute.c php_explain.c php_explain_mysqli.c \
216-
php_explain_pdo_mysql.c php_extension.c php_file_get_contents.c \
216+
php_explain_pdo_mysql.c php_extension.c php_fibers.c php_file_get_contents.c \
217217
php_globals.c php_hash.c php_header.c php_httprequest_send.c \
218218
php_internal_instrument.c php_memcached.c php_minit.c php_mshutdown.c php_mysql.c \
219219
php_mysqli.c php_newrelic.c php_nrini.c php_observer.c php_output.c php_pdo.c \

agent/lib_predis.c

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -81,16 +81,7 @@ const zend_long nr_predis_default_port = 6379;
8181
static NR_PHP_WRAPPER_PROTOTYPE(nr_predis_connection_readResponse);
8282
static NR_PHP_WRAPPER_PROTOTYPE(nr_predis_connection_writeRequest);
8383

84-
static void nr_predis_command_destroy(nrtime_t* time) {
85-
nr_free(time);
86-
}
87-
8884
static inline nr_hashmap_t* nr_predis_get_commands(TSRMLS_D) {
89-
if (NULL == NRPRG_CTX(predis_commands)) {
90-
NRPRG_CTX(predis_commands)
91-
= nr_hashmap_create((nr_hashmap_dtor_func_t)nr_predis_command_destroy);
92-
}
93-
9485
return NRPRG_CTX(predis_commands);
9586
}
9687

agent/php_fibers.c

Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
/*
2+
* Copyright 2026 New Relic Corporation. All rights reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*
5+
* This file handles fibers instrumentation.
6+
*/
7+
8+
#include "php_includes.h"
9+
#include "php_compat.h"
10+
#include "php_fibers.h"
11+
#include "php_agent.h"
12+
#include "php_newrelic.h"
13+
#include "php_zval.h"
14+
#include "util_hashmap.h"
15+
#include "util_logging.h"
16+
#include "util_memory.h"
17+
#include "util_stack.h"
18+
#include "util_strings.h"
19+
#include "zend_types.h"
20+
21+
#if ZEND_MODULE_API_NO >= ZEND_8_1_X_API_NO
22+
23+
#define COPY_BASIC(x) (dest->x = src->x)
24+
#define COPY_POINTER(x) (dest->x = &src->x)
25+
#define COPY_STRING(x) (dest->x = src->x ? nr_strdup(src->x) : NULL)
26+
#define COPY_STACK(x, y) (dest->x = nr_stack_copy(&src->x, y))
27+
28+
static void* copy_elem_zval(void* elem) {
29+
if (NULL == elem) {
30+
return NULL;
31+
}
32+
zval* copy = nr_php_zval_alloc();
33+
ZVAL_DUP(copy, elem);
34+
return copy;
35+
}
36+
37+
static void* copy_elem_ident(void* elem) {
38+
return elem;
39+
}
40+
41+
static void* copy_elem_str(void* elem) {
42+
return nr_strdup((char*)elem);
43+
}
44+
45+
static void fiber_str_dtor(void* e, NRUNUSED void* d) {
46+
nr_free(e);
47+
}
48+
49+
static void nr_ctx_global_deep_copy(ctx_globals_t* dest, ctx_globals_t* src) {
50+
if (NULL == dest) {
51+
return;
52+
}
53+
54+
COPY_STRING(doctrine_dql);
55+
56+
dest->drupal_http_request_depth = 0;
57+
58+
COPY_BASIC(php_cur_stack_depth);
59+
60+
COPY_STRING(mysql_last_conn);
61+
COPY_STRING(pgsql_last_conn);
62+
63+
COPY_BASIC(datastore_connections);
64+
65+
COPY_BASIC(deprecated_capture_request_parameters);
66+
COPY_BASIC(check_cufa);
67+
68+
COPY_BASIC(predis_commands);
69+
70+
dest->context_root = NULL;
71+
72+
COPY_STACK(drupal_invoke_all_hooks, copy_elem_zval);
73+
COPY_STACK(drupal_invoke_all_states, copy_elem_ident);
74+
75+
dest->drupal_http_request_segment = NULL;
76+
77+
COPY_STACK(wordpress_tags, copy_elem_str);
78+
dest->wordpress_tags.dtor = fiber_str_dtor;
79+
COPY_STACK(wordpress_tag_states, copy_elem_ident);
80+
COPY_STACK(predis_ctxs, copy_elem_str);
81+
}
82+
83+
#undef COPY_STACK
84+
#undef COPY_STRING
85+
#undef COPY_BASIC
86+
87+
ctx_globals_t* nr_fiber_copy_ctx_globals(ctx_globals_t* src) {
88+
ctx_globals_t* fiber_ctx_globals = NULL;
89+
90+
if (NULL == src) {
91+
return NULL;
92+
}
93+
94+
fiber_ctx_globals = nr_malloc(sizeof(ctx_globals_t));
95+
96+
nr_ctx_global_deep_copy(fiber_ctx_globals, src);
97+
98+
return fiber_ctx_globals;
99+
}
100+
101+
void free_fiber_globals(void* fiber_globals) {
102+
fiber_globals_t* f = (fiber_globals_t*)fiber_globals;
103+
104+
if (NULL == fiber_globals) {
105+
nrl_warning(NRL_AGENT, "Failed to free fiber globals, target is NULL");
106+
return;
107+
}
108+
109+
if (NULL == f->ctx_globals) {
110+
nrl_warning(NRL_AGENT, "Failed to free fiber globals, ctx_globals is NULL");
111+
nr_free(f);
112+
return;
113+
}
114+
115+
nr_free(f->ctx_globals->doctrine_dql);
116+
nr_free(f->ctx_globals->mysql_last_conn);
117+
nr_free(f->ctx_globals->pgsql_last_conn);
118+
nr_stack_destroy_fields(&f->ctx_globals->drupal_invoke_all_hooks);
119+
nr_stack_destroy_fields(&f->ctx_globals->drupal_invoke_all_states);
120+
nr_stack_destroy_fields(&f->ctx_globals->wordpress_tags);
121+
nr_stack_destroy_fields(&f->ctx_globals->wordpress_tag_states);
122+
nr_stack_destroy_fields(&f->ctx_globals->predis_ctxs);
123+
nr_free(f->ctx_globals);
124+
nr_free(f);
125+
}
126+
127+
nr_status_t nr_fiber_init_global_hashmap(nr_hashmap_t** fiber_globals_map) {
128+
if (NULL == fiber_globals_map) {
129+
return NR_FAILURE;
130+
}
131+
if (NULL != *fiber_globals_map) {
132+
return NR_FAILURE;
133+
}
134+
135+
*fiber_globals_map = nr_hashmap_create(free_fiber_globals);
136+
return NR_SUCCESS;
137+
}
138+
139+
nr_status_t nr_fiber_destroy_global_hashmap(nr_hashmap_t** fiber_globals_map) {
140+
if (NULL == fiber_globals_map) {
141+
return NR_FAILURE;
142+
}
143+
if (NULL != *fiber_globals_map) {
144+
nr_hashmap_destroy(fiber_globals_map);
145+
}
146+
return NR_SUCCESS;
147+
}
148+
149+
nr_status_t nr_add_fiber_context_to_global_hashmap(
150+
nr_hashmap_t* fiber_globals_map,
151+
ctx_globals_t* src_ctx_globals,
152+
const char* key) {
153+
fiber_globals_t* fg = NULL;
154+
ctx_globals_t* cg = NULL;
155+
156+
if (NULL == key || nr_strlen(key) < 1) {
157+
return NR_FAILURE;
158+
}
159+
160+
if (NULL == fiber_globals_map) {
161+
return NR_FAILURE;
162+
}
163+
164+
fg = nr_malloc(sizeof(fiber_globals_t));
165+
cg = nr_fiber_copy_ctx_globals(src_ctx_globals);
166+
if (NULL == cg) {
167+
nr_free(fg);
168+
return NR_FAILURE;
169+
}
170+
171+
fg->ctx_globals = cg;
172+
173+
nr_hashmap_update(fiber_globals_map, key, nr_strlen(key), fg);
174+
175+
return NR_SUCCESS;
176+
}
177+
178+
nr_status_t nr_remove_fiber_context_from_global_hashmap(
179+
nr_hashmap_t* fiber_globals_map,
180+
const char* key) {
181+
if (nr_strempty(key)) {
182+
return NR_FAILURE;
183+
}
184+
185+
if (NULL == fiber_globals_map) {
186+
return NR_FAILURE;
187+
}
188+
189+
return nr_hashmap_delete(fiber_globals_map, key, nr_strlen(key));
190+
}
191+
192+
nr_status_t nr_fiber_switch_global_context(nr_hashmap_t* fiber_globals_map,
193+
fiber_globals_t** fiber_global_ptr,
194+
const char* key) {
195+
fiber_globals_t* fg = NULL;
196+
197+
if (NULL == fiber_global_ptr) {
198+
return NR_FAILURE;
199+
}
200+
201+
if (NULL == key) {
202+
// a NULL key indicates we're in the MAIN php context rather than a fiber.
203+
// Set the fiber pointer to NULL to prevent using the fiber-specific
204+
// accessors.
205+
*fiber_global_ptr = NULL;
206+
return NR_SUCCESS;
207+
}
208+
209+
if (NULL == fiber_globals_map) {
210+
*fiber_global_ptr = NULL;
211+
return NR_FAILURE;
212+
}
213+
214+
fg = nr_hashmap_get(fiber_globals_map, key, nr_strlen(key));
215+
216+
if (NULL == fg) {
217+
*fiber_global_ptr = NULL;
218+
return NR_FAILURE;
219+
}
220+
221+
*fiber_global_ptr = fg;
222+
223+
return NR_SUCCESS;
224+
}
225+
226+
#endif // PHP 8.1+

0 commit comments

Comments
 (0)