diff --git a/agent/Makefile.frag b/agent/Makefile.frag index f1b07447b..6e6e76a62 100644 --- a/agent/Makefile.frag +++ b/agent/Makefile.frag @@ -85,6 +85,7 @@ TEST_BINARIES = \ tests/test_curl_md \ tests/test_datastore \ tests/test_environment \ + tests/test_fibers \ tests/test_fw_codeigniter \ tests/test_fw_drupal \ tests/test_fw_laravel_queue \ diff --git a/agent/config.m4 b/agent/config.m4 index 495ee1df2..7d0154111 100644 --- a/agent/config.m4 +++ b/agent/config.m4 @@ -213,7 +213,7 @@ if test "$PHP_NEWRELIC" = "yes"; then php_api_distributed_trace.c php_api_internal.c php_autorum.c \ php_call.c php_curl.c php_curl_md.c php_datastore.c php_environment.c \ php_error.c php_execute.c php_explain.c php_explain_mysqli.c \ - php_explain_pdo_mysql.c php_extension.c php_file_get_contents.c \ + php_explain_pdo_mysql.c php_extension.c php_fibers.c php_file_get_contents.c \ php_globals.c php_hash.c php_header.c php_httprequest_send.c \ php_internal_instrument.c php_memcached.c php_minit.c php_mshutdown.c php_mysql.c \ php_mysqli.c php_newrelic.c php_nrini.c php_observer.c php_output.c php_pdo.c \ diff --git a/agent/fw_drupal.c b/agent/fw_drupal.c index f663af20d..ad0991825 100644 --- a/agent/fw_drupal.c +++ b/agent/fw_drupal.c @@ -261,7 +261,7 @@ NR_PHP_WRAPPER(nr_drupal_http_request_before) { * after function properly */ NRPRG_CTX(drupal_http_request_segment) - = nr_segment_start(NRPRG(txn), NULL, NULL); + = NR_SEGMENT_START_WITH_PARENT_CONTEXT(NRPRG(txn), auto_segment); /* * The new segment needs to have the wraprec data attached, so that * fcall_end is able to properly dispatch to the after wrapper, as @@ -412,7 +412,7 @@ NR_PHP_WRAPPER(nr_drupal_http_request_exec) { external_params.procedure = nr_drupal_http_request_get_method(NR_EXECUTE_ORIG_ARGS TSRMLS_CC); - segment = nr_segment_start(NRPRG(txn), NULL, NULL); + segment = NR_SEGMENT_START_WITH_PARENT_CONTEXT(NRPRG(txn), auto_segment); /* * Our wrapper for drupal_http_request (which we installed in diff --git a/agent/lib_aws_sdk_php.c b/agent/lib_aws_sdk_php.c index b0bc66bdb..13ec35f95 100644 --- a/agent/lib_aws_sdk_php.c +++ b/agent/lib_aws_sdk_php.c @@ -111,7 +111,8 @@ void nr_lib_aws_sdk_php_kinesis_handle(nr_segment_t* auto_segment, * only create the segment now, grab the parent segment start time, add our * special segment attributes/metrics then close the newly created segment. */ - message_segment = nr_segment_start(NRPRG(txn), NULL, NULL); + message_segment + = NR_SEGMENT_START_WITH_PARENT_CONTEXT(NRPRG(txn), auto_segment); if (NULL == message_segment) { return; } @@ -395,7 +396,8 @@ void nr_lib_aws_sdk_php_sqs_handle(nr_segment_t* auto_segment, * only create the segment now, grab the parent segment start time, add our * special segment attributes/metrics then close the newly created segment. */ - message_segment = nr_segment_start(NRPRG(txn), NULL, NULL); + message_segment + = NR_SEGMENT_START_WITH_PARENT_CONTEXT(NRPRG(txn), auto_segment); if (NULL == message_segment) { return; } @@ -601,7 +603,8 @@ void nr_lib_aws_sdk_php_lambda_handle(nr_segment_t* auto_segment, * only create the segment now, grab the parent segment start time, add our * special segment attributes/metrics then close the newly created segment. */ - external_segment = nr_segment_start(NRPRG(txn), NULL, NULL); + external_segment + = NR_SEGMENT_START_WITH_PARENT_CONTEXT(NRPRG(txn), auto_segment); if (NULL == external_segment) { nr_free(cloud_attrs.cloud_resource_id); return; @@ -925,7 +928,8 @@ void nr_lib_aws_sdk_php_dynamodb_handle(nr_segment_t* auto_segment, * start time, add the special segment attributes/metrics then close the newly * created segment. */ - datastore_segment = nr_segment_start(NRPRG(txn), NULL, NULL); + datastore_segment + = NR_SEGMENT_START_WITH_PARENT_CONTEXT(NRPRG(txn), auto_segment); if (NULL != datastore_segment) { /* re-use start time from auto_segment started in func_begin */ datastore_segment->start_time = auto_segment->start_time; diff --git a/agent/lib_guzzle6.c b/agent/lib_guzzle6.c index 59592b1cf..2664be6f4 100644 --- a/agent/lib_guzzle6.c +++ b/agent/lib_guzzle6.c @@ -112,12 +112,22 @@ static void nr_guzzle6_requesthandler_handle_response(zval* handler, zval* request; zval* method; zval* status = NULL; + int old_context = 0; if (NR_FAILURE == nr_guzzle_obj_find_and_remove(handler, &segment TSRMLS_CC)) { return; } + /* + * This is a special case because guzzle stores all the external segments in + * the NRTXN(guzzle_objs) hashmap. When the extracted segment is ended, we + * don't want the context of this segment's parent to be set on the txn + * because for guzzle that is a special newrelic guzzle middleware segment. + * Save it now so we can restore it later. + */ + old_context = NRTXN(current_async_context); + request = nr_guzzle6_requesthandler_get_request(handler TSRMLS_CC); if (NULL == request) { return; @@ -137,16 +147,17 @@ static void nr_guzzle6_requesthandler_handle_response(zval* handler, if (NULL != response && nr_php_psr7_is_response(response TSRMLS_CC)) { /* - * Get the X-NewRelic-App-Data response header. If there isn't one, NULL is - * returned, and everything still works just fine. - */ - external_params.encoded_response_header - = nr_php_psr7_message_get_header(response, X_NEWRELIC_APP_DATA TSRMLS_CC); + * Get the X-NewRelic-App-Data response header. If there isn't one, NULL is + * returned, and everything still works just fine. + */ + external_params.encoded_response_header = nr_php_psr7_message_get_header( + response, X_NEWRELIC_APP_DATA TSRMLS_CC); if (NRPRG(txn) && NRTXN(special_flags.debug_cat)) { nrl_verbosedebug( NRL_CAT, "CAT: outbound response: transport='Guzzle 6' %s=" NRP_FMT, - X_NEWRELIC_APP_DATA, NRP_CAT(external_params.encoded_response_header)); + X_NEWRELIC_APP_DATA, + NRP_CAT(external_params.encoded_response_header)); } status = nr_php_call(response, "getStatusCode"); @@ -158,6 +169,8 @@ static void nr_guzzle6_requesthandler_handle_response(zval* handler, nr_segment_external_end(&segment, &external_params); + NRTXN(current_async_context) = old_context; + nr_free(external_params.encoded_response_header); nr_free(external_params.uri); nr_free(external_params.procedure); diff --git a/agent/lib_guzzle_common.c b/agent/lib_guzzle_common.c index 966748b8b..939f5c55c 100644 --- a/agent/lib_guzzle_common.c +++ b/agent/lib_guzzle_common.c @@ -83,16 +83,38 @@ nr_segment_t* nr_guzzle_obj_add(const zval* obj, const char* async_context_prefix TSRMLS_DC) { nr_segment_t* segment = NULL; char* async_context = NULL; + int old_context = 0; /* * Create the async context, in case there was parallelism. */ async_context = nr_guzzle_create_async_context_name(async_context_prefix, obj); + /* + * This is a special case where we don't want the context of the newly + * created segment to be set on the txn because this segment is tracked + * separately based off of the unique created guzzle async context. We need to + * save the actual context so we can restore it later. + */ + old_context = NRTXN(current_async_context); + + /* + * Must explicitly be parented to the current segment of the txn context; + * otherwise, it will be parented to the main NULL context. In non-fiber + * cases, this will be a segment on the default (NULL) context; otherwise, it + * will be the current segment on the actively executing fiber. + * + * Similar to curl_multi_exec curl handle segments, because async guzzle + * segments are stored separately in a hashmap, we don't want to this segment + * creation to affect the txn context. + */ - segment = nr_segment_start(NRPRG(txn), NULL, async_context); + segment = nr_segment_start(NRPRG(txn), + nr_txn_get_current_segment_txn_context(NRPRG(txn)), + async_context); nr_free(async_context); + NRTXN(current_async_context) = old_context; /* * Create the guzzle_objs hash table if we haven't already done so. @@ -108,6 +130,7 @@ nr_segment_t* nr_guzzle_obj_add(const zval* obj, * architecture, and saves us having to transform the object handle into a * string to use string keys. */ + nr_hashmap_index_update(NRTXNGLOBAL(guzzle_objs), (uint64_t)Z_OBJ_HANDLE_P(obj), segment); diff --git a/agent/lib_laminas_http.c b/agent/lib_laminas_http.c index dc87507af..686eb35a6 100644 --- a/agent/lib_laminas_http.c +++ b/agent/lib_laminas_http.c @@ -337,7 +337,7 @@ NR_PHP_WRAPPER_START(nr_zend_http_client_request) { goto leave; } - segment = nr_segment_start(NRPRG(txn), NULL, NULL); + segment = NR_SEGMENT_START_WITH_PARENT_CONTEXT(NRPRG(txn), auto_segment); /* * We have to manually force this segment as the current segment on @@ -422,6 +422,6 @@ NR_PHP_WRAPPER_START(nr_zend_http_client_request) { NR_PHP_WRAPPER_END void nr_laminas_http_enable(TSRMLS_D) { - nr_php_wrap_user_function(NR_PSTR(HTTP_CLIENT_REQUEST_L), - nr_zend_http_client_request TSRMLS_CC); + nr_php_wrap_user_function(NR_PSTR(HTTP_CLIENT_REQUEST_L), + nr_zend_http_client_request TSRMLS_CC); } diff --git a/agent/lib_mongodb.c b/agent/lib_mongodb.c index 28d2571c6..f3aede233 100644 --- a/agent/lib_mongodb.c +++ b/agent/lib_mongodb.c @@ -182,7 +182,7 @@ NR_PHP_WRAPPER_END NR_PHP_WRAPPER(nr_mongodb_operation_before) { (void)wraprec; nr_segment_t* segment = NULL; - segment = nr_segment_start(NRPRG(txn), NULL, NULL); + segment = NR_SEGMENT_START_WITH_PARENT_CONTEXT(NRPRG(txn), auto_segment); if (NULL != segment) { segment->wraprec = auto_segment->wraprec; } diff --git a/agent/lib_php_amqplib.c b/agent/lib_php_amqplib.c index 121445e6e..d9d5e6d89 100644 --- a/agent/lib_php_amqplib.c +++ b/agent/lib_php_amqplib.c @@ -621,7 +621,8 @@ NR_PHP_WRAPPER(nr_rabbitmq_basic_publish) { goto end; } - message_segment = nr_segment_start(NRPRG(txn), NULL, NULL); + message_segment + = NR_SEGMENT_START_WITH_PARENT_CONTEXT(NRPRG(txn), auto_segment); if (NULL != message_segment) { /* re-use start time from auto_segment started in func_begin */ message_segment->start_time = auto_segment->start_time; @@ -749,7 +750,8 @@ NR_PHP_WRAPPER(nr_rabbitmq_basic_get) { * time, add our message segment attributes/metrics then close the newly * created message segment. */ - message_segment = nr_segment_start(NRPRG(txn), NULL, NULL); + message_segment + = NR_SEGMENT_START_WITH_PARENT_CONTEXT(NRPRG(txn), auto_segment); if (NULL == message_segment) { goto end; diff --git a/agent/lib_predis.c b/agent/lib_predis.c index 7c8715104..80c94fdd2 100644 --- a/agent/lib_predis.c +++ b/agent/lib_predis.c @@ -81,16 +81,7 @@ const zend_long nr_predis_default_port = 6379; static NR_PHP_WRAPPER_PROTOTYPE(nr_predis_connection_readResponse); static NR_PHP_WRAPPER_PROTOTYPE(nr_predis_connection_writeRequest); -static void nr_predis_command_destroy(nrtime_t* time) { - nr_free(time); -} - static inline nr_hashmap_t* nr_predis_get_commands(TSRMLS_D) { - if (NULL == NRPRG_CTX(predis_commands)) { - NRPRG_CTX(predis_commands) - = nr_hashmap_create((nr_hashmap_dtor_func_t)nr_predis_command_destroy); - } - return NRPRG_CTX(predis_commands); } @@ -561,7 +552,13 @@ NR_PHP_WRAPPER(nr_predis_connection_readResponse) { async_context = nr_formatf("%s." NR_UINT64_FMT, ctx, index); } - segment = nr_segment_start(NRPRG(txn), NULL, async_context); + /* + * Must explicitly be parented to the parent segment; otherwise, it will + * parent to the main NULL context. In non-fiber cases, this will be a segment + * on the default (NULL) context; otherwise, it will be the current segment on + * the actively executing fiber. + */ + segment = nr_segment_start(NRPRG(txn), auto_segment, async_context); nr_segment_set_timing(segment, *start, duration); nr_segment_datastore_end(&segment, ¶ms); @@ -765,7 +762,12 @@ NR_PHP_WRAPPER(nr_predis_webdisconnection_executeCommand_before) { (void)wraprec; nr_segment_t* segment = NULL; - segment = nr_segment_start(NRPRG(txn), NULL, NULL); + /* + * Must be started in the context of its parent. + * In non-fiber cases, the context will be NULL(default context); otherwise, + * it will be the the context of the parent in an actively executing fiber. + */ + segment = NR_SEGMENT_START_WITH_PARENT_CONTEXT(NRPRG(txn), auto_segment); if (NULL != segment) { segment->wraprec = auto_segment->wraprec; } @@ -799,7 +801,7 @@ NR_PHP_WRAPPER(nr_predis_webdisconnection_executeCommand_after) { } NR_PHP_WRAPPER_END -#else +#else /* NON-OAPI, non-Fiber cases*/ NR_PHP_WRAPPER(nr_predis_webdisconnection_executeCommand) { char* operation = NULL; @@ -819,7 +821,9 @@ NR_PHP_WRAPPER(nr_predis_webdisconnection_executeCommand) { operation = nr_predis_get_operation_name_from_object(command_obj TSRMLS_CC); params.operation = operation; - + /* + * This segment start can remain untouched since it is a non-fiber scenario. + */ segment = nr_segment_start(NRPRG(txn), NULL, NULL); NR_PHP_WRAPPER_CALL; diff --git a/agent/php_api.c b/agent/php_api.c index f29d9ddec..d8b39f74f 100644 --- a/agent/php_api.c +++ b/agent/php_api.c @@ -1242,9 +1242,12 @@ static nr_status_t nr_php_api_add_custom_span_attribute(const char* keystr, TSRMLS_DC) { bool rv; char* key = NULL; - nr_segment_t* current; + nr_segment_t* current = NULL; + nrtxn_t* txn = NULL; - current = nr_txn_get_current_segment(NRPRG(txn), NULL); + txn = NRPRG(txn); + + current = nr_txn_get_current_segment_txn_context(txn); if (!current) { return NR_FAILURE; } @@ -1483,6 +1486,8 @@ PHP_FUNCTION(newrelic_get_linking_metadata) { NR_UNUSED_RETURN_VALUE_USED; NR_UNUSED_THIS_PTR; + nrtxn_t* txn = NULL; + nr_php_api_add_supportability_metric("get_linking_metadata" TSRMLS_CC); array_init(return_value); @@ -1495,6 +1500,8 @@ PHP_FUNCTION(newrelic_get_linking_metadata) { return; } + txn = NRPRG(txn); + if (nrlikely(NRPRG_SHARED(app))) { nr_php_add_assoc_string_const(return_value, "entity.name", nr_app_get_entity_name(NRPRG_SHARED(app))); @@ -1506,10 +1513,9 @@ PHP_FUNCTION(newrelic_get_linking_metadata) { nr_app_get_host_name(NRPRG_SHARED(app))); } - if (nrlikely(NRPRG(txn))) { - trace_id = nr_txn_get_current_trace_id(NRPRG(txn)); - span_id = nr_txn_get_current_span_id(NRPRG(txn)); - + if (nrlikely(txn)) { + trace_id = nr_txn_get_current_trace_id(txn); + span_id = nr_txn_get_current_span_id(txn, nr_txn_get_current_context(txn)); if (trace_id) { nr_php_add_assoc_string(return_value, "trace.id", trace_id); } @@ -1536,6 +1542,8 @@ PHP_FUNCTION(newrelic_get_trace_metadata) { NR_UNUSED_RETURN_VALUE_USED; NR_UNUSED_THIS_PTR; + nrtxn_t* txn = NULL; + nr_php_api_add_supportability_metric("get_trace_metadata" TSRMLS_CC); array_init(return_value); @@ -1548,14 +1556,15 @@ PHP_FUNCTION(newrelic_get_trace_metadata) { return; } - if (nrlikely(NRPRG(txn))) { - trace_id = nr_txn_get_current_trace_id(NRPRG(txn)); + txn = NRPRG(txn); + + if (nrlikely(txn)) { + trace_id = nr_txn_get_current_trace_id(txn); if (trace_id) { nr_php_add_assoc_string(return_value, "trace_id", trace_id); } nr_free(trace_id); - - span_id = nr_txn_get_current_span_id(NRPRG(txn)); + span_id = nr_txn_get_current_span_id(txn, nr_txn_get_current_context(txn)); if (span_id) { nr_php_add_assoc_string(return_value, "span_id", span_id); } @@ -1565,7 +1574,7 @@ PHP_FUNCTION(newrelic_get_trace_metadata) { /* * Purpose Allows a caller to add a user id string to agent attributes in - transaction event, transaction trace, error and span. + * transaction event, transaction trace, error and span. * */ #ifdef TAGS diff --git a/agent/php_api_datastore.c b/agent/php_api_datastore.c index cf3948084..6c9eb1c6e 100644 --- a/agent/php_api_datastore.c +++ b/agent/php_api_datastore.c @@ -53,7 +53,7 @@ zval* nr_php_api_datastore_validate(const HashTable* params) { nr_php_zval_free(&validated_params); return NULL; } else if (datastore_validators[i].default_value) { - nr_php_add_assoc_string(validated_params, key, + nr_php_add_assoc_string(validated_params, key, (char*)datastore_validators[i].default_value); } } else { @@ -142,7 +142,7 @@ PHP_FUNCTION(newrelic_record_datastore_segment) { } if (instrument) { - segment = nr_segment_start(NRPRG(txn), NULL, NULL); + segment = NR_SEGMENT_START_WITH_TXN_CONTEXT(NRPRG(txn)); #if ZEND_MODULE_API_NO < ZEND_8_0_X_API_NO \ || defined OVERWRITE_ZEND_EXECUTE_DATA /* not OAPI */ /* diff --git a/agent/php_api_distributed_trace.c b/agent/php_api_distributed_trace.c index c43a76429..c17e3eb1e 100644 --- a/agent/php_api_distributed_trace.c +++ b/agent/php_api_distributed_trace.c @@ -314,7 +314,7 @@ PHP_FUNCTION(newrelic_create_distributed_trace_payload) { * explicitly here. */ char* payload = nr_txn_create_distributed_trace_payload( - NRPRG(txn), nr_txn_get_current_segment(NRPRG(txn), NULL)); + NRPRG(txn), nr_txn_get_current_segment_txn_context(NRPRG(txn))); if (payload) { zend_update_property_string( @@ -378,11 +378,11 @@ PHP_FUNCTION(newrelic_insert_distributed_trace_headers) { * takes place in nr_txn_create_distributed_trace_payload. */ newrelic = nr_txn_create_distributed_trace_payload( - NRPRG(txn), nr_txn_get_current_segment(NRPRG(txn), NULL)); + NRPRG(txn), nr_txn_get_current_segment_txn_context(NRPRG(txn))); traceparent = nr_txn_create_w3c_traceparent_header( - NRPRG(txn), nr_txn_get_current_segment(NRPRG(txn), NULL)); + NRPRG(txn), nr_txn_get_current_segment_txn_context(NRPRG(txn))); tracestate = nr_txn_create_w3c_tracestate_header( - NRPRG(txn), nr_txn_get_current_segment(NRPRG(txn), NULL)); + NRPRG(txn), nr_txn_get_current_segment_txn_context(NRPRG(txn))); SEPARATE_ARRAY(header_array); diff --git a/agent/php_api_internal.c b/agent/php_api_internal.c index 05c6d4836..6a5902282 100644 --- a/agent/php_api_internal.c +++ b/agent/php_api_internal.c @@ -51,7 +51,7 @@ PHP_FUNCTION(newrelic_get_request_metadata) { array_init(return_value); outbound_headers = nr_header_outbound_request_create( - NRPRG(txn), nr_txn_get_current_segment(NRPRG(txn), NULL)); + NRPRG(txn), nr_txn_get_current_segment_txn_context(NRPRG(txn))); if (NULL == outbound_headers) { return; diff --git a/agent/php_curl.c b/agent/php_curl.c index a8df77b86..2be8258d2 100644 --- a/agent/php_curl.c +++ b/agent/php_curl.c @@ -656,6 +656,15 @@ void nr_php_curl_exec_pre(zval* curlres, const char* async_context TSRMLS_DC) { nr_segment_t* segment = NULL; char* uri = NULL; + int old_context = 0; + + /* + * This is a special case where we don't want the context of the newly + * created segment to be set on the txn because this segment is tracked + * separately based off of the curlres. We need to save the actual context + * so we can restore it later. + */ + old_context = NRTXN(current_async_context); uri = nr_php_curl_get_url(curlres TSRMLS_CC); @@ -672,6 +681,8 @@ void nr_php_curl_exec_pre(zval* curlres, */ nr_php_curl_exec_set_httpheaders(curlres, segment TSRMLS_CC); + NRTXN(current_async_context) = old_context; + nr_free(uri); } @@ -717,6 +728,7 @@ void nr_php_curl_multi_exec_pre(zval* curlres TSRMLS_DC) { zval* handle = NULL; nr_vector_t* handles = NULL; const char* async_context = NULL; + int old_context = 0; if (nr_php_curl_multi_md_is_initialized(curlres TSRMLS_CC)) { return; @@ -730,11 +742,34 @@ void nr_php_curl_multi_exec_pre(zval* curlres TSRMLS_DC) { * curl_multi_exec, the end time of the segment is updated. */ if (!nr_guzzle_in_call_stack(TSRMLS_C)) { + /* + * Must explicitly be parented to the current segment of the txn context. In + * non-fiber cases, this will be a segment on the default (NULL) context; + * otherwise, it will become the current segment on the actively executing + * fiber. + */ + + /* + * This is a special case where we don't want the context of the newly + * created segment to be set on the txn because this segment is tracked + * separately based off of the curlres. We need to save the actual context + * so we can restore it later. + */ + old_context = NRTXN(current_async_context); + segment = nr_segment_start( - NRPRG(txn), NULL, + NRPRG(txn), nr_txn_get_current_segment_txn_context(NRPRG(txn)), nr_php_curl_multi_md_get_async_context(curlres TSRMLS_CC)); + nr_segment_set_name(segment, "curl_multi_exec"); nr_php_curl_multi_md_set_segment(curlres, segment TSRMLS_CC); + + /* The current txn context needs to go back to the old context.*/ + NRTXN(current_async_context) = old_context; + + } else { + /* Get the correct parent in case we aren't in the default context. */ + segment = nr_txn_get_current_segment_txn_context(NRPRG(txn)); } /* diff --git a/agent/php_error.c b/agent/php_error.c index d1c1a4d85..dacc5e511 100644 --- a/agent/php_error.c +++ b/agent/php_error.c @@ -813,7 +813,7 @@ nr_status_t nr_php_error_record_exception_segment(nrtxn_t* txn, error_message = nr_formatf("%s'%s'", prefix, klass); } - nr_segment_record_exception(nr_txn_get_current_segment(NRPRG(txn), NULL), + nr_segment_record_exception(nr_txn_get_current_segment_txn_context(txn), error_message, klass); nr_free(file); diff --git a/agent/php_execute.c b/agent/php_execute.c index 3199629a7..97e018788 100644 --- a/agent/php_execute.c +++ b/agent/php_execute.c @@ -25,6 +25,7 @@ #include "util_metrics.h" #include "util_number_converter.h" #include "util_strings.h" +#include "php_txn.h" #include "util_url.h" #include "util_url.h" #include "util_metrics.h" @@ -1600,7 +1601,10 @@ void nr_php_execute_internal(zend_execute_data* execute_data, if (nrunlikely(NR_PHP_PROCESS_GLOBALS(special_flags).show_executes)) { nr_php_show_exec_internal(NR_EXECUTE_ORIG_ARGS_OVERWRITE, func TSRMLS_CC); } - segment = nr_segment_start(NRPRG(txn), NULL, NULL); + segment = nr_segment_start(NRPRG(txn), NRPRG_SHARED(fiber_parent_segment), + NRPRG_SHARED(current_php_context)); + NRPRG_SHARED(fiber_parent_segment) = NULL; + CALL_ORIGINAL; duration = nr_time_duration(segment->start_time, nr_txn_now_rel(NRPRG(txn))); @@ -1696,6 +1700,96 @@ void nr_php_user_instrumentation_from_opcache(TSRMLS_D) { #if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ && !defined OVERWRITE_ZEND_EXECUTE_DATA /* PHP8+ and OAPI */ +#if ZEND_MODULE_API_NO >= ZEND_8_1_X_API_NO +void nr_fiber_show_fiber(zend_fiber_context* zfc, const char* fiber_action) { + if (nrlikely(0 == NR_PHP_PROCESS_GLOBALS(special_flags).show_fibers)) { + return; + } + + zend_fiber* zfc_fiber = NULL; + char* zfc_status = NULL; + char* fiber_destroy_flag = NULL; + char* fiber_threw_flag = NULL; + char* fiber_bailout_flag = NULL; + char* fiber_func_name = NULL; + char* label = " fiber"; + + if (NULL == zfc) { + nrl_warning(NRL_AGENT, "PHP Issue: %s Fiber context is unexpectedly NULL.", + fiber_action); + return; + } + + switch (zfc->status) { + case ZEND_FIBER_STATUS_INIT: + zfc_status = "ZEND_FIBER_STATUS_INIT"; + break; + case ZEND_FIBER_STATUS_RUNNING: + zfc_status = "ZEND_FIBER_STATUS_RUNNING"; + break; + case ZEND_FIBER_STATUS_SUSPENDED: + zfc_status = "ZEND_FIBER_STATUS_SUSPENDED"; + break; + case ZEND_FIBER_STATUS_DEAD: + zfc_status = "ZEND_FIBER_STATUS_DEAD"; + break; + default: + zfc_status = "UNKNOWN STATUS"; + } + + if (zend_ce_fiber != zfc->kind) { + /* + * Return with what we have since we can't extract a fiber from the main + * context for any additional info. + */ + nrl_verbosedebug( + NRL_AGENT, "%s: %.*s %s:`main` context ctx={%p} status{%s}", label, + nr_php_show_exec_indentation(TSRMLS_C), nr_php_indentation_spaces, + NRSAFESTR(fiber_action), zfc, zfc_status); + return; + } + zfc_fiber = zend_fiber_from_context(zfc); + + if (NULL == zfc_fiber) { + nrl_verbosedebug( + NRL_INSTRUMENT, + "PHP Issue: The Fiber associated with %p is unexpectedly NULL.", zfc); + return; + } + if (zfc_fiber->flags & ZEND_FIBER_FLAG_DESTROYED) { + fiber_destroy_flag = "destroyed"; + } + + if (zfc_fiber->flags & ZEND_FIBER_FLAG_THREW) { + fiber_threw_flag = "threw"; + } + + if (zfc_fiber->flags & ZEND_FIBER_FLAG_BAILOUT) { + fiber_bailout_flag = "bailout"; + } + + /* + * Use fci_cache vs fci since fci.function_name will be set to undef in PHP + * when the fiber is dead and cannot be depended on. + */ + zend_fcall_info_cache zfc_fci_cache = zfc_fiber->fci_cache; + if (NULL != zfc_fci_cache.function_handler) { + fiber_func_name + = nr_php_function_debug_name(zfc_fci_cache.function_handler); + } + + nrl_verbosedebug( + NRL_AGENT, + "%s: %.*s %s: fiber context ctx={%p} function={%s} status{%s}" + "flags={%s|%s|%s}", + label, nr_php_show_exec_indentation(TSRMLS_C), nr_php_indentation_spaces, + NRSAFESTR(fiber_action), zfc, NRSAFESTR(fiber_func_name), + NRSAFESTR(zfc_status), NRSAFESTR(fiber_destroy_flag), + NRSAFESTR(fiber_threw_flag), NRSAFESTR(fiber_bailout_flag)); + nr_free(fiber_func_name); +} +#endif /* PHP 8.1+ */ + static void nr_php_observer_attempt_call_cufa_handler(NR_EXECUTE_PROTO) { NR_UNUSED_FUNC_RETURN_VALUE; if (NULL == execute_data->prev_execute_data) { @@ -1817,7 +1911,9 @@ static void nr_php_instrument_func_begin(NR_EXECUTE_PROTO) { } wraprec = nr_php_get_wraprec(execute_data->func); - segment = nr_segment_start(NRPRG(txn), NULL, NULL); + segment = nr_segment_start(NRPRG(txn), NRPRG_SHARED(fiber_parent_segment), + NRPRG_SHARED(current_php_context)); + NRPRG_SHARED(fiber_parent_segment) = NULL; if (nrunlikely(NULL == segment)) { nrl_verbosedebug(NRL_AGENT, "Error starting segment."); @@ -1890,7 +1986,8 @@ static void nr_php_instrument_func_end(NR_EXECUTE_PROTO) { /* * Get the current segment and return if null. */ - segment = nr_txn_get_current_segment(NRPRG(txn), NULL); + segment = nr_php_txn_get_current_segment_php_context(NRPRG(txn)); + if (nrunlikely(NULL == segment)) { /* * Most likely caused by txn ending prematurely and closing all segments. We @@ -1910,7 +2007,6 @@ static void nr_php_instrument_func_end(NR_EXECUTE_PROTO) { * _clean callbacks if they exist. If it doesn't exist, we exit as nothing * needs to be done. */ - if (NULL == segment->wraprec) { return; } @@ -2009,9 +2105,9 @@ static void nr_php_instrument_func_end(NR_EXECUTE_PROTO) { /* * Reassign segment to the current segment, as some before/after wraprecs * start and then stop a segment. If that happened, we want to ensure we - * get the now-current segment + * get the current segment with the correct context. */ - segment = nr_txn_get_current_segment(NRPRG(txn), NULL); + segment = nr_php_txn_get_current_segment_php_context(NRPRG(txn)); nr_php_execute_metadata_init(&metadata, NR_OP_ARRAY); nr_php_execute_segment_end(segment, &metadata, create_metric); nr_php_execute_metadata_release(&metadata); diff --git a/agent/php_execute.h b/agent/php_execute.h index b7fb23e30..c0151811f 100644 --- a/agent/php_execute.h +++ b/agent/php_execute.h @@ -30,6 +30,13 @@ extern void nr_php_execute_file(const zend_op_array* op_array, NR_EXECUTE_PROTO TSRMLS_DC); +#if ZEND_MODULE_API_NO >= ZEND_8_1_X_API_NO +/* + * Purpose: Log information about the fiber for a given fiber context + */ +extern void nr_fiber_show_fiber(zend_fiber_context* zfc, + const char* fiber_action); +#endif /* PHP 8.1+ */ /* * Purpose: Log information about the execute data in a given execution * context - either 'execute' (zend_execute) or 'observe' (fcall_init). diff --git a/agent/php_fibers.c b/agent/php_fibers.c new file mode 100644 index 000000000..7dfadcbb1 --- /dev/null +++ b/agent/php_fibers.c @@ -0,0 +1,226 @@ +/* + * Copyright 2026 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * This file handles fibers instrumentation. + */ + +#include "php_includes.h" +#include "php_compat.h" +#include "php_fibers.h" +#include "php_agent.h" +#include "php_newrelic.h" +#include "php_zval.h" +#include "util_hashmap.h" +#include "util_logging.h" +#include "util_memory.h" +#include "util_stack.h" +#include "util_strings.h" +#include "zend_types.h" + +#if ZEND_MODULE_API_NO >= ZEND_8_1_X_API_NO + +#define COPY_BASIC(x) (dest->x = src->x) +#define COPY_POINTER(x) (dest->x = &src->x) +#define COPY_STRING(x) (dest->x = src->x ? nr_strdup(src->x) : NULL) +#define COPY_STACK(x, y) (dest->x = nr_stack_copy(&src->x, y)) + +static void* copy_elem_zval(void* elem) { + if (NULL == elem) { + return NULL; + } + zval* copy = nr_php_zval_alloc(); + ZVAL_DUP(copy, elem); + return copy; +} + +static void* copy_elem_ident(void* elem) { + return elem; +} + +static void* copy_elem_str(void* elem) { + return nr_strdup((char*)elem); +} + +static void fiber_str_dtor(void* e, NRUNUSED void* d) { + nr_free(e); +} + +static void nr_ctx_global_deep_copy(ctx_globals_t* dest, ctx_globals_t* src) { + if (NULL == dest) { + return; + } + + COPY_STRING(doctrine_dql); + + dest->drupal_http_request_depth = 0; + + COPY_BASIC(php_cur_stack_depth); + + COPY_STRING(mysql_last_conn); + COPY_STRING(pgsql_last_conn); + + COPY_BASIC(datastore_connections); + + COPY_BASIC(deprecated_capture_request_parameters); + COPY_BASIC(check_cufa); + + COPY_BASIC(predis_commands); + + dest->context_root = NULL; + + COPY_STACK(drupal_invoke_all_hooks, copy_elem_zval); + COPY_STACK(drupal_invoke_all_states, copy_elem_ident); + + dest->drupal_http_request_segment = NULL; + + COPY_STACK(wordpress_tags, copy_elem_str); + dest->wordpress_tags.dtor = fiber_str_dtor; + COPY_STACK(wordpress_tag_states, copy_elem_ident); + COPY_STACK(predis_ctxs, copy_elem_str); +} + +#undef COPY_STACK +#undef COPY_STRING +#undef COPY_BASIC + +ctx_globals_t* nr_fiber_copy_ctx_globals(ctx_globals_t* src) { + ctx_globals_t* fiber_ctx_globals = NULL; + + if (NULL == src) { + return NULL; + } + + fiber_ctx_globals = nr_malloc(sizeof(ctx_globals_t)); + + nr_ctx_global_deep_copy(fiber_ctx_globals, src); + + return fiber_ctx_globals; +} + +void free_fiber_globals(void* fiber_globals) { + fiber_globals_t* f = (fiber_globals_t*)fiber_globals; + + if (NULL == fiber_globals) { + nrl_warning(NRL_AGENT, "Failed to free fiber globals, target is NULL"); + return; + } + + if (NULL == f->ctx_globals) { + nrl_warning(NRL_AGENT, "Failed to free fiber globals, ctx_globals is NULL"); + nr_free(f); + return; + } + + nr_free(f->ctx_globals->doctrine_dql); + nr_free(f->ctx_globals->mysql_last_conn); + nr_free(f->ctx_globals->pgsql_last_conn); + nr_stack_destroy_fields(&f->ctx_globals->drupal_invoke_all_hooks); + nr_stack_destroy_fields(&f->ctx_globals->drupal_invoke_all_states); + nr_stack_destroy_fields(&f->ctx_globals->wordpress_tags); + nr_stack_destroy_fields(&f->ctx_globals->wordpress_tag_states); + nr_stack_destroy_fields(&f->ctx_globals->predis_ctxs); + nr_free(f->ctx_globals); + nr_free(f); +} + +nr_status_t nr_fiber_init_global_hashmap(nr_hashmap_t** fiber_globals_map) { + if (NULL == fiber_globals_map) { + return NR_FAILURE; + } + if (NULL != *fiber_globals_map) { + return NR_FAILURE; + } + + *fiber_globals_map = nr_hashmap_create(free_fiber_globals); + return NR_SUCCESS; +} + +nr_status_t nr_fiber_destroy_global_hashmap(nr_hashmap_t** fiber_globals_map) { + if (NULL == fiber_globals_map) { + return NR_FAILURE; + } + if (NULL != *fiber_globals_map) { + nr_hashmap_destroy(fiber_globals_map); + } + return NR_SUCCESS; +} + +nr_status_t nr_add_fiber_context_to_global_hashmap( + nr_hashmap_t* fiber_globals_map, + ctx_globals_t* src_ctx_globals, + const char* key) { + fiber_globals_t* fg = NULL; + ctx_globals_t* cg = NULL; + + if (NULL == key || nr_strlen(key) < 1) { + return NR_FAILURE; + } + + if (NULL == fiber_globals_map) { + return NR_FAILURE; + } + + fg = nr_malloc(sizeof(fiber_globals_t)); + cg = nr_fiber_copy_ctx_globals(src_ctx_globals); + if (NULL == cg) { + nr_free(fg); + return NR_FAILURE; + } + + fg->ctx_globals = cg; + + nr_hashmap_update(fiber_globals_map, key, nr_strlen(key), fg); + + return NR_SUCCESS; +} + +nr_status_t nr_remove_fiber_context_from_global_hashmap( + nr_hashmap_t* fiber_globals_map, + const char* key) { + if (nr_strempty(key)) { + return NR_FAILURE; + } + + if (NULL == fiber_globals_map) { + return NR_FAILURE; + } + + return nr_hashmap_delete(fiber_globals_map, key, nr_strlen(key)); +} + +nr_status_t nr_fiber_switch_global_context(nr_hashmap_t* fiber_globals_map, + fiber_globals_t** fiber_global_ptr, + const char* key) { + fiber_globals_t* fg = NULL; + + if (NULL == fiber_global_ptr) { + return NR_FAILURE; + } + + if (NULL == key) { + // a NULL key indicates we're in the MAIN php context rather than a fiber. + // Set the fiber pointer to NULL to prevent using the fiber-specific + // accessors. + *fiber_global_ptr = NULL; + return NR_SUCCESS; + } + + if (NULL == fiber_globals_map) { + *fiber_global_ptr = NULL; + return NR_FAILURE; + } + + fg = nr_hashmap_get(fiber_globals_map, key, nr_strlen(key)); + + if (NULL == fg) { + *fiber_global_ptr = NULL; + return NR_FAILURE; + } + + *fiber_global_ptr = fg; + + return NR_SUCCESS; +} + +#endif // PHP 8.1+ diff --git a/agent/php_fibers.h b/agent/php_fibers.h new file mode 100644 index 000000000..98c0e4099 --- /dev/null +++ b/agent/php_fibers.h @@ -0,0 +1,141 @@ +/* + * Copyright 2026 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef NEWRELIC_PHP_AGENT_PHP_FIBERS_H +#define NEWRELIC_PHP_AGENT_PHP_FIBERS_H + +#if ZEND_MODULE_API_NO >= ZEND_8_1_X_API_NO + +#include "php_newrelic.h" + +/* + * Purpose : Allocate and deep-copy the given context globals into a new + * ctx_globals_t suitable for use by a fiber. + * + * Params : 1. A pointer to the source ctx_globals_t to copy from. Must be + * non-NULL. The source is not modified and remains owned by + * the caller; typically this is &NRPRG(ctx). + * + * Returns : A pointer to a newly allocated ctx_globals_t. Ownership of the + * returned struct is transferred to the caller, which is responsible + * for freeing it. + */ +extern ctx_globals_t* nr_fiber_copy_ctx_globals(ctx_globals_t* src); + +/* + * Purpose : Free a fiber_globals_t and all owned resources held by its + * contained ctx_globals + * + * Intended to be used as the destructor callback registered with + * the fiber globals hashmap. + * + * Params : 1. A pointer to a fiber_globals_t, passed as void* to satisfy + * the hashmap destructor signature. + */ +extern void free_fiber_globals(void* fiber_globals); + +/* + * Purpose : Initialize the per-request hashmap that maps fiber context keys + * to their saved fiber_globals_t snapshots. + * + * If *fiber_globals_map is NULL, a new hashmap is allocated with + * free_fiber_globals as its destructor (so that removed or replaced + * entries are fully cleaned up) and stored back into + * *fiber_globals_map. If *fiber_globals_map already refers to a + * hashmap, it is left unchanged and NR_FAILURE is returned so the + * existing hashmap is not overwritten (and leaked). + * + * Params : 1. Address of the fiber globals hashmap pointer to initialize; + * typically &NRPRG(fiber_globals_map). Must be non-NULL, and + * *fiber_globals_map must be NULL. + * + * Returns : NR_SUCCESS if a new hashmap was allocated and stored; + * NR_FAILURE if fiber_globals_map is NULL or *fiber_globals_map + * was already non-NULL. + */ +extern nr_status_t nr_fiber_init_global_hashmap( + nr_hashmap_t** fiber_globals_map); + +/* + * Purpose : Destroy the per-request fiber globals hashmap, freeing every + * fiber_globals_t snapshot it contains via free_fiber_globals, and + * set *fiber_globals_map to NULL. + * + * Params : 1. Address of the fiber globals hashmap pointer to destroy; + * typically &NRPRG(fiber_globals_map). Safe to call when + * *fiber_globals_map is NULL. + * + * Returns : NR_SUCCESS if the operation was performed (the parameter was + * non-NULL), NR_FAILURE otherwise. + */ +extern nr_status_t nr_fiber_destroy_global_hashmap( + nr_hashmap_t** fiber_globals_map); + +/* + * Purpose : Deep-copy the given context globals into a new fiber_globals_t + * snapshot and store it in the given fiber globals hashmap + * under the given key. Used when a fiber is suspended so + * its globals can be restored on resume. + * + * Params : 1. The fiber globals hashmap into which the snapshot should be + * stored; typically NRPRG(fiber_globals_map). Must be non-NULL + * and previously initialized via nr_fiber_init_global_hashmap. + * 2. A pointer to the source ctx_globals_t to snapshot from; + * typically &NRPRG(ctx). The source is not modified and remains + * owned by the caller. + * 3. The key identifying the fiber whose globals are being saved. + * + * Returns : NR_SUCCESS on success, or NR_FAILURE if the key is invalid or + * the fiber globals hashmap is NULL. + */ +extern nr_status_t nr_add_fiber_context_to_global_hashmap( + nr_hashmap_t* fiber_globals_map, + ctx_globals_t* src_ctx_globals, + const char* key); + +/* + * Purpose : Remove the fiber globals snapshot associated with the given key + * from the given fiber globals hashmap, freeing the snapshot. + * + * Params : 1. The fiber globals hashmap from which the snapshot should be + * removed; typically NRPRG(fiber_globals_map). Must be non-NULL. + * 2. The key identifying the fiber whose globals should be removed. + * + * Returns : NR_SUCCESS on success, or NR_FAILURE if the key is invalid, the + * hashmap is NULL, or no entry exists for the key. + */ +extern nr_status_t nr_remove_fiber_context_from_global_hashmap( + nr_hashmap_t* fiber_globals_map, + const char* key); + +/* + * Purpose : Switch the active fiber globals pointer to the snapshot stored + * in the given fiber globals hashmap under the given key. Used when + * a fiber is resumed so that subsequent instrumentation operates + * against that fiber's saved context state. + * + * On success, the snapshot retrieved from the hashmap is written + * into the caller-supplied destination pointer; typically this is + * NRPRG(fiber_globals). + * + * Params : 1. The fiber globals hashmap to look up the snapshot in; + * typically NRPRG(fiber_globals_map). Must be non-NULL. + * 2. The destination where the active snapshot pointer should be + * written; typically &NRPRG(fiber_globals). The snapshot itself + * remains owned by the hashmap and must not be freed by the + * caller. + * 3. The key identifying the fiber whose globals should become + * active. + * + * Returns : NR_SUCCESS on success, or NR_FAILURE if the key is invalid, the + * hashmap is NULL, or no snapshot is stored under the given key. + */ +extern nr_status_t nr_fiber_switch_global_context( + nr_hashmap_t* fiber_globals_map, + fiber_globals_t** fiber_global_ptr, + const char* key); + +#endif // PHP 8.1+ +#endif // NEWRELIC_PHP_AGENT_PHP_FIBERS_H diff --git a/agent/php_file_get_contents.c b/agent/php_file_get_contents.c index fa6029fbd..6347d0505 100644 --- a/agent/php_file_get_contents.c +++ b/agent/php_file_get_contents.c @@ -140,8 +140,8 @@ static void nr_php_file_get_contents_add_headers_internal(zval* context, return; } - if (nr_stridx(Z_STRVAL_P(http_header), W3C_TRACESTATE":") != -1 && - nr_stridx(headers, W3C_TRACESTATE":") != -1) { + if (nr_stridx(Z_STRVAL_P(http_header), W3C_TRACESTATE ":") != -1 + && nr_stridx(headers, W3C_TRACESTATE ":") != -1) { /* Distributed Tracing headers already present and we are trying to add them again, don't add duplicates. */ return; @@ -511,7 +511,7 @@ PHP_FUNCTION(newrelic_add_headers_to_context) { return; } nr_php_file_get_contents_add_headers( - context, nr_txn_get_current_segment(NRPRG(txn), NULL) TSRMLS_CC); + context, nr_txn_get_current_segment_txn_context(NRPRG(txn)) TSRMLS_CC); } /* Test scaffolding */ diff --git a/agent/php_globals.h b/agent/php_globals.h index 4a1ede1c8..a1539220d 100644 --- a/agent/php_globals.h +++ b/agent/php_globals.h @@ -50,10 +50,11 @@ typedef struct _nrphpglobals_t { #if ZEND_MODULE_API_NO >= ZEND_8_1_X_API_NO /* PHP 8.1+ */ zend_long zend_offset; /* Zend extension offset */ #else - int zend_offset; /* Zend extension offset */ + int zend_offset; /* Zend extension offset */ #endif #if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO /* PHP 8.0+ */ - int op_array_extension_handle; /* Zend op_array extension handle to attach agent's data to function */ + int op_array_extension_handle; /* Zend op_array extension handle to attach + agent's data to function */ #endif int done_instrumentation; /* Set to true if we have installed instrumentation handlers */ @@ -100,6 +101,7 @@ typedef struct _nrphpglobals_t { uint8_t show_execute_stack; uint8_t show_execute_returns; uint8_t show_executes_untrimmed; + uint8_t show_fibers; uint8_t no_exception_handler; uint8_t no_signal_handler; uint8_t debug_autorum; diff --git a/agent/php_internal_instrument.c b/agent/php_internal_instrument.c index 8d6e3c192..fadd6ab61 100644 --- a/agent/php_internal_instrument.c +++ b/agent/php_internal_instrument.c @@ -531,7 +531,7 @@ NR_INNER_WRAPPER(mysql_query) { return; } - segment = nr_segment_start(NRPRG(txn), NULL, NULL); + segment = NR_SEGMENT_START_WITH_TXN_CONTEXT(NRPRG(txn)); zcaught = nr_zend_call_old_handler(nr_wrapper->oldhandler, INTERNAL_FUNCTION_PARAM_PASSTHRU); @@ -582,7 +582,7 @@ NR_INNER_WRAPPER(mysql_db_query) { return; } - segment = nr_segment_start(NRPRG(txn), NULL, NULL); + segment = NR_SEGMENT_START_WITH_TXN_CONTEXT(NRPRG(txn)); zcaught = nr_zend_call_old_handler(nr_wrapper->oldhandler, INTERNAL_FUNCTION_PARAM_PASSTHRU); @@ -790,7 +790,7 @@ static void nr_php_instrument_datastore_operation_call( }, }; - segment = nr_segment_start(NRPRG(txn), NULL, NULL); + segment = NR_SEGMENT_START_WITH_TXN_CONTEXT(NRPRG(txn)); zcaught = nr_zend_call_old_handler(nr_wrapper->oldhandler, INTERNAL_FUNCTION_PARAM_PASSTHRU); @@ -1051,8 +1051,7 @@ NR_INNER_WRAPPER(mysqli_general_query) { } } - segment = nr_segment_start(NRPRG(txn), NULL, NULL); - + segment = NR_SEGMENT_START_WITH_TXN_CONTEXT(NRPRG(txn)); zcaught = nr_zend_call_old_handler(nr_wrapper->oldhandler, INTERNAL_FUNCTION_PARAM_PASSTHRU); @@ -1176,7 +1175,7 @@ NR_INNER_WRAPPER(pdostatement_execute) { sqlstr = nr_php_prepared_statement_find(stmt_obj, "pdo" TSRMLS_CC); sqlstrlen = nr_strlen(sqlstr); - segment = nr_segment_start(NRPRG(txn), NULL, NULL); + segment = NR_SEGMENT_START_WITH_TXN_CONTEXT(NRPRG(txn)); zcaught = nr_zend_call_old_handler(nr_wrapper->oldhandler, INTERNAL_FUNCTION_PARAM_PASSTHRU); @@ -1304,21 +1303,20 @@ NR_INNER_WRAPPER(mysqli_stmt_execute) { if (FAILURE == zend_parse_parameters_ex(ZEND_PARSE_PARAMS_QUIET, - ZEND_NUM_ARGS() TSRMLS_CC, "o|a!", - &stmt_obj, ¶ms)) { + ZEND_NUM_ARGS() TSRMLS_CC, "o|a!", &stmt_obj, + ¶ms)) { stmt_obj = NR_PHP_INTERNAL_FN_THIS(); if (FAILURE - == zend_parse_parameters_ex(ZEND_PARSE_PARAMS_QUIET, - ZEND_NUM_ARGS() TSRMLS_CC, "|a!", - ¶ms)) { - nrl_warning(NRL_INSTRUMENT, - "failed to parse mysqli_stmt_execute params"); + == zend_parse_parameters_ex(ZEND_PARSE_PARAMS_QUIET, + ZEND_NUM_ARGS() TSRMLS_CC, "|a!", + ¶ms)) { + nrl_warning(NRL_INSTRUMENT, "failed to parse mysqli_stmt_execute params"); } } sqlstr = nr_php_prepared_statement_find(stmt_obj, "mysqli" TSRMLS_CC); sqlstrlen = nr_strlen(sqlstr); - segment = nr_segment_start(NRPRG(txn), NULL, NULL); + segment = NR_SEGMENT_START_WITH_TXN_CONTEXT(NRPRG(txn)); zcaught = nr_zend_call_old_handler(nr_wrapper->oldhandler, INTERNAL_FUNCTION_PARAM_PASSTHRU); @@ -1332,8 +1330,7 @@ NR_INNER_WRAPPER(mysqli_stmt_execute) { && nr_php_mysqli_zval_is_stmt(stmt_obj TSRMLS_CC) && !nr_php_is_zval_null(return_value)) { plan = nr_php_explain_mysqli_stmt(NRPRG(txn), Z_OBJ_HANDLE_P(stmt_obj), - params, - segment->start_time, + params, segment->start_time, segment->stop_time TSRMLS_CC); } @@ -1857,7 +1854,8 @@ NR_INNER_WRAPPER(pg_execute) { query = NR_PHP_PREPARED_STATEMENT_UNKNOWN; } - segment = nr_segment_start(NRPRG(txn), NULL, NULL); + segment = NR_SEGMENT_START_WITH_TXN_CONTEXT(NRPRG(txn)); + zcaught = nr_zend_call_old_handler(nr_wrapper->oldhandler, INTERNAL_FUNCTION_PARAM_PASSTHRU); nr_php_txn_end_segment_sql(&segment, query, nr_strlen(query), NULL, @@ -1970,7 +1968,8 @@ NR_INNER_WRAPPER(pg_query) { */ instance = nr_php_pgsql_retrieve_datastore_instance(pgsql_link TSRMLS_CC); - segment = nr_segment_start(NRPRG(txn), NULL, NULL); + segment = NR_SEGMENT_START_WITH_TXN_CONTEXT(NRPRG(txn)); + zcaught = nr_zend_call_old_handler(nr_wrapper->oldhandler, INTERNAL_FUNCTION_PARAM_PASSTHRU); @@ -2033,7 +2032,7 @@ NR_INNER_WRAPPER(pg_query_params) { } } - segment = nr_segment_start(NRPRG(txn), NULL, NULL); + segment = NR_SEGMENT_START_WITH_TXN_CONTEXT(NRPRG(txn)); zcaught = nr_zend_call_old_handler(nr_wrapper->oldhandler, INTERNAL_FUNCTION_PARAM_PASSTHRU); @@ -2087,7 +2086,7 @@ NR_INNER_WRAPPER(sqlite_query_function) { } } - segment = nr_segment_start(NRPRG(txn), NULL, NULL); + segment = NR_SEGMENT_START_WITH_TXN_CONTEXT(NRPRG(txn)); zcaught = nr_zend_call_old_handler(nr_wrapper->oldhandler, INTERNAL_FUNCTION_PARAM_PASSTHRU); @@ -2139,7 +2138,7 @@ NR_INNER_WRAPPER(sqlite_exec_or_query) { } } - segment = nr_segment_start(NRPRG(txn), NULL, NULL); + segment = NR_SEGMENT_START_WITH_TXN_CONTEXT(NRPRG(txn)); zcaught = nr_zend_call_old_handler(nr_wrapper->oldhandler, INTERNAL_FUNCTION_PARAM_PASSTHRU); @@ -2172,7 +2171,7 @@ NR_INNER_WRAPPER(sqlite3) { sqlstrlen = nr_strlen(sqlstr); } - segment = nr_segment_start(NRPRG(txn), NULL, NULL); + segment = NR_SEGMENT_START_WITH_TXN_CONTEXT(NRPRG(txn)); zcaught = nr_zend_call_old_handler(nr_wrapper->oldhandler, INTERNAL_FUNCTION_PARAM_PASSTHRU); @@ -2207,7 +2206,7 @@ NR_INNER_WRAPPER(sqlite3_querysingle) { sqlstrlen = nr_strlen(sqlstr); } - segment = nr_segment_start(NRPRG(txn), NULL, NULL); + segment = NR_SEGMENT_START_WITH_TXN_CONTEXT(NRPRG(txn)); zcaught = nr_zend_call_old_handler(nr_wrapper->oldhandler, INTERNAL_FUNCTION_PARAM_PASSTHRU); @@ -2288,7 +2287,7 @@ NR_INNER_WRAPPER(pdo_exec) { sqlstrlen = nr_strlen(sqlstr); } - segment = nr_segment_start(NRPRG(txn), NULL, NULL); + segment = NR_SEGMENT_START_WITH_TXN_CONTEXT(NRPRG(txn)); zcaught = nr_zend_call_old_handler(nr_wrapper->oldhandler, INTERNAL_FUNCTION_PARAM_PASSTHRU); @@ -2332,7 +2331,7 @@ NR_INNER_WRAPPER(pdo_query) { sqlstrlen = nr_strlen(sqlstr); } - segment = nr_segment_start(NRPRG(txn), NULL, NULL); + segment = NR_SEGMENT_START_WITH_TXN_CONTEXT(NRPRG(txn)); zcaught = nr_zend_call_old_handler(nr_wrapper->oldhandler, INTERNAL_FUNCTION_PARAM_PASSTHRU); @@ -2440,7 +2439,15 @@ NR_INNER_WRAPPER(curl_exec) { return; } - nr_php_curl_exec_pre(curlres, NULL, NULL TSRMLS_CC); + /* + * This function creates a segment that is then stored in the + * curl_multi_metadata hashmap. Because it is creating the segment, we need + * to ensure it is properly parented with the correct context and parent in + * the case of a fiber environment. + */ + nr_php_curl_exec_pre(curlres, + nr_txn_get_current_segment_txn_context(NRPRG(txn)), + nr_txn_get_current_context(NRPRG(txn)) TSRMLS_CC); zcaught = nr_zend_call_old_handler(nr_wrapper->oldhandler, INTERNAL_FUNCTION_PARAM_PASSTHRU); @@ -2504,6 +2511,7 @@ NR_INNER_WRAPPER(curl_multi_remove_handle) { zval* multires = NULL; zval* curlres = NULL; int rv = FAILURE; + int old_context = 0; #if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO /* PHP 8.0+ */ rv = zend_parse_parameters_ex(ZEND_PARSE_PARAMS_QUIET, @@ -2515,6 +2523,13 @@ NR_INNER_WRAPPER(curl_multi_remove_handle) { &curlres); #endif /* PHP 8.0+ */ + /* + * This is a special case where we don't want the parent context of the newly + * discarded segment to be set on the txn because curl segments are tracked + * separately. We need to save the actual context so we can restore it later. + */ + old_context = NRTXN(current_async_context); + if (SUCCESS == rv) { if (nr_php_curl_multi_md_remove(multires, curlres TSRMLS_CC)) { /* @@ -2526,7 +2541,8 @@ NR_INNER_WRAPPER(curl_multi_remove_handle) { } } } - + /* The current txn context needs to go back to the old context.*/ + NRTXN(current_async_context) = old_context; nr_wrapper->oldhandler(INTERNAL_FUNCTION_PARAM_PASSTHRU); } @@ -2593,7 +2609,7 @@ NR_INNER_WRAPPER(mssql_query) { return; } - segment = nr_segment_start(NRPRG(txn), NULL, NULL); + segment = NR_SEGMENT_START_WITH_TXN_CONTEXT(NRPRG(txn)); zcaught = nr_zend_call_old_handler(nr_wrapper->oldhandler, INTERNAL_FUNCTION_PARAM_PASSTHRU); @@ -2653,7 +2669,8 @@ NR_INNER_WRAPPER(mongocollection_15) { params.collection = collection_name; - segment = nr_segment_start(NRPRG(txn), NULL, NULL); + segment = NR_SEGMENT_START_WITH_TXN_CONTEXT(NRPRG(txn)); + zcaught = nr_zend_call_old_handler(nr_wrapper->oldhandler, INTERNAL_FUNCTION_PARAM_PASSTHRU); nr_segment_datastore_end(&segment, ¶ms); @@ -2745,7 +2762,7 @@ NR_INNER_WRAPPER(oci_execute) { sqlstr = nr_php_prepared_statement_find(stmt_obj, "oci" TSRMLS_CC); sqlstrlen = nr_strlen(sqlstr); - segment = nr_segment_start(NRPRG(txn), NULL, NULL); + segment = NR_SEGMENT_START_WITH_TXN_CONTEXT(NRPRG(txn)); zcaught = nr_zend_call_old_handler(nr_wrapper->oldhandler, INTERNAL_FUNCTION_PARAM_PASSTHRU); @@ -2829,7 +2846,7 @@ NR_INNER_WRAPPER(file_get_contents) { external_params.uri = nr_strndup(Z_STRVAL_P(file_zval), Z_STRLEN_P(file_zval)); - segment = nr_segment_start(NRPRG(txn), NULL, NULL); + segment = NR_SEGMENT_START_WITH_TXN_CONTEXT(NRPRG(txn)); nr_php_file_get_contents_add_headers(context, segment TSRMLS_CC); zcaught = nr_zend_call_old_handler(nr_wrapper->oldhandler, @@ -2937,7 +2954,7 @@ NR_INNER_WRAPPER(httprequest_send) { this_var = NR_PHP_INTERNAL_FN_THIS(); - segment = nr_segment_start(NRPRG(txn), NULL, NULL); + segment = NR_SEGMENT_START_WITH_TXN_CONTEXT(NRPRG(txn)); nr_php_httprequest_send_request_headers(this_var, segment TSRMLS_CC); external_params.uri = nr_php_httprequest_send_get_url(this_var TSRMLS_CC); @@ -2994,7 +3011,8 @@ NR_INNER_WRAPPER(soapclient_dorequest) { external_params.uri = nr_strndup(loc, loc_len); } - segment = nr_segment_start(NRPRG(txn), NULL, NULL); + segment = NR_SEGMENT_START_WITH_TXN_CONTEXT(NRPRG(txn)); + zcaught = nr_zend_call_old_handler(nr_wrapper->oldhandler, INTERNAL_FUNCTION_PARAM_PASSTHRU); nr_segment_external_end(&segment, &external_params); diff --git a/agent/php_newrelic.h b/agent/php_newrelic.h index c2a5a0c6f..bd1a48bb5 100644 --- a/agent/php_newrelic.h +++ b/agent/php_newrelic.h @@ -408,6 +408,23 @@ typedef struct _ini_t { } ini_t; typedef struct _shared_globals_t { + /* + * Pointer to the current context string. + * If NULL, execution is occuring in the main PHP Process. + * Otherwise it will point to the fiber context string. + */ + const char* current_php_context; + + /* Contains the fiber address as a string to use as the async_context.*/ + char fiber_context_string[32]; + + /* + * Contains the parent segment of a fiber only when starting a fiber within a + * fiber. For all other cases will be null which indicates that the main PHP + * process is the parent. + */ + nr_segment_t* fiber_parent_segment; + nrframework_t current_framework; // Current request framework (forced or detected) bool wordpress_plugins; // set based on @@ -507,6 +524,8 @@ typedef struct _ctx_globals_t { nr_hashmap_t* predis_commands; + nr_segment_t* context_root; // Pointer to the root segment of a fiber + #if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO /* Without OAPI, we are able to utilize the call stack to keep track * of the previous hooks. With OAPI, we can no longer do this so @@ -557,6 +576,10 @@ typedef struct _txn_globals_t { nr_hashmap_t* prepared_statements; // Prepared statement storage } txn_globals_t; +typedef struct _fiber_globals_t { + ctx_globals_t* ctx_globals; +} fiber_globals_t; + /* * Globals */ @@ -571,6 +594,12 @@ nrtxn_t* txn; // The all-important transaction pointer txn_globals_t txn_globals; // Transaction Globals +nr_hashmap_t* + fiber_globals_map; // Hashmap containing all tracked fiber global contexts + +fiber_globals_t* fiber_globals; // Pointer to the current fiber global context, + // or NULL if main + ZEND_END_MODULE_GLOBALS(newrelic) /* @@ -601,10 +630,18 @@ extern PHP_GSHUTDOWN_FUNCTION(newrelic); #define NRINI(Y) (NRPRG(ini).Y.value) #define NRPRG_SHARED(Y) (NRPRG(shared).Y) -#define NRPRG_CTX(Y) (NRPRG(ctx).Y) #define NRTXN(Y) (NRPRG(txn)->Y) #define NRTXNGLOBAL(Y) (NRPRG(txn_globals).Y) +#if ZEND_MODULE_API_NO >= ZEND_8_1_X_API_NO +#define NRPRG_CTX(Y) \ + ((NULL != NRPRG(fiber_globals) ? NRPRG(fiber_globals)->ctx_globals \ + : &NRPRG(ctx)) \ + ->Y) +#else +#define NRPRG_CTX(Y) (NRPRG(ctx).Y) +#endif + static inline int nr_php_recording(TSRMLS_D) { if (nrlikely((0 != NRPRG(txn)) && (0 != NRPRG(txn)->status.recording))) { return 1; diff --git a/agent/php_nrini.c b/agent/php_nrini.c index f0ce54918..ab247430a 100644 --- a/agent/php_nrini.c +++ b/agent/php_nrini.c @@ -943,6 +943,10 @@ static void foreach_special_control_flag(const char* str, NR_PHP_PROCESS_GLOBALS(special_flags).show_executes_untrimmed = 1; return; } + if (0 == nr_strcmp(str, "show_fibers")) { + NR_PHP_PROCESS_GLOBALS(special_flags).show_fibers = 1; + return; + } if (0 == nr_strcmp(str, "no_exception_handler")) { NR_PHP_PROCESS_GLOBALS(special_flags).no_exception_handler = 1; return; @@ -3268,7 +3272,7 @@ STD_PHP_INI_ENTRY_EX("newrelic.message_tracer.segment_parameters.enabled", * request. */ STD_PHP_INI_ENTRY_EX("newrelic.fibers.disabled", - "1", // default will eventually be false + "0", NR_PHP_REQUEST, nr_boolean_mh, ini.fibers_disabled, diff --git a/agent/php_observer.c b/agent/php_observer.c index a9d64ec83..c04a59109 100644 --- a/agent/php_observer.c +++ b/agent/php_observer.c @@ -16,12 +16,14 @@ #include "php_error.h" #include "php_execute.h" #include "php_extension.h" +#include "php_fibers.h" #include "php_globals.h" #include "php_header.h" #include "php_hooks.h" #include "php_internal_instrument.h" #include "php_observer.h" #include "php_samplers.h" +#include "php_txn.h" #include "php_user_instrument.h" #include "php_user_instrument_wraprec_hashmap.h" #include "php_vm.h" @@ -119,9 +121,8 @@ static zend_observer_fcall_handlers nr_php_fcall_register_handlers( = nr_php_user_instrument_wraprec_hashmap_get(func_name, scope_name); // store the wraprec in the op_array extension for the duration of the // request for later lookup - ZEND_OP_ARRAY_EXTENSION(NR_OP_ARRAY, - NR_PHP_PROCESS_GLOBALS(op_array_extension_handle)) - = wr; + ZEND_OP_ARRAY_EXTENSION( + NR_OP_ARRAY, NR_PHP_PROCESS_GLOBALS(op_array_extension_handle)) = wr; } handlers.begin = nr_php_observer_fcall_begin; @@ -130,28 +131,178 @@ static zend_observer_fcall_handlers nr_php_fcall_register_handlers( } #if ZEND_MODULE_API_NO > ZEND_8_0_X_API_NO /* PHP8.1+ */ + +#define NR_FIBER_USED_CREATE_METRIC \ + if (NULL != NRPRG(txn)) { \ + nrm_force_add(NRPRG(txn)->unscoped_metrics, \ + "Supportability/PHP/Fiber/used", 0); \ + } + static void nr_fiber_disable(zend_fiber_context* fiber_context) { + if (nrunlikely(NR_PHP_PROCESS_GLOBALS(special_flags).show_fibers)) { + nr_fiber_show_fiber(fiber_context, "init/destroy"); + } if (NULL != NRPRG(txn)) { /* Fiber init/destroy detected, end and keep the transaction. */ - nrl_verbosedebug(NRL_INSTRUMENT, - "Transaction is truncated because PHP Fiber use is detected."); - nrm_force_add(NRPRG(txn)->unscoped_metrics, "Supportability/PHP/Fiber/used", - 0); + nrl_verbosedebug( + NRL_INSTRUMENT, + "Transaction is truncated because PHP Fiber use is detected."); + NR_FIBER_USED_CREATE_METRIC nr_php_txn_end(0, 0 TSRMLS_CC); } } static void nr_fiber_switch_disable(zend_fiber_context* from, zend_fiber_context* to) { + if (nrunlikely(NR_PHP_PROCESS_GLOBALS(special_flags).show_fibers)) { + nr_fiber_show_fiber(from, "switch from"); + nr_fiber_show_fiber(to, "switch to"); + } if (NULL != NRPRG(txn)) { /* Fiber switch detected, end and keep the transaction. */ - nrl_verbosedebug(NRL_INSTRUMENT, - "Transaction is truncated because PHP Fiber use is detected."); - nrm_force_add(NRPRG(txn)->unscoped_metrics, "Supportability/PHP/Fiber/used", - 0); + nrl_verbosedebug( + NRL_INSTRUMENT, + "Transaction is truncated because PHP Fiber use is detected."); + NR_FIBER_USED_CREATE_METRIC nr_php_txn_end(0, 0 TSRMLS_CC); } } + +static void nr_fiber_init_observe(zend_fiber_context* zfc) { + char zfc_key[32]; + NR_FIBER_USED_CREATE_METRIC + if (nrunlikely(NR_PHP_PROCESS_GLOBALS(special_flags).show_fibers)) { + nr_fiber_show_fiber(zfc, "init"); + } + if (NULL == NRPRG(fiber_globals_map)) { + // initialize the fiber global hashmap if it does not already exist + if (NR_FAILURE == nr_fiber_init_global_hashmap(&NRPRG(fiber_globals_map))) { + nrl_warning(NRL_AGENT, "Failed to initialize the fiber global hashmap needed for a fiber aware transaction and must therefore end the transaction."); + nr_php_txn_end(0, 0 TSRMLS_CC); + } + } + + snprintf(zfc_key, sizeof(zfc_key), "%p", zfc); + + // Add the current context to the global hashmap for the new fiber + if (NR_FAILURE + == nr_add_fiber_context_to_global_hashmap( + NRPRG(fiber_globals_map), + NRPRG(fiber_globals) ? NRPRG(fiber_globals)->ctx_globals + : &NRPRG(ctx), + zfc_key)) { + nrl_warning(NRL_AGENT, + "Failed to add fiber context to global hashmap for fiber %s needed for a fiber aware transaction and must therefore end the transaction.", + zfc_key); + nr_php_txn_end(0, 0 TSRMLS_CC); + } +} + +static void nr_fiber_destroy_observe(zend_fiber_context* zfc) { + char zfc_key[32]; + NR_FIBER_USED_CREATE_METRIC + if (nrunlikely(NR_PHP_PROCESS_GLOBALS(special_flags).show_fibers)) { + nr_fiber_show_fiber(zfc, "destroy"); + } + + snprintf(zfc_key, sizeof(zfc_key), "%p", zfc); + if (0 == nr_strcmp(NRPRG_SHARED(fiber_context_string), zfc_key)) { + // clear the current fiber global ptr if it is the context to be destroyed + NRPRG(fiber_globals) = NULL; + } + + // Remove the entry in the fiber global hashmap for this fiber + if (NR_FAILURE + == nr_remove_fiber_context_from_global_hashmap(NRPRG(fiber_globals_map), + zfc_key)) { + nrl_warning( + NRL_AGENT, + "Failed to remove fiber context from global hashmap for fiber %s", + zfc_key); + } +} + +static inline void nr_fiber_set_contexts(zend_fiber_context* zfc) { + nr_segment_t* current_segment = NULL; + nrtxn_t* txn = NRPRG(txn); + + if (zfc->kind != zend_ce_fiber) { + /* Context is the Main PHP Process */ + NRPRG_SHARED(current_php_context) = NULL; + NRPRG_SHARED(fiber_context_string)[0] = '\0'; + if (nrlikely(NULL != txn)) { + txn->current_async_context = 0; + } + } else { + snprintf(NRPRG_SHARED(fiber_context_string), + sizeof(NRPRG_SHARED(fiber_context_string)), "%p", zfc); + NRPRG_SHARED(current_php_context) = NRPRG_SHARED(fiber_context_string); + if (nrlikely(NULL != txn)) { + txn->current_async_context = nr_string_add( + txn->trace_strings, NRPRG_SHARED(current_php_context)); + } + } +} + +/* + * The fiber_parent_segment is only set to non-NULL when starting a fiber within + a fiber. + * For all other cases it will be null which indicates that the main PHP process + is the parent. + * In the case of end/start txn happening within fibers, this can also be null + due to the following + * current agent behavior when a txn ends/starts: + * 1) when a segment is discarded, its children get re-parented to its parent + * 2) when a txn is ended, all segments (even those that haven't completed yet) + are closed + * 3) For subsequent children of a calling segment that was closed by the txn + end, since the calling segment no longer exists, the main process becomes the + parent. + * + */ +static inline void nr_fiber_set_fiber_parent_segment(zend_fiber_context* zfc) { + if (zfc->kind != zend_ce_fiber) { + /* Main process is the parent, set to NULL. */ + NRPRG_SHARED(fiber_parent_segment) = NULL; + } else { + char* parent_fiber_context_string = nr_formatf("%p", zfc); + NRPRG_SHARED(fiber_parent_segment) + = nr_txn_get_current_segment(NRPRG(txn), parent_fiber_context_string); + nr_free(parent_fiber_context_string); + } +} + +static void nr_fiber_switch_observe(zend_fiber_context* from, + zend_fiber_context* to) { + NR_FIBER_USED_CREATE_METRIC + if (nrunlikely(NR_PHP_PROCESS_GLOBALS(special_flags).show_fibers)) { + nr_fiber_show_fiber(from, "switch from"); + nr_fiber_show_fiber(to, "switch to"); + } + + /* + * If kind != zend_ce_fiber that means the fiber context is the MAIN php + * context not an actual fiber. + */ + + nr_fiber_set_contexts(to); + + /* If we are starting a new fiber. We need to ensure it is properly parented + * to the "from" context. */ + if (ZEND_FIBER_STATUS_INIT == to->status) { + nr_fiber_set_fiber_parent_segment(from); + } + + if (NR_FAILURE + == nr_fiber_switch_global_context(NRPRG(fiber_globals_map), + &NRPRG(fiber_globals), + NRPRG_SHARED(current_php_context))) { + nrl_warning(NRL_AGENT, "Failed to switch fiber context to %s needed for a fiber aware transaction and must therefore end the transaction.", + NRPRG_SHARED(current_php_context)); + nr_php_txn_end(0, 0 TSRMLS_CC); + } +} + #endif /* PHP 8.1+ */ void nr_php_observer_no_op(zend_execute_data* execute_data NRUNUSED) {}; @@ -175,11 +326,31 @@ void nr_php_observer_minit() { * Register the Observer API fiber handlers. */ - /* Currently only register if we are disabling instrumentation. */ + /* + * Life cycle of a fiber: + * 1) fiber->start which triggers + * a) fiber init + * b) fiber switch from calling context to fiber context + * 2) fiber->resume which triggers + * a) fiber switch from calling context to fiber context + * 3) fiber->suspend which triggers + * a) fiber switch from fiber context to calling context + * 4) fiber exits/completes which triggers + * a) fiber switch from fiber context to calling context + * b) fiber destroy + * 5) calling function exits without calling fiber->resume which triggers + * a) fiber switch from calling context to fiber context + * b) fiber switch from fiber context to calling context + * c) fiber destroy + */ if (NRINI(fibers_disabled)) { zend_observer_fiber_init_register(nr_fiber_disable); zend_observer_fiber_switch_register(nr_fiber_switch_disable); zend_observer_fiber_destroy_register(nr_fiber_disable); + } else { + zend_observer_fiber_init_register(nr_fiber_init_observe); + zend_observer_fiber_switch_register(nr_fiber_switch_observe); + zend_observer_fiber_destroy_register(nr_fiber_destroy_observe); } #endif /* PHP 8.1+ */ diff --git a/agent/php_rinit.c b/agent/php_rinit.c index 11b29a89a..4d3d42a3c 100644 --- a/agent/php_rinit.c +++ b/agent/php_rinit.c @@ -25,6 +25,10 @@ static void nr_php_datastore_instance_destroy( nr_datastore_instance_destroy(&instance); } +static void nr_predis_command_destroy(nrtime_t* time) { + nr_free(time); +} + #if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ && !defined OVERWRITE_ZEND_EXECUTE_DATA /* OAPI global stacks (as opposed to call stack used previously) @@ -113,6 +117,9 @@ PHP_RINIT_FUNCTION(newrelic) { NRPRG_CTX(check_cufa) = false; + NRPRG_SHARED(current_php_context) = NULL; + NRPRG_SHARED(fiber_parent_segment) = NULL; + /* * Pre-OAPI, this variables were kept on the call stack and * therefore had no need to be in an nr_stack @@ -134,6 +141,8 @@ PHP_RINIT_FUNCTION(newrelic) { NRPRG_CTX(pgsql_last_conn) = NULL; NRPRG_CTX(datastore_connections) = nr_hashmap_create( (nr_hashmap_dtor_func_t)nr_php_datastore_instance_destroy); + NRPRG_CTX(predis_commands) + = nr_hashmap_create((nr_hashmap_dtor_func_t)nr_predis_command_destroy); nr_php_txn_begin(0, 0 TSRMLS_CC); diff --git a/agent/php_rshutdown.c b/agent/php_rshutdown.c index ee971ba19..cfc31a7e9 100644 --- a/agent/php_rshutdown.c +++ b/agent/php_rshutdown.c @@ -9,6 +9,7 @@ #include "php_agent.h" #include "php_curl_md.h" #include "php_error.h" +#include "php_fibers.h" #include "php_globals.h" #include "php_user_instrument.h" #include "php_wrapper.h" @@ -46,6 +47,13 @@ PHP_RSHUTDOWN_FUNCTION(newrelic) { nrl_verbosedebug(NRL_INIT, "RSHUTDOWN processing started"); +#if ZEND_MODULE_API_NO >= ZEND_8_1_X_API_NO + // Set the fiber_globals pointer to NULL to prevent trying to access the + // fiber-specific global values when freeing below. Fiber globals are free'd + // separately in a dedicated function called in nr_php_post_deactivate. + NRPRG(fiber_globals) = NULL; +#endif + /* nr_php_txn_shutdown will check for a NULL transaction. */ nr_php_txn_shutdown(TSRMLS_C); @@ -84,6 +92,13 @@ int nr_php_post_deactivate(void) { nrl_verbosedebug(NRL_INIT, "post-deactivate processing started"); +#if ZEND_MODULE_API_NO >= ZEND_8_1_X_API_NO + // Set the fiber_globals pointer to NULL to prevent trying to access the + // fiber-specific global values when freeing below. Fiber globals are free'd + // separately in a dedicated function. + NRPRG(fiber_globals) = NULL; +#endif + /* * PHP 7 has a singleton trampoline op array that is used for the life of an * executor (which, in non-ZTS mode, is the life of the process). We need to @@ -149,6 +164,10 @@ int nr_php_post_deactivate(void) { NRPRG_CTX(drupal_http_request_segment) = NULL; #endif +#if ZEND_MODULE_API_NO >= ZEND_8_1_X_API_NO + nr_fiber_destroy_global_hashmap(&NRPRG(fiber_globals_map)); +#endif + nrl_verbosedebug(NRL_INIT, "post-deactivate processing done"); return SUCCESS; } diff --git a/agent/php_txn.c b/agent/php_txn.c index d5a32026e..7820b3980 100644 --- a/agent/php_txn.c +++ b/agent/php_txn.c @@ -15,6 +15,7 @@ #include "php_samplers.h" #include "php_user_instrument.h" #include "php_stacked_segment.h" +#include "php_txn.h" #include "php_txn_private.h" #include "nr_agent.h" #include "nr_commands.h" @@ -1269,11 +1270,7 @@ nr_status_t nr_php_txn_end(int ignoretxn, int in_post_deactivate TSRMLS_DC) { */ #if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ && !defined OVERWRITE_ZEND_EXECUTE_DATA - nr_segment_t* segment = nr_txn_get_current_segment(NRPRG(txn), NULL); - while (NULL != segment && segment != NRTXN(segment_root)) { - nr_segment_end(&segment); - segment = nr_txn_get_current_segment(NRPRG(txn), NULL); - } + nr_txn_finalize_parent_stacks(NRPRG(txn)); #else nr_php_stacked_segment_unwind(TSRMLS_C); #endif diff --git a/agent/php_txn.h b/agent/php_txn.h new file mode 100644 index 000000000..e27abf716 --- /dev/null +++ b/agent/php_txn.h @@ -0,0 +1,155 @@ +/* + * Copyright 2020 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef PHP_TXN_HDR +#define PHP_TXN_HDR + +#include "php_newrelic.h" +#include "nr_txn.h" + +/* + * Returns whether a particular security policy feature is considered + * secure or not according to the current client configuration. These + * values are not the ultimate source of truth for whether a certain + * security policy is enabled or not. The agent sends these values to the + * daemon for further calculation/consideration. + */ +bool nr_php_txn_is_policy_secure(const char* policy_name, + const nrtxnopt_t* opts); + +/* + * Returns an object of supported policies + * + * We need to send the daemon a hash of the LASP policies we support. + * This function returns those policies as an nro object with the + * follow structure + * + * {"policy_name":{ //the policy name + * "supported": bool //does the agent support this policy + * "enabled: bool //does the policy seem enabled or + * //disabled from the pov of the configuration + * + * @return An nrobj_t*, caller is responsible for freeing with nro_delete + */ +nrobj_t* nr_php_txn_get_supported_security_policy_settings(); + +/* + * Purpose : Override the transaction name if PHP-FPM generated an error + * response internally. + * + * Params : 1. The current transaction. + */ +extern void nr_php_txn_handle_fpm_error(nrtxn_t* txn TSRMLS_DC); + +/* + * Purpose : Create and record metrics for the PHP and agent versions. + * + * Params : 1. The current transaction. + * + * Notes : This function relies on NR_VERSION and the value of + * NRPRG(php_version) to create the metrics. + */ +extern void nr_php_txn_create_agent_php_version_metrics(nrtxn_t* txn); + +/* + * Purpose : Create and record metric for a specific agent version. + * + * Params : 1. The current transaction. + * + * Notes : This function relies on the value of the macro NR_VERSION + * to create. + */ +extern void nr_php_txn_create_agent_version_metric(nrtxn_t* txn); + +/* + * Purpose : Create and record metric for a specific PHP version. + * + * Params : 1. The current transaction. + * 2. The PHP agent version. + */ +extern void nr_php_txn_create_php_version_metric(nrtxn_t* txn, + const char* version); + +/* + * Purpose : Callback for nr_php_packages_iterate to create major + * version metrics. + * + * Params : 1. PHP suggestion package version + * 2. PHP suggestion package name + * 3. PHP suggestion package name length + * 4. The current transaction (via userdata) + */ +extern void nr_php_txn_php_package_create_major_metric(void* value, + const char* key, + size_t key_len, + void* user_data); + +/* + * Purpose : Create and record metric for a package major versions. + * + * Params : 1. The current transaction. + */ +extern void nr_php_txn_create_packages_major_metrics(nrtxn_t* txn); + +/* + * Purpose : Filter the labels hash to exclude any labels that are in the + * newrelic.application_logging.forwarding.labels.exclude list. + * + * Params : 1. The labels hash to filter. + * + * Returns : A new hash containing the filtered labels. + * If no labels exist or all labels are excluded, then return NULL. + * + */ + +extern nrobj_t* nr_php_txn_get_log_forwarding_labels(nrobj_t* labels); + +/* + * Purpose : Get a pointer to the currently-executing segment for a given + * async context. + * + * Params : 1. The transaction. + * + * Returns : A pointer to the active segment for the current PHP context that a + * function is executing in (either main context or a fiber context). + * Note : The NRPRG_SHARED(current_php_context) and + * txn->current_async_context values are in sync most of the time, but it's + * important we keep track of both and use where appropriate.  Most of the time + * they will be the same.  It's very rare that they aren't, but we want to + * ensure we cover that case.   NRPRG_SHARED(current_php_context) is independent + * of txn and always tells what context (fiber/main) PHP is executing in.  + * txn->current_async_context depends on the currently executing segment of the + * txn and allows alignment with whatever segment is currently executing on the + * txn.  Examples: We could be in a fiber with context + * NRPRG_SHARED(current_php_context), but if we are creating a few additional + * async segments for some async instrumentation we are doing then that would be + * current txn context. If a txn start/stop occurs, that will also change the + * context state for the txn even though the current_php_context remains + * unchanged. + * + * With PHP Fibers, the current_php_context switches frequently. We need to be + * able to retrieve the segment associated with a particular fiber and not just + * the last one the txn was executing. Any function that is based on an + * execution trigger (ex: OAPI func_begin/end or the overwriting of + * zend_execute_ex) implementation that does not use stacked segments (OAPI + * implementation) must use NRPRG_SHARED(current_php_context) to retrieve the + * segment and ensure both segment alignment with current context and correct + * context parenting. Begin and end func are dependent on fiber context since it + * deals with initial creation and proper retrieval of a segment. + * NRPRG_SHARED(current_php_context) will always tell the current fiber context + * (whether fiber or main) that we need to create or get a segment. Note : + * Callers wishing to get the segment currently executing in + * the txn should use nr_txn_get_current_segment_txn_context. + * Callers wishing to + * get the current segment on a specific context should use + * nr_txn_get_current_segment() to get the current segment on any other context + * they are interested in. + */ +static inline nr_segment_t* nr_php_txn_get_current_segment_php_context( + nrtxn_t* txn) { + return nr_txn_get_current_segment(txn, NRPRG_SHARED(current_php_context)); +} + +#endif /* PHP_TXN_HDR */ diff --git a/agent/scripts/newrelic.ini.private.template b/agent/scripts/newrelic.ini.private.template index e32d44844..e0eb4b04f 100644 --- a/agent/scripts/newrelic.ini.private.template +++ b/agent/scripts/newrelic.ini.private.template @@ -44,6 +44,7 @@ ; show_execute_stack ; show_execute_returns ; show_executes_untrimmed +; show_fibers ; no_exception_handler ; no_signal_handler ; debug_autorum @@ -209,8 +210,8 @@ ; Setting: newrelic.fibers.disabled ; Type : bool ; Scope : per-directory -; Default: true +; Default: false ; Info : If set to true, the agent, on detecting fiber activity, will end the txn, ; send the recorded telementry, and disable instrumentation for the remainder of the request. ; -;newrelic.fibers.disabled = true +;newrelic.fibers.disabled = false diff --git a/agent/tests/test_fibers.c b/agent/tests/test_fibers.c new file mode 100644 index 000000000..c782b17a6 --- /dev/null +++ b/agent/tests/test_fibers.c @@ -0,0 +1,756 @@ +/* + * Copyright 2026 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "tlib_php.h" + +#include + +#include "nr_datastore_instance.h" +#include "php_agent.h" +#include "php_fibers.h" +#include "php_newrelic.h" +#include "nr_mysqli_metadata.h" +#include "util_hashmap.h" +#include "util_memory.h" +#include "util_stack.h" +#include "util_strings.h" +#include "util_time.h" + +tlib_parallel_info_t parallel_info + = {.suggested_nthreads = -1, .state_size = 0}; + +#if ZEND_MODULE_API_NO >= ZEND_8_1_X_API_NO + +static void dtor_datastore(void* val) { + nr_datastore_instance_t* d = (nr_datastore_instance_t*)val; + nr_datastore_instance_destroy(&d); +} + +static nr_datastore_instance_t* make_datastore_instance(const char* host, + const char* port, + const char* db) { + nr_datastore_instance_t* d = nr_malloc(sizeof(nr_datastore_instance_t)); + d->host = nr_strdup(host); + d->port_path_or_id = nr_strdup(port); + d->database_name = nr_strdup(db); + return d; +} + +static nr_hashmap_t* dummy_hashmap_datastore() { + nr_hashmap_t* h = nr_hashmap_create(dtor_datastore); + nr_hashmap_set(h, NR_PSTR("a"), + make_datastore_instance("hostA", "portA", "dbA")); + nr_hashmap_set(h, NR_PSTR("b"), + make_datastore_instance("hostB", "portB", "dbB")); + nr_hashmap_set(h, NR_PSTR("c"), + make_datastore_instance("hostC", "portC", "dbC")); + return h; +} + +static void dtor_time(void* val) { + nr_free(val); +} + +static nr_hashmap_t* dummy_hashmap_time() { + nr_hashmap_t* h = nr_hashmap_create(dtor_time); + nrtime_t* ta = nr_malloc(sizeof(nrtime_t)); + nrtime_t* tb = nr_malloc(sizeof(nrtime_t)); + nrtime_t* tc = nr_malloc(sizeof(nrtime_t)); + *ta = 1000; + *tb = 2000; + *tc = 3000; + nr_hashmap_set(h, NR_PSTR("a"), ta); + nr_hashmap_set(h, NR_PSTR("b"), tb); + nr_hashmap_set(h, NR_PSTR("c"), tc); + return h; +} + +static void dtor_free_stack_str(void* e, NRUNUSED void* d) { + char* str = (char*)e; + nr_free(str); +} + +static nr_stack_t dummy_stack_data_str() { + nr_stack_t s; + nr_stack_init(&s, NR_STACK_DEFAULT_CAPACITY); + s.dtor = dtor_free_stack_str; + nr_stack_push(&s, nr_strdup("elemA")); + nr_stack_push(&s, nr_strdup("elemB")); + nr_stack_push(&s, nr_strdup("elemC")); + return s; +} + +static nr_stack_t dummy_stack_data_bool() { + nr_stack_t s; + nr_stack_init(&s, NR_STACK_DEFAULT_CAPACITY); + nr_stack_push(&s, (void*)(uintptr_t)1); + nr_stack_push(&s, NULL); + nr_stack_push(&s, (void*)(uintptr_t)1); + return s; +} + +static void dtor_free_stack_zval(void* e, NRUNUSED void* d) { + zval* zv = (zval*)e; + nr_php_zval_free(&zv); +} + +static nr_stack_t dummy_stack_data_zval() { + nr_stack_t s; + zval* z = NULL; + zval* zz = NULL; + zval* zzz = NULL; + nr_stack_init(&s, NR_STACK_DEFAULT_CAPACITY); + s.dtor = dtor_free_stack_zval; + z = nr_php_zval_alloc(); + nr_php_zval_str(z, "stackA"); + nr_stack_push(&s, z); + zz = nr_php_zval_alloc(); + nr_php_zval_str(zz, "stackB"); + nr_stack_push(&s, zz); + zzz = nr_php_zval_alloc(); + nr_php_zval_str(zzz, "stackC"); + nr_stack_push(&s, zzz); + return s; +} + +static ctx_globals_t* dummy_ctx_globals() { + ctx_globals_t* cg = NULL; + cg = nr_malloc(sizeof(ctx_globals_t)); + cg->doctrine_dql = nr_strdup("doctrine_dql"); + cg->drupal_http_request_depth = 3; + cg->php_cur_stack_depth = 4; + cg->mysql_last_conn = nr_strdup("mysql_last_conn"); + cg->pgsql_last_conn = nr_strdup("pgsql_last_conn"); + cg->datastore_connections = dummy_hashmap_datastore(); + cg->deprecated_capture_request_parameters = 1; + cg->check_cufa = true; + cg->predis_commands = dummy_hashmap_time(); + cg->context_root = NULL; // Main ctx root should always be NULL + cg->drupal_invoke_all_hooks = dummy_stack_data_zval(); + cg->drupal_invoke_all_states = dummy_stack_data_bool(); + cg->wordpress_tags = dummy_stack_data_str(); + cg->wordpress_tag_states = dummy_stack_data_bool(); + cg->predis_ctxs = dummy_stack_data_str(); + return cg; +} + +static void free_ctx_globals(ctx_globals_t* cg) { + nr_free(cg->doctrine_dql); + nr_free(cg->mysql_last_conn); + nr_free(cg->pgsql_last_conn); + nr_hashmap_destroy(&cg->datastore_connections); + nr_hashmap_destroy(&cg->predis_commands); + nr_stack_destroy_fields(&cg->drupal_invoke_all_hooks); + nr_stack_destroy_fields(&cg->drupal_invoke_all_states); + nr_stack_destroy_fields(&cg->wordpress_tags); + nr_stack_destroy_fields(&cg->wordpress_tag_states); + nr_stack_destroy_fields(&cg->predis_ctxs); + nr_free(cg); +} + +/* + * Cleanup for a ctx_globals returned by nr_fiber_copy_ctx_globals: skips the + * fields that are shallow-copied (datastore_connections, predis_commands) + * since those are still owned by the source. Mirrors the ctx_globals section + * of free_fiber_globals. + */ +static void free_ctx_globals_copy(ctx_globals_t* cg) { + nr_free(cg->doctrine_dql); + nr_free(cg->mysql_last_conn); + nr_free(cg->pgsql_last_conn); + nr_stack_destroy_fields(&cg->drupal_invoke_all_hooks); + nr_stack_destroy_fields(&cg->drupal_invoke_all_states); + nr_stack_destroy_fields(&cg->wordpress_tags); + nr_stack_destroy_fields(&cg->wordpress_tag_states); + nr_stack_destroy_fields(&cg->predis_ctxs); + nr_free(cg); +} + +static void test_init_destroy_hashmap(void) { + nr_hashmap_t* test_fiber_global_map = NULL; + tlib_php_request_start(); + + /* + * Test : init creates the hashmap when one does not yet exist. + */ + nr_fiber_destroy_global_hashmap(&test_fiber_global_map); + tlib_pass_if_null("hashmap is NULL after destroy", test_fiber_global_map); + + nr_fiber_init_global_hashmap(&test_fiber_global_map); + tlib_pass_if_not_null("init creates the hashmap", test_fiber_global_map); + + /* + * Test : init is idempotent and does not replace an existing hashmap. + */ + { + nr_hashmap_t* original = test_fiber_global_map; + nr_fiber_init_global_hashmap(&test_fiber_global_map); + tlib_pass_if_ptr_equal("init does not replace existing hashmap", original, + test_fiber_global_map); + } + + /* + * Test : destroy nulls out the hashmap pointer. + */ + nr_fiber_destroy_global_hashmap(&test_fiber_global_map); + tlib_pass_if_null("hashmap is NULL after destroy", test_fiber_global_map); + + /* + * Test : destroy is safe to call when hashmap is already NULL. + */ + nr_fiber_destroy_global_hashmap(&test_fiber_global_map); + tlib_pass_if_null("destroy is null safe", test_fiber_global_map); + + tlib_php_request_end(); +} + +static void test_copy_ctx_globals(void) { + ctx_globals_t* src; + ctx_globals_t* copy; + + tlib_php_request_start(); + + src = dummy_ctx_globals(); + + copy = nr_fiber_copy_ctx_globals(src); + + tlib_pass_if_not_null("copy returns a non-NULL pointer", copy); + + /* + * drupal_http_request_depth is reset to 0 in the copy; the fiber starts + * outside any drupal_http_request invocation regardless of main's depth. + * + * php_cur_stack_depth is copied from src rather than reset. The agent + * tracks the PHP call depth across fiber switches so that exit handlers + * pop the same number of frames they pushed; resetting to 0 in the fiber + * snapshot caused depth underflow when control returned to main. + */ + tlib_pass_if_size_t_equal("drupal_http_request_depth is reset to 0", 0, + copy->drupal_http_request_depth); + tlib_pass_if_int_equal("php_cur_stack_depth is copied from src", 4, + copy->php_cur_stack_depth); + + tlib_pass_if_int_equal("deprecated_capture_request_parameters is copied", 1, + copy->deprecated_capture_request_parameters); + tlib_pass_if_int_equal("check_cufa is copied", true, copy->check_cufa); + + /* + * datastore_connections and predis_commands are shallow-copied: the copy + * must share the same hashmap pointer as the source. + */ + tlib_pass_if_ptr_equal("datastore_connections is shared with src", + src->datastore_connections, + copy->datastore_connections); + tlib_pass_if_ptr_equal("predis_commands is shared with src", + src->predis_commands, copy->predis_commands); + + /* + * drupal_http_request_segment is intentionally reset to NULL in the copy. + */ + tlib_pass_if_null("drupal_http_request_segment is reset to NULL", + copy->drupal_http_request_segment); + + /* + * wordpress_tags is COPY_STACK'd with copy_elem_str (which clones each + * string), then the copy's stack dtor is explicitly set so the copy owns + * the cloned strings. Without that dtor override, the cloned strings would + * leak when the fiber snapshot is destroyed. We can't compare against the + * static fiber_str_dtor symbol, but a non-NULL dtor proves the override + * line ran. + */ + tlib_pass_if_true( + "wordpress_tags copy has a dtor set so cloned " + "strings are freed by destroy", + NULL != copy->wordpress_tags.dtor, "expected non-NULL dtor"); + + free_ctx_globals_copy(copy); + free_ctx_globals(src); + + tlib_php_request_end(); +} + +/* + * Build a minimal ctx_globals_t with NULL string fields so we can verify + * COPY_STRING preserves NULL rather than substituting the empty string "" + * via nr_strdup. Callers like php_mysql.c and php_pgsql.c use these pointers + * as a "valid last-connection set" sentinel; a copy that turned NULL into "" + * would flip those checks truthy in the fiber snapshot. + * + * Hashmap and stack fields still need to be non-NULL because the COPY_STACK + * helpers don't tolerate NULL sources. + */ +static ctx_globals_t* dummy_ctx_globals_null_strings() { + ctx_globals_t* cg = NULL; + cg = nr_malloc(sizeof(ctx_globals_t)); + cg->doctrine_dql = NULL; + cg->drupal_http_request_depth = 0; + cg->php_cur_stack_depth = 0; + cg->mysql_last_conn = NULL; + cg->pgsql_last_conn = NULL; + cg->datastore_connections = dummy_hashmap_datastore(); + cg->deprecated_capture_request_parameters = 0; + cg->check_cufa = false; + cg->predis_commands = dummy_hashmap_time(); + cg->drupal_invoke_all_hooks = dummy_stack_data_zval(); + cg->drupal_invoke_all_states = dummy_stack_data_bool(); + cg->wordpress_tags = dummy_stack_data_str(); + cg->wordpress_tag_states = dummy_stack_data_bool(); + cg->predis_ctxs = dummy_stack_data_str(); + return cg; +} + +static void test_copy_ctx_globals_null_strings(void) { + ctx_globals_t* src; + ctx_globals_t* copy; + + tlib_php_request_start(); + + src = dummy_ctx_globals_null_strings(); + copy = nr_fiber_copy_ctx_globals(src); + + tlib_pass_if_not_null("copy returns a non-NULL pointer", copy); + tlib_pass_if_null("NULL doctrine_dql copies as NULL, not \"\"", + copy->doctrine_dql); + tlib_pass_if_null("NULL mysql_last_conn copies as NULL, not \"\"", + copy->mysql_last_conn); + tlib_pass_if_null("NULL pgsql_last_conn copies as NULL, not \"\"", + copy->pgsql_last_conn); + + free_ctx_globals_copy(copy); + free_ctx_globals(src); + + tlib_php_request_end(); +} + +/* + * Verify context_root is reset to NULL in the fiber snapshot regardless of + * what src holds. context_root points to the root segment of the function + * that started a fiber; the segment is owned by the segment subsystem and + * lives elsewhere, so the copy must not inherit src's pointer. Otherwise + * two contexts would alias the same root and the segment subsystem's + * cleanup would race with our own. + * + * dummy_ctx_globals() seeds context_root as NULL (main never holds a root), + * so we override it with a non-NULL sentinel here. A future regression that + * swapped the explicit reset for COPY_BASIC(context_root) would trip this. + */ +static void test_copy_ctx_globals_context_root_reset(void) { + ctx_globals_t* src; + ctx_globals_t* copy; + nr_segment_t sentinel; + + tlib_php_request_start(); + + src = dummy_ctx_globals(); + src->context_root = &sentinel; + + copy = nr_fiber_copy_ctx_globals(src); + + tlib_pass_if_not_null("copy returns a non-NULL pointer", copy); + tlib_pass_if_null( + "context_root is reset to NULL even when src->context_root is non-NULL", + copy->context_root); + + free_ctx_globals_copy(copy); + src->context_root = NULL; + free_ctx_globals(src); + + tlib_php_request_end(); +} + +/* + * free_fiber_globals must not free ctx_globals->context_root. The pointer + * tracks a segment owned by the segment subsystem; freeing it here would + * double-free when that subsystem tears the segment down. + * + * We seed context_root with the address of a stack-local variable: passing + * that to free() would abort (it was never malloc'd). If free_fiber_globals + * stays in its lane, this test completes cleanly. + */ +static void test_free_fiber_globals_does_not_free_context_root(void) { + fiber_globals_t* f = NULL; + ctx_globals_t* src = NULL; + nr_segment_t sentinel; + + tlib_php_request_start(); + + src = dummy_ctx_globals(); + + f = nr_malloc(sizeof(fiber_globals_t)); + f->ctx_globals = nr_fiber_copy_ctx_globals(src); + + /* + * Simulate the post-copy assignment of context_root by the code that + * starts a fiber. The copy itself resets context_root to NULL; here we + * stand in for whoever sets it afterward. + */ + f->ctx_globals->context_root = &sentinel; + + free_fiber_globals(f); + + free_ctx_globals(src); + + tlib_php_request_end(); +} + +static void test_copy_ctx_globals_null_src(void) { + tlib_php_request_start(); + + /* + * nr_fiber_copy_ctx_globals returns NULL on a NULL source so callers can + * detect the failure. add_fiber_context relies on this to refuse to insert + * a partial entry into the fiber globals hashmap. + */ + tlib_pass_if_null("copy returns NULL when src is NULL", + nr_fiber_copy_ctx_globals(NULL)); + + tlib_php_request_end(); +} + +/* + * Exercises the free_fiber_globals NULL-tolerance and the partial-wrapper + * cleanup path. Without the fix, a wrapper with NULL ctx_globals would be + * leaked because the destructor early-returned without freeing f. + */ +static void test_free_fiber_globals_null_paths(void) { + fiber_globals_t* f = NULL; + + tlib_php_request_start(); + + /* NULL fiber_globals must not crash. */ + free_fiber_globals(NULL); + + /* fiber_globals_t with NULL ctx_globals: f itself must still be freed. + If this path leaks, valgrind/asan will surface it. */ + f = nr_malloc(sizeof(fiber_globals_t)); + f->ctx_globals = NULL; + free_fiber_globals(f); + + tlib_php_request_end(); +} + +static void test_add_fiber_context_null_src(void) { + nr_hashmap_t* test_fiber_global_map = NULL; + + tlib_php_request_start(); + + nr_fiber_init_global_hashmap(&test_fiber_global_map); + + /* + * NULL src_ctx_globals must produce NR_FAILURE and must NOT insert a + * partial wrapper into the hashmap. Without the guard, the destructor + * would later warn-and-leak when the orphan was overwritten or the map + * destroyed. + */ + tlib_pass_if_status_failure( + "add fails when src_ctx_globals is NULL", + nr_add_fiber_context_to_global_hashmap(test_fiber_global_map, NULL, + "fiber-null-src")); + tlib_pass_if_size_t_equal("hashmap remains empty after failed add", 0, + nr_hashmap_count(test_fiber_global_map)); + + nr_fiber_destroy_global_hashmap(&test_fiber_global_map); + + tlib_php_request_end(); +} + +static void test_add_fiber_context_bad_input(void) { + nr_hashmap_t* test_fiber_global_map = NULL; + tlib_php_request_start(); + + /* + * Ensure the hashmap is not initialized. + */ + nr_fiber_destroy_global_hashmap(&test_fiber_global_map); + + /* + * Test : NULL key returns failure. + */ + tlib_pass_if_status_failure("NULL key fails when hashmap is uninitialized", + nr_add_fiber_context_to_global_hashmap( + test_fiber_global_map, NULL, NULL)); + + /* + * Test : Uninitialized hashmap returns failure even with a valid key. + */ + tlib_pass_if_status_failure("uninitialized hashmap fails", + nr_add_fiber_context_to_global_hashmap( + test_fiber_global_map, NULL, "fiber-key")); + + /* + * Test : NULL key still fails after init. + */ + nr_fiber_init_global_hashmap(&test_fiber_global_map); + tlib_pass_if_status_failure("NULL key fails after init", + nr_add_fiber_context_to_global_hashmap( + test_fiber_global_map, NULL, NULL)); + + nr_fiber_destroy_global_hashmap(&test_fiber_global_map); + + tlib_php_request_end(); +} + +static void test_add_fiber_context_happy_path(void) { + nr_hashmap_t* test_fiber_global_map = NULL; + ctx_globals_t* cg = NULL; + tlib_php_request_start(); + + cg = dummy_ctx_globals(); + + nr_fiber_init_global_hashmap(&test_fiber_global_map); + + tlib_pass_if_status_success("adding a fiber context succeeds", + nr_add_fiber_context_to_global_hashmap( + test_fiber_global_map, cg, "fiber-1")); + + tlib_pass_if_size_t_equal("hashmap has one entry after add", 1, + nr_hashmap_count(test_fiber_global_map)); + + /* + * Test : adding a second context increments the count. + */ + tlib_pass_if_status_success("adding a second fiber context succeeds", + nr_add_fiber_context_to_global_hashmap( + test_fiber_global_map, cg, "fiber-2")); + + tlib_pass_if_size_t_equal("hashmap has two entries", 2, + nr_hashmap_count(test_fiber_global_map)); + + /* free_fiber_globals destructor cleans up the deep-copied snapshots. */ + nr_fiber_destroy_global_hashmap(&test_fiber_global_map); + free_ctx_globals(cg); + + tlib_php_request_end(); +} + +static void test_remove_fiber_context_bad_input(void) { + nr_hashmap_t* test_fiber_global_map = NULL; + tlib_php_request_start(); + + /* + * Test : NULL key fails when hashmap is uninitialized. + */ + tlib_pass_if_status_failure( + "NULL key fails when hashmap is uninitialized", + nr_remove_fiber_context_from_global_hashmap(test_fiber_global_map, NULL)); + + /* + * Test : Empty key fails (nr_strlen("") < 1). + */ + tlib_pass_if_status_failure( + "empty key fails", + nr_remove_fiber_context_from_global_hashmap(test_fiber_global_map, "")); + + /* + * Test : valid key with uninitialized hashmap fails. + */ + tlib_pass_if_status_failure("uninitialized hashmap fails", + nr_remove_fiber_context_from_global_hashmap( + test_fiber_global_map, "fiber-1")); + + /* + * Test : removing a nonexistent key from an initialized hashmap fails. + */ + nr_fiber_init_global_hashmap(&test_fiber_global_map); + tlib_pass_if_status_failure("missing key fails", + nr_remove_fiber_context_from_global_hashmap( + test_fiber_global_map, "does-not-exist")); + + nr_fiber_destroy_global_hashmap(&test_fiber_global_map); + + tlib_php_request_end(); +} + +static void test_remove_fiber_context_happy_path(void) { + static const char* key = "fiber-remove"; + nr_hashmap_t* test_fiber_global_map = NULL; + ctx_globals_t* cg = NULL; + + tlib_php_request_start(); + + cg = dummy_ctx_globals(); + + nr_fiber_init_global_hashmap(&test_fiber_global_map); + tlib_pass_if_status_success( + "add succeeds before remove", + nr_add_fiber_context_to_global_hashmap(test_fiber_global_map, cg, key)); + + tlib_pass_if_size_t_equal("hashmap has one entry before remove", 1, + nr_hashmap_count(test_fiber_global_map)); + + tlib_pass_if_status_success( + "remove succeeds for existing key", + nr_remove_fiber_context_from_global_hashmap(test_fiber_global_map, key)); + + tlib_pass_if_size_t_equal("hashmap is empty after remove", 0, + nr_hashmap_count(test_fiber_global_map)); + + /* + * Test : removing the same key again fails. + */ + tlib_pass_if_status_failure( + "remove fails when key has already been removed", + nr_remove_fiber_context_from_global_hashmap(test_fiber_global_map, key)); + + nr_fiber_destroy_global_hashmap(&test_fiber_global_map); + free_ctx_globals(cg); + + tlib_php_request_end(); +} + +static void test_switch_global_context_bad_input(void) { + nr_hashmap_t* test_fiber_global_map = NULL; + fiber_globals_t* fiber_globals = NULL; + /* + * A non-NULL sentinel value used to detect that the failure paths actively + * clear *fiber_global_ptr instead of leaving a stale pointer. The address + * itself is never dereferenced. + */ + fiber_globals_t stale_global_ptr; + + tlib_php_request_start(); + + /* + * Test : NULL fiber_global_ptr fails without crashing. + */ + tlib_pass_if_status_failure( + "NULL fiber_global_ptr fails", + nr_fiber_switch_global_context(test_fiber_global_map, NULL, "fiber-1")); + + /* + * Test : NULL key resolves to "main PHP context" and clears + * *fiber_global_ptr to NULL with NR_SUCCESS. + */ + fiber_globals = &stale_global_ptr; + tlib_pass_if_status_success("NULL key succeeds (main context)", + nr_fiber_switch_global_context( + test_fiber_global_map, &fiber_globals, NULL)); + tlib_pass_if_null("NULL key clears fiber_globals to NULL", fiber_globals); + + /* + * Test : valid key with uninitialized hashmap fails AND clears any stale + * fiber_globals pointer the caller may have held. Without the fix, a + * caller's pointer would keep aliasing whatever fiber's snapshot was last + * active, producing silent cross-fiber state corruption. + */ + fiber_globals = &stale_global_ptr; + tlib_pass_if_status_failure( + "uninitialized hashmap fails", + nr_fiber_switch_global_context(test_fiber_global_map, &fiber_globals, + "fiber-1")); + tlib_pass_if_null( + "uninitialized hashmap failure path clears fiber_globals to NULL", + fiber_globals); + + /* + * Test : switching to a key that has no snapshot fails and clears any + * stale fiber_globals pointer. + */ + nr_fiber_init_global_hashmap(&test_fiber_global_map); + fiber_globals = &stale_global_ptr; + tlib_pass_if_status_failure( + "missing key fails", + nr_fiber_switch_global_context(test_fiber_global_map, &fiber_globals, + "does-not-exist")); + tlib_pass_if_null("missing key failure path clears fiber_globals to NULL", + fiber_globals); + + nr_fiber_destroy_global_hashmap(&test_fiber_global_map); + + tlib_php_request_end(); +} + +static void test_switch_global_context_happy_path(void) { + static const char* key = "fiber-switch"; + nr_hashmap_t* test_fiber_global_map = NULL; + fiber_globals_t* fiber_globals = NULL; + ctx_globals_t* cg = NULL; + + tlib_php_request_start(); + + cg = dummy_ctx_globals(); + + nr_fiber_init_global_hashmap(&test_fiber_global_map); + fiber_globals = NULL; + + tlib_pass_if_status_success( + "add succeeds before switch", + nr_add_fiber_context_to_global_hashmap(test_fiber_global_map, cg, key)); + + tlib_pass_if_status_success("switch succeeds for an existing key", + nr_fiber_switch_global_context( + test_fiber_global_map, &fiber_globals, key)); + + tlib_pass_if_not_null("switch sets fiber_globals to a non-NULL snapshot", + fiber_globals); + tlib_pass_if_not_null("the active snapshot has ctx_globals", + fiber_globals->ctx_globals); + + /* + * destroy frees the snapshot via free_fiber_globals; clear the dangling + * pointer first so other code in request shutdown does not dereference it. + */ + nr_fiber_destroy_global_hashmap(&test_fiber_global_map); + fiber_globals = NULL; + free_ctx_globals(cg); + + tlib_php_request_end(); +} + +static void test_destroy_frees_entries(void) { + nr_hashmap_t* test_fiber_global_map = NULL; + ctx_globals_t* cg = NULL; + + tlib_php_request_start(); + + cg = dummy_ctx_globals(); + + nr_fiber_init_global_hashmap(&test_fiber_global_map); + + tlib_pass_if_status_success("add a context", + nr_add_fiber_context_to_global_hashmap( + test_fiber_global_map, cg, "entry-a")); + tlib_pass_if_status_success("add another context", + nr_add_fiber_context_to_global_hashmap( + test_fiber_global_map, cg, "entry-b")); + + /* + * Destroy invokes free_fiber_globals on every entry. We can't directly + * assert the destructor fired, but we can confirm the hashmap pointer is + * cleared and the request can complete cleanly with no leaks. + */ + nr_fiber_destroy_global_hashmap(&test_fiber_global_map); + tlib_pass_if_null("hashmap is cleared by destroy", test_fiber_global_map); + + free_ctx_globals(cg); + + tlib_php_request_end(); +} + +#endif /* PHP 8.1+ */ + +void test_main(void* p NRUNUSED) { + tlib_php_engine_create("" PTSRMLS_CC); + +#if ZEND_MODULE_API_NO >= ZEND_8_1_X_API_NO + test_init_destroy_hashmap(); + test_copy_ctx_globals(); + test_copy_ctx_globals_null_strings(); + test_copy_ctx_globals_context_root_reset(); + test_copy_ctx_globals_null_src(); + test_free_fiber_globals_null_paths(); + test_free_fiber_globals_does_not_free_context_root(); + test_add_fiber_context_bad_input(); + test_add_fiber_context_null_src(); + test_add_fiber_context_happy_path(); + test_remove_fiber_context_bad_input(); + test_remove_fiber_context_happy_path(); + test_switch_global_context_bad_input(); + test_switch_global_context_happy_path(); + test_destroy_frees_entries(); +#endif + + tlib_php_engine_destroy(TSRMLS_C); +} diff --git a/axiom/nr_mysqli_metadata.c b/axiom/nr_mysqli_metadata.c index 04fea64c1..c11e23152 100644 --- a/axiom/nr_mysqli_metadata.c +++ b/axiom/nr_mysqli_metadata.c @@ -209,3 +209,14 @@ void nr_mysqli_metadata_save(nr_mysqli_metadata_t* metadata, nr_mysqli_metadata_id(handle, id); nro_set_hash(metadata->links, id, link); } + +nr_mysqli_metadata_t* nr_mysqli_metadata_copy(nr_mysqli_metadata_t* src) { + nr_mysqli_metadata_t* metadata = NULL; + if (NULL == src) { + return NULL; + } + + metadata = (nr_mysqli_metadata_t*)nr_malloc(sizeof(nr_mysqli_metadata_t)); + metadata->links = nro_copy(src->links); + return metadata; +} diff --git a/axiom/nr_mysqli_metadata.h b/axiom/nr_mysqli_metadata.h index 57e5851fd..ee1b8dd65 100644 --- a/axiom/nr_mysqli_metadata.h +++ b/axiom/nr_mysqli_metadata.h @@ -132,4 +132,15 @@ extern nr_status_t nr_mysqli_metadata_set_option( long option, const char* value); +/* + * Purpose : Copy an existing metadata object. + * + * Params : 1. The metadata object to be copied + * + * Returns : A copy of the metadata object. This object will need to be freed + * when no longer needed. + */ +extern nr_mysqli_metadata_t* nr_mysqli_metadata_copy( + nr_mysqli_metadata_t* metadata); + #endif /* NR_MYSQLI_METADATA_HDR */ diff --git a/axiom/nr_segment.c b/axiom/nr_segment.c index e93e11ee5..70495bda3 100644 --- a/axiom/nr_segment.c +++ b/axiom/nr_segment.c @@ -163,6 +163,16 @@ static void nr_segment_discard_merge_metrics(nr_segment_t* segment) { } } +const char* nr_segment_get_context(nr_segment_t* segment) { + if (NULL == segment || NULL == segment->txn) { + return NULL; + } + if (0 == segment->async_context) { + return NULL; + } + return nr_string_get(segment->txn->trace_strings, segment->async_context); +} + nr_segment_t* nr_segment_start(nrtxn_t* txn, nr_segment_t* parent, const char* async_context) { @@ -218,9 +228,30 @@ bool nr_segment_init(nr_segment_t* segment, if (parent) { segment->parent = parent; nr_segment_children_add(&parent->children, segment); - } /* Otherwise, the parent of this new segment is the current segment on the - transaction */ - else { + /* + * Special case: if the new async context stack does not exist and the async + * context is not NULL, then this indicates that the new segment is the root + * of a new async context. In that case, we'll parent it to the requested + * parent and set it as current for that async context. (Users who want to + * have their new context be parented to current on main should not provide + * a parent which handles that case.) */ + if (NULL != async_context + && NULL + == (nr_stack_t*)nr_hashmap_index_get(txn->parent_stacks, + segment->async_context)) { + /* + * This means this IS the first segment in the async context that is the + * child of a parent of another async context and so it will need to be + * set as current on the new context. + */ + nr_txn_set_current_segment(txn, segment); + } + + } else { + /* + * Otherwise, the parent of this new segment is the current segment on the + * transaction + */ nr_segment_t* current_segment = nr_txn_get_current_segment(txn, async_context); diff --git a/axiom/nr_segment.h b/axiom/nr_segment.h index af6a756dc..568c2f48b 100644 --- a/axiom/nr_segment.h +++ b/axiom/nr_segment.h @@ -285,10 +285,18 @@ typedef nr_segment_iter_return_t (*nr_segment_iter_t)(nr_segment_t* segment, * 3. The async_context to be applied to the segment, or NULL to * indicate that the segment is not asynchronous. * - * Note : At the time of this writing, if an explicit parent is supplied - * then an async_context must also be supplied. If parent is NULL - * and async is not NULL, or vice-versa, it can lead to undefined - * behavior in the agent. + * Note : + * If parent is NULL and async is not NULL: If the async context has a + * current segment, it will parent the segment to the current segment on the + * async context and become the current segment for the context; otherwise it + * will be parented to the current of the default context but will not itself + * become the current segment. + * If async is NULL and parent is not NULL, the segment will be parented to + * given parent with the default context, but it will not become the current + * segment on the txn or the default context. + * If parent is not NULL and async is not NULL, it will parent the segment to + * the parent and the segment will become the current segment on the async + * context. * * Returns : A segment. */ @@ -296,6 +304,29 @@ extern nr_segment_t* nr_segment_start(nrtxn_t* txn, nr_segment_t* parent, const char* async_context); +/* + * Purpose : Get a the string value of a given segment async context + * + * Params : 1. The segment. + * + * Note : This will be NULL for default/main, and a string context for async + * Returns : A pointer to the string context. + */ +extern const char* nr_segment_get_context(nr_segment_t* segment); + +/* + * Purpose : This is a wrapper to nr_segment_start to automatically start the + * segment with the current parent context. + */ +#define NR_SEGMENT_START_WITH_PARENT_CONTEXT(txn, parent) \ + nr_segment_start(txn, NULL, nr_segment_get_context(parent)) +/* + * Purpose : This is a wrapper to nr_segment_start to automatically start the + * segment with the current txn context. + */ +#define NR_SEGMENT_START_WITH_TXN_CONTEXT(txn) \ + nr_segment_start(txn, NULL, nr_txn_get_current_context(txn)) + /* * Purpose : Start an already allocated segment. * diff --git a/axiom/nr_segment_private.c b/axiom/nr_segment_private.c index b29b72e75..34d6fa40a 100644 --- a/axiom/nr_segment_private.c +++ b/axiom/nr_segment_private.c @@ -89,6 +89,7 @@ void nr_segment_destroy_fields(nr_segment_t* segment) { nr_segment_destroy_typed_attributes(segment->type, &segment->typed_attributes); nr_segment_error_destroy_fields(segment->error); + nr_segment_children_deinit(&segment->children); } void nr_segment_metric_destroy_fields(nr_segment_metric_t* sm) { diff --git a/axiom/nr_segment_traces.c b/axiom/nr_segment_traces.c index 6d8870565..3a408ee1c 100644 --- a/axiom/nr_segment_traces.c +++ b/axiom/nr_segment_traces.c @@ -392,7 +392,7 @@ nr_segment_iter_return_t nr_segment_traces_stot_iterator_callback( * Such segments are skipped, as zero duration segments don't make * sense. */ - if (segment->start_time == segment->stop_time) { + if ((segment->start_time == segment->stop_time) || (0 == segment->stop_time)) { return NR_SEGMENT_NO_POST_ITERATION_CALLBACK; } @@ -550,6 +550,9 @@ void nr_segment_traces_create_data( "generated for this transaction"); nr_string_pool_destroy(&segment_names); nr_buffer_destroy(&buf); + if (span_events) { + nr_vector_destroy(&span_events); + } return; } diff --git a/axiom/nr_txn.c b/axiom/nr_txn.c index 59be073c7..c4db0b0f9 100644 --- a/axiom/nr_txn.c +++ b/axiom/nr_txn.c @@ -571,11 +571,12 @@ nrtxn_t* nr_txn_begin(nrapp_t* app, nt->abs_start_time = nr_get_time(); /* - * Allocate the stacks to manage segment parenting + * Allocate the stacks to manage segment parenting and set context to default */ nr_stack_init(&nt->default_parent_stack, NR_STACK_DEFAULT_CAPACITY); nt->parent_stacks = nr_hashmap_create((nr_hashmap_dtor_func_t)nr_txn_destroy_parent_stack); + nt->current_async_context = 0; /* * Install the root segment @@ -1598,7 +1599,7 @@ void nr_txn_record_error(nrtxn_t* txn, /* Only try to get a span_id in cases where we know spans should be created. */ if (nr_txn_should_create_span_events(txn)) { - span_id = nr_txn_get_current_span_id(txn); + span_id = nr_txn_get_current_span_id(txn, nr_txn_get_current_context(txn)); /* * The specification says span_id MUST be included so if span events are @@ -1612,7 +1613,7 @@ void nr_txn_record_error(nrtxn_t* txn, } if (add_to_current_segment) { - current_segment = nr_txn_get_current_segment(txn, NULL); + current_segment = nr_txn_get_current_segment_txn_context(txn); if (current_segment) { nr_segment_set_error(current_segment, errmsg, errclass); @@ -1754,7 +1755,7 @@ nr_status_t nr_txn_add_user_custom_parameter(nrtxn_t* txn, } if (nr_txn_should_create_span_events(txn)) { - current = nr_txn_get_current_segment(txn, NULL); + current = nr_txn_get_current_segment_txn_context(txn); nr_segment_attributes_user_txn_event_add( current, NR_ATTRIBUTE_DESTINATION_SPAN, key, value); @@ -3190,6 +3191,16 @@ void nr_txn_finalize_parent_stacks(nrtxn_t* txn) { nr_txn_end_segments_in_stack(&txn->default_parent_stack, txn); } +const char* nr_txn_get_current_context(nrtxn_t* txn) { + if (NULL == txn) { + return NULL; + } + if (0 == txn->current_async_context) { + return NULL; + } + return nr_string_get(txn->trace_strings, txn->current_async_context); +} + nr_segment_t* nr_txn_get_current_segment(nrtxn_t* txn, const char* async_context) { if (nrunlikely(NULL == txn)) { @@ -3206,6 +3217,8 @@ nr_segment_t* nr_txn_get_current_segment(nrtxn_t* txn, nr_hashmap_index_get(txn->parent_stacks, (uint64_t)async_context_idx)); } + /* This is ONLY ever set with non-OAPI and PHPs less than 8. Do not assume or + * use force_current_segment with PHPs >= 8.0 */ if (txn->force_current_segment) { return txn->force_current_segment; } @@ -3231,15 +3244,20 @@ void nr_txn_set_current_segment(nrtxn_t* txn, nr_segment_t* segment) { if (NR_SUCCESS != nr_hashmap_index_set(txn->parent_stacks, key, (void*)stack)) { - // If we can't add the stack to the hashmap, then we should free it to - // avoid a memory leak. + /* + * If we can't add the stack to the hashmap, then we should free it to + * avoid a memory leak. + */ nr_stack_destroy_fields(stack); nr_free(stack); return; } } + txn->current_async_context = segment->async_context; } else { stack = &txn->default_parent_stack; + + txn->current_async_context = 0; } nr_stack_push(stack, (void*)segment); @@ -3255,6 +3273,13 @@ void nr_txn_retire_current_segment(nrtxn_t* txn, nr_segment_t* segment) { (nr_stack_t*)nr_hashmap_index_get(txn->parent_stacks, (uint64_t)segment->async_context), segment); + + if (NULL != segment->parent) { + txn->current_async_context = segment->parent->async_context; + } else { + txn->current_async_context = 0; + } + } else { nr_stack_remove_topmost(&txn->default_parent_stack, segment); } @@ -3284,15 +3309,15 @@ char* nr_txn_get_current_trace_id(nrtxn_t* txn) { return nr_strdup(trace_id); } -char* nr_txn_get_current_span_id(nrtxn_t* txn) { - nr_segment_t* segment; - char* span_id; +char* nr_txn_get_current_span_id(nrtxn_t* txn, const char* async_context) { + nr_segment_t* segment = NULL; + char* span_id = NULL; if (NULL == txn) { return NULL; } - segment = nr_txn_get_current_segment(txn, NULL); + segment = nr_txn_get_current_segment(txn, async_context); if (NULL == segment) { return NULL; } @@ -3422,7 +3447,8 @@ static void log_event_set_linking_metadata(nr_log_event_t* e, nr_log_event_set_trace_id(e, trace_id); nr_free(trace_id); - span_id = nr_txn_get_current_span_id(txn); + span_id = nr_txn_get_current_span_id( + txn, nr_string_get(txn->trace_strings, txn->current_async_context)); nr_log_event_set_span_id(e, span_id); nr_free(span_id); diff --git a/axiom/nr_txn.h b/axiom/nr_txn.h index ce2fee46b..f738bb852 100644 --- a/axiom/nr_txn.h +++ b/axiom/nr_txn.h @@ -275,7 +275,9 @@ typedef struct _nrtxn_t { context */ nr_segment_t* force_current_segment; /* Enforce a current segment for the default context, overriding the - default parent stack. */ + default parent stack. Only used with + PHP < 8.0. Do no tuse with OAPI; it is + not setup to utilize this.*/ size_t segment_count; /* A count of segments for this transaction, maintained throughout the life of this transaction */ nr_minmax_heap_t* @@ -287,6 +289,8 @@ typedef struct _nrtxn_t { * all segment start and end times are relative to * this field */ + int current_async_context; /* Execution context (pooled string index) */ + nr_error_t* error; /* Captured error */ nr_slowsqls_t* slowsqls; /* Slow SQL statements */ nrpool_t* datastore_products; /* Datastore products seen */ @@ -1058,6 +1062,16 @@ char* nr_txn_create_w3c_tracestate_header(const nrtxn_t* txn, */ extern bool nr_txn_should_create_span_events(const nrtxn_t* txn); +/* + * Purpose : Get a the string value of the current txn context + * + * Params : 1. The transaction. + * + * Note : This will be NULL for default/main, and a string context for async + * Returns : A pointer to string context. + */ +extern const char* nr_txn_get_current_context(nrtxn_t* txn); + /* * Purpose : Get a pointer to the currently-executing segment for a given * async context. @@ -1076,6 +1090,22 @@ extern bool nr_txn_should_create_span_events(const nrtxn_t* txn); extern nr_segment_t* nr_txn_get_current_segment(nrtxn_t* txn, const char* async_context); +/* + * Purpose : Get a pointer to the currently-executing segment for a given + * async context. + * + * Params : 1. The transaction. + * + * Returns : A pointer to the active segment for the currently executing segment + * based on the txn async_context. Note : Callers wishing to get the current + * segment on a specific context should use nr_txn_get_current_segment() to get + * the current segment on any other context they are interested in. + */ +static inline nr_segment_t* nr_txn_get_current_segment_txn_context( + nrtxn_t* txn) { + return nr_txn_get_current_segment(txn, nr_txn_get_current_context(txn)); +} + /* * Purpose : Force the given segment to be the current segment. * @@ -1089,6 +1119,8 @@ extern nr_segment_t* nr_txn_get_current_segment(nrtxn_t* txn, * * This function is useful to temporarily inject segments that don't * use the default allocator. + * Note : This is ONLY ever set with non-OAPI and PHPs less than 8.0. Do not + * use with OAPI; it is not setup to utilize this. */ inline static void nr_txn_force_current_segment(nrtxn_t* txn, nr_segment_t* segment) { @@ -1149,12 +1181,13 @@ extern char* nr_txn_get_current_trace_id(nrtxn_t* txn); * Purpose : Return the current span ID or create it if doesn't have one yet. * * Params : 1. The transaction. - * + * 2. The async context. * Note : The string returned has to be freed by the caller. * * Returns : current span ID if the segment is valid, otherwise NULL. */ -extern char* nr_txn_get_current_span_id(nrtxn_t* txn); +extern char* nr_txn_get_current_span_id(nrtxn_t* txn, + const char* async_context); /* * Purpose : End all currently active segments. diff --git a/axiom/tests/test_hashmap.c b/axiom/tests/test_hashmap.c index 67c74b91e..3ecf35454 100644 --- a/axiom/tests/test_hashmap.c +++ b/axiom/tests/test_hashmap.c @@ -139,9 +139,9 @@ static void test_get_set(void) { tlib_pass_if_status_failure("NULL hashmap", nr_hashmap_set(NULL, NR_PSTR("foo"), NULL)); - tlib_pass_if_status_failure("NULL hashmap", + tlib_pass_if_status_failure("NULL key", nr_hashmap_set(hashmap, NULL, 1, NULL)); - tlib_pass_if_status_failure("NULL hashmap", + tlib_pass_if_status_failure("NULL value", nr_hashmap_set(hashmap, NR_PSTR(""), NULL)); tlib_pass_if_size_t_equal("NULL hashmap", 0, nr_hashmap_count(NULL)); @@ -355,6 +355,57 @@ static void test_keys(void) { nr_hashmap_destroy(&hashmap); } +static void* clone_str(void* elem) { + return nr_strdup((char*)elem); +} + +static void test_copy(void) { + nr_hashmap_t* src + = nr_hashmap_create((nr_hashmap_dtor_func_t)nr_hashmap_dtor_str); + nr_hashmap_t* dest; + char* value_foo = nr_strdup("testfoo"); + char* value_bar = nr_strdup("testbar"); + char* value_baz = nr_strdup("testbaz"); + + tlib_pass_if_null("NULL src returns NULL", nr_hashmap_copy(NULL, clone_str)); + + nr_hashmap_set(src, NR_PSTR("foo"), value_foo); + nr_hashmap_set(src, NR_PSTR("bar"), value_bar); + nr_hashmap_set(src, NR_PSTR("baz"), value_baz); + + dest = nr_hashmap_copy(src, clone_str); + + tlib_pass_if_not_null("'dest' hashmap is not null", dest); + + tlib_pass_if_int_equal( + "destination hashmap contains the same number of elements as src", 3, + nr_hashmap_count(dest)); + + tlib_pass_if_int_equal("'foo' key exists", 1, + nr_hashmap_has(dest, NR_PSTR("foo"))); + tlib_pass_if_int_equal("'bar' key exists", 1, + nr_hashmap_has(dest, NR_PSTR("bar"))); + tlib_pass_if_int_equal("'baz' key exists", 1, + nr_hashmap_has(dest, NR_PSTR("baz"))); + + tlib_pass_if_str_equal("'foo' key produces 'testfoo' value", "testfoo", + nr_hashmap_get(dest, NR_PSTR("foo"))); + tlib_pass_if_str_equal("'bar' key produces 'testbar' value", "testbar", + nr_hashmap_get(dest, NR_PSTR("bar"))); + tlib_pass_if_str_equal("'baz' key produces 'testbaz' value", "testbaz", + nr_hashmap_get(dest, NR_PSTR("baz"))); + + tlib_fail_if_ptr_equal("'foo' value is cloned, not aliased", value_foo, + nr_hashmap_get(dest, NR_PSTR("foo"))); + tlib_fail_if_ptr_equal("'bar' value is cloned, not aliased", value_bar, + nr_hashmap_get(dest, NR_PSTR("bar"))); + tlib_fail_if_ptr_equal("'baz' value is cloned, not aliased", value_baz, + nr_hashmap_get(dest, NR_PSTR("baz"))); + + nr_hashmap_destroy(&src); + nr_hashmap_destroy(&dest); +} + void test_main(void* p NRUNUSED) { test_create_destroy(); test_apply(); @@ -365,4 +416,5 @@ void test_main(void* p NRUNUSED) { test_keys(); test_stress(); test_update(); + test_copy(); } diff --git a/axiom/tests/test_mysqli_metadata.c b/axiom/tests/test_mysqli_metadata.c index 94ac787be..ddc867dc3 100644 --- a/axiom/tests/test_mysqli_metadata.c +++ b/axiom/tests/test_mysqli_metadata.c @@ -9,6 +9,7 @@ #include "nr_mysqli_metadata.h" #include "nr_mysqli_metadata_private.h" +#include "util_object.h" #include "util_strings.h" #include "tlib_main.h" @@ -228,6 +229,41 @@ static void test_id(void) { tlib_pass_if_str_equal("1", "1", id); } +static void test_copy(void) { + nr_mysqli_metadata_t* src = NULL; + nr_mysqli_metadata_t* dest = NULL; + const nrobj_t* link = NULL; + + src = nr_mysqli_metadata_create(); + + nr_mysqli_metadata_set_connect(src, 1, "db-host", "db-user", "db-password", + "db-database", 3306, "db-socket", 1); + + dest = nr_mysqli_metadata_copy(src); + + tlib_pass_if_not_null("'dest' metadata is not NULL", dest); + + link = nro_get_hash_value(dest->links, "1", NULL); + + tlib_pass_if_not_null("link", link); + tlib_pass_if_str_equal("host", "db-host", + nro_get_hash_string(link, "host", NULL)); + tlib_pass_if_str_equal("user", "db-user", + nro_get_hash_string(link, "user", NULL)); + tlib_pass_if_str_equal("password", "db-password", + nro_get_hash_string(link, "password", NULL)); + tlib_pass_if_str_equal("database", "db-database", + nro_get_hash_string(link, "database", NULL)); + tlib_pass_if_str_equal("socket", "db-socket", + nro_get_hash_string(link, "socket", NULL)); + tlib_pass_if_int_equal("port", 3306, nro_get_hash_int(link, "port", NULL)); + tlib_pass_if_int64_t_equal("flags", 1, + nro_get_hash_long(link, "flags", NULL)); + + nr_mysqli_metadata_destroy(&src); + nr_mysqli_metadata_destroy(&dest); +} + tlib_parallel_info_t parallel_info = {.suggested_nthreads = 2, .state_size = 0}; void test_main(void* p NRUNUSED) { @@ -238,4 +274,5 @@ void test_main(void* p NRUNUSED) { test_set_database(); test_set_option(); test_id(); + test_copy(); } diff --git a/axiom/tests/test_stack.c b/axiom/tests/test_stack.c index 3df0d163b..82e316041 100644 --- a/axiom/tests/test_stack.c +++ b/axiom/tests/test_stack.c @@ -121,6 +121,208 @@ static void test_get(void) { nr_stack_destroy_fields(&s); } +static void* clone_identity(void* element) { + return element; +} + +static int clone_call_count = 0; + +static void* clone_counting(void* element) { + clone_call_count++; + return element; +} + +static void* clone_strdup(void* element) { + return nr_strdup((const char*)element); +} + +static int dtor_call_count = 0; + +static void dtor_free_string(void* element, void* userdata NRUNUSED) { + dtor_call_count++; + nr_free(element); +} + +static void test_copy_empty(void) { + nr_stack_t src; + nr_stack_t dest; + + nr_stack_init(&src, 5); + + dest = nr_stack_copy(&src, clone_identity); + + tlib_pass_if_true("Copy of an empty stack must be empty", + nr_stack_is_empty(&dest), "Expected true"); + tlib_pass_if_size_t_equal("Copy of an empty stack must have size 0", 0, + dest.used); + tlib_pass_if_not_null( + "Copy of an empty stack must have allocated memory for its elements", + dest.elements); + + nr_stack_destroy_fields(&src); + nr_stack_destroy_fields(&dest); +} + +static void test_copy_preserves_order(void) { + nr_stack_t src; + nr_stack_t dest; + + nr_stack_init(&src, 5); + nr_stack_push(&src, (void*)1); + nr_stack_push(&src, (void*)2); + nr_stack_push(&src, (void*)3); + + dest = nr_stack_copy(&src, clone_identity); + + tlib_pass_if_size_t_equal("Copy must have the same size as the source", 3, + dest.used); + tlib_pass_if_ptr_equal("Copy must preserve order: top element matches", + (void*)3, nr_stack_pop(&dest)); + tlib_pass_if_ptr_equal("Copy must preserve order: middle element matches", + (void*)2, nr_stack_pop(&dest)); + tlib_pass_if_ptr_equal("Copy must preserve order: bottom element matches", + (void*)1, nr_stack_pop(&dest)); + + tlib_pass_if_size_t_equal("Source must remain unchanged after copy", 3, + src.used); + tlib_pass_if_ptr_equal("Source top must remain unchanged after copy", + (void*)3, nr_stack_get_top(&src)); + + nr_stack_destroy_fields(&src); + nr_stack_destroy_fields(&dest); +} + +static void test_copy_invokes_clone_per_element(void) { + nr_stack_t src; + nr_stack_t dest; + + clone_call_count = 0; + + nr_stack_init(&src, 5); + nr_stack_push(&src, (void*)1); + nr_stack_push(&src, (void*)2); + nr_stack_push(&src, (void*)3); + nr_stack_push(&src, (void*)4); + + dest = nr_stack_copy(&src, clone_counting); + + tlib_pass_if_int_equal("Clone callback must be invoked once per element", 4, + clone_call_count); + + nr_stack_destroy_fields(&src); + nr_stack_destroy_fields(&dest); +} + +static void test_copy_deep(void) { + nr_stack_t src; + nr_stack_t dest; + char* a = nr_strdup("alpha"); + char* b = nr_strdup("beta"); + void* dest_top; + + /* Use nr_vector_init directly to install a destructor; the inherited + dtor will free the cloned strings when dest is destroyed. */ + nr_vector_init(&src, 5, dtor_free_string, NULL); + nr_stack_push(&src, a); + nr_stack_push(&src, b); + + dtor_call_count = 0; + dest = nr_stack_copy(&src, clone_strdup); + + tlib_pass_if_size_t_equal("Deep copy must have the same size as the source", + 2, dest.used); + + dest_top = nr_stack_get_top(&dest); + tlib_pass_if_str_equal("Deep copy contents must equal source contents", + "beta", (const char*)dest_top); + tlib_pass_if_true( + "Deep copy must produce distinct allocations from the source", + dest_top != b, "Expected pointer inequality"); + + nr_stack_destroy_fields(&dest); + + tlib_pass_if_int_equal( + "Destroying the copy must invoke the inherited dtor on each cloned " + "element", + 2, dtor_call_count); + + nr_stack_destroy_fields(&src); + + tlib_pass_if_int_equal( + "Destroying the source must invoke the dtor on the source's own " + "elements, leaving cloned-element frees independent", + 4, dtor_call_count); +} + +static void test_copy_independent_of_source(void) { + nr_stack_t src; + nr_stack_t dest; + + nr_stack_init(&src, 5); + nr_stack_push(&src, (void*)10); + nr_stack_push(&src, (void*)20); + + dest = nr_stack_copy(&src, clone_identity); + + /* Mutating the source must not affect the copy. */ + nr_stack_push(&src, (void*)30); + + tlib_pass_if_size_t_equal( + "Pushing to the source after copy must not affect the copy", 2, + dest.used); + tlib_pass_if_ptr_equal("Copy's top must remain the source's top at copy time", + (void*)20, nr_stack_get_top(&dest)); + + /* Mutating the copy must not affect the source. */ + (void)nr_stack_pop(&dest); + tlib_pass_if_size_t_equal( + "Popping from the copy must not affect the source's size", 3, src.used); + tlib_pass_if_ptr_equal("Source's top must remain unchanged after copy pop", + (void*)30, nr_stack_get_top(&src)); + + nr_stack_destroy_fields(&src); + nr_stack_destroy_fields(&dest); +} + +static void test_copy_null_inputs(void) { + nr_stack_t src; + nr_stack_t dest; + + clone_call_count = 0; + + dest = nr_stack_copy(NULL, clone_counting); + tlib_pass_if_true("Copy of a NULL source must be empty", + nr_stack_is_empty(&dest), "Expected true"); + tlib_pass_if_not_null( + "Copy of a NULL source must be an initialized stack with allocated " + "element storage", + dest.elements); + tlib_pass_if_int_equal( + "Copy of a NULL source must not invoke the clone callback", 0, + clone_call_count); + nr_stack_destroy_fields(&dest); + + nr_stack_init(&src, 5); + nr_stack_push(&src, (void*)1); + nr_stack_push(&src, (void*)2); + + dest = nr_stack_copy(&src, NULL); + tlib_pass_if_true( + "Copy with a NULL clone callback must produce an empty stack", + nr_stack_is_empty(&dest), "Expected true"); + tlib_pass_if_not_null( + "Copy with a NULL clone callback must produce an initialized stack with " + "allocated element storage", + dest.elements); + + tlib_pass_if_size_t_equal( + "Source must remain unchanged after a copy with a NULL clone", 2, + src.used); + + nr_stack_destroy_fields(&dest); + nr_stack_destroy_fields(&src); +} + static void test_remove_topmost(void) { nr_stack_t s; @@ -179,4 +381,10 @@ void test_main(void* p NRUNUSED) { test_push_pop_extended(); test_get(); test_remove_topmost(); + test_copy_empty(); + test_copy_preserves_order(); + test_copy_invokes_clone_per_element(); + test_copy_deep(); + test_copy_independent_of_source(); + test_copy_null_inputs(); } diff --git a/axiom/tests/test_txn.c b/axiom/tests/test_txn.c index 1101f41b8..5c53c9c68 100644 --- a/axiom/tests/test_txn.c +++ b/axiom/tests/test_txn.c @@ -7807,10 +7807,16 @@ static void test_get_current_trace_id(void) { static void test_get_current_span_id(void) { nrapp_t app; nrtxnopt_t opts; - nr_segment_t* segment; - char* span_id; - nrtxn_t* txn; + nr_segment_t* segment = NULL; + nr_segment_t* segment_async = NULL; + char* span_id = NULL; + char* span_id_async = NULL; + nrtxn_t* txn = NULL; uint32_t priority; + const char* async_context = "sunshine"; + nr_segment_t* parent; + nr_segment_t* first_born; + nr_segment_t* async_grandchild; /* start txn and segment */ nr_memset(&app, 0, sizeof(app)); @@ -7818,28 +7824,28 @@ static void test_get_current_span_id(void) { app.state = NR_APP_OK; opts.distributed_tracing_enabled = 1; txn = nr_txn_begin(&app, &opts, NULL, NULL); - segment = nr_segment_start(txn, txn->segment_root, NULL); + segment = nr_segment_start(txn, NULL, NULL); nr_distributed_trace_set_sampled(txn->distributed_trace, true); - nr_txn_set_current_segment(txn, segment); /* * Test : Bad parameters */ tlib_pass_if_null("no span id. txn is null", - nr_txn_get_current_span_id(NULL)); + nr_txn_get_current_span_id(NULL, NULL)); /* * Test : disabled span events */ txn->options.span_events_enabled = 0; - tlib_pass_if_null("span events disabled", nr_txn_get_current_span_id(txn)); + tlib_pass_if_null("span events disabled", + nr_txn_get_current_span_id(txn, NULL)); /* - * Test : span id is created + * Test : span id is created for default (NULL) context */ txn->options.span_events_enabled = 1; - span_id = nr_txn_get_current_span_id(txn); - tlib_fail_if_null("span id is created", span_id); + span_id = nr_txn_get_current_span_id(txn, NULL); + tlib_fail_if_null("span id is created for default (NULL) context", span_id); nr_free(span_id); /* @@ -7849,6 +7855,67 @@ static void test_get_current_span_id(void) { tlib_pass_if_true("log segment priority", priority & NR_SEGMENT_PRIORITY_LOG, "priority=0x%08x", priority); + /* + * Test : span id is created for a segment with async context + */ + + segment_async = nr_segment_start(txn, segment, async_context); + parent = nr_segment_start(txn, NULL, NULL); + first_born = nr_segment_start(txn, NULL, NULL); + tlib_pass_if_not_null("parent should not be null", parent); + + async_grandchild = nr_segment_start(txn, first_born, "another_async"); + tlib_pass_if_not_null( + "Starting a segment on a valid txn and an implicit parent must succeed", + async_grandchild); + + tlib_pass_if_int_equal( + "grandchild segment has initialized async context", + async_grandchild->async_context, + nr_string_find(async_grandchild->txn->trace_strings, "another_async")); + + tlib_pass_if_int_equal( + "grandchild has initialized async context", + nr_string_find(txn->trace_strings, "another_async"), + nr_string_find(async_grandchild->txn->trace_strings, "another_async")); + + tlib_pass_if_ptr_equal( + "The most recently started, implicitly-parented segment must not alter " + "the NULL context's current segment", + nr_txn_get_current_segment(txn, NULL), first_born); + + tlib_pass_if_ptr_equal( + "The most recently started, implicitly-parented segment must set the " + "current segment for the new context", + nr_txn_get_current_segment(txn, "another_async"), async_grandchild); + + tlib_pass_if_not_null("Default Segment should not be null", segment); + tlib_pass_if_not_null("Async Segment should not be null", segment_async); + tlib_pass_if_int_equal( + "Async segment has initialized async context", + segment_async->async_context, + nr_string_find(segment_async->txn->trace_strings, async_context)); + tlib_pass_if_ptr_equal( + "should be able to retrieve current of an async context", + nr_txn_get_current_segment(txn, async_context), segment_async); + + span_id_async = nr_txn_get_current_span_id(txn, async_context); + tlib_fail_if_null("span id is created for segment with async context", + span_id_async); + nr_free(span_id_async); + + /* + * Test : default segment and async segment are different. + * And span ids are also different. + */ + span_id_async = nr_txn_get_current_span_id(txn, async_context); + span_id = nr_txn_get_current_span_id(txn, NULL); + tlib_pass_if_str_equal( + "async current span id should not equal default current span id", + span_id_async, span_id); + nr_free(span_id); + nr_free(span_id_async); + nr_txn_destroy(&txn); } diff --git a/axiom/util_hashmap.c b/axiom/util_hashmap.c index 08d5c3d5e..e50c06c9b 100644 --- a/axiom/util_hashmap.c +++ b/axiom/util_hashmap.c @@ -365,3 +365,27 @@ nr_vector_t* nr_hashmap_keys(nr_hashmap_t* hashmap) { return keys; } + +nr_hashmap_t* nr_hashmap_copy(nr_hashmap_t* src, + nr_hashmap_clone_elem_t clone_fn) { + nr_hashmap_t* hashmap = NULL; + size_t num_buckets; + size_t i; + if (NULL == src) { + return NULL; + } + + hashmap = nr_hashmap_create(src->dtor_func); + num_buckets = nr_hashmap_count_buckets(src); + + for (i = 0; i < num_buckets; i++) { + nr_hashmap_bucket_t* bucket = NULL; + + for (bucket = src->buckets[i]; bucket; bucket = bucket->next) { + nr_hashmap_set(hashmap, bucket->key.value, bucket->key.length, + clone_fn(bucket->value)); + } + } + + return hashmap; +} diff --git a/axiom/util_hashmap.h b/axiom/util_hashmap.h index 13125bfe4..f93b0b2d8 100644 --- a/axiom/util_hashmap.h +++ b/axiom/util_hashmap.h @@ -21,6 +21,7 @@ * The opaque hashmap type. */ typedef struct _nr_hashmap_t nr_hashmap_t; +typedef void* (*nr_hashmap_clone_elem_t)(void* element); /* * Type declaration for apply functions. @@ -187,6 +188,19 @@ extern void nr_hashmap_update(nr_hashmap_t* hashmap, */ extern nr_vector_t* nr_hashmap_keys(nr_hashmap_t* hashmap); +/* + * Purpose : Return a copy of a hashmap. + * + * Params : 1. The hashmap to copy. + * 2. A clone function invoked on each element's value to produce the + * value stored in the copy. Must not be NULL. + * + * Returns : A copy of the provided hashmap. This will have to be freed when no + * longer needed. + */ +extern nr_hashmap_t* nr_hashmap_copy(nr_hashmap_t* src, + nr_hashmap_clone_elem_t clone_fn); + /* * The below functions are simple wrappers for the main functions above that * allow uint64_t keys to be used. diff --git a/axiom/util_stack.c b/axiom/util_stack.c index 74771252a..0eaa5eca9 100644 --- a/axiom/util_stack.c +++ b/axiom/util_stack.c @@ -55,3 +55,23 @@ bool nr_stack_remove_topmost(nr_stack_t* s, const void* element) { return false; } + +nr_stack_t nr_stack_copy(nr_stack_t* src, nr_stack_clone_elem_ptr_t clone) { + nr_stack_t dest; + size_t n; + + if (NULL == src || NULL == clone) { + nr_vector_init(&dest, NR_STACK_DEFAULT_CAPACITY, NULL, NULL); + return dest; + } + + n = nr_vector_size(src); + nr_vector_init(&dest, n ? n : NR_STACK_DEFAULT_CAPACITY, src->dtor, NULL); + + for (size_t i = 0; i < n; i++) { + void* orig = nr_vector_get(src, i); + nr_stack_push(&dest, clone(orig)); + } + + return dest; +} diff --git a/axiom/util_stack.h b/axiom/util_stack.h index dd166a780..86e9e730a 100644 --- a/axiom/util_stack.h +++ b/axiom/util_stack.h @@ -20,6 +20,7 @@ #define NR_STACK_DEFAULT_CAPACITY 32 typedef struct _nr_vector_t nr_stack_t; +typedef void* (*nr_stack_clone_elem_ptr_t)(void* element); /* * Purpose : Initialize a stack data type. @@ -98,4 +99,25 @@ void nr_stack_destroy_fields(nr_stack_t* s); */ bool nr_stack_remove_topmost(nr_stack_t* s, const void* element); +/* + * Purpose : Create a copy of a stack. Each element of src is passed through + * the supplied clone callback to produce the corresponding element + * in the returned stack, so the depth of the copy is determined by + * the callback (return the input pointer for a shallow copy, or + * allocate and populate a duplicate for a deep copy). The source + * stack's contents and order are preserved. + * + * The destination stack inherits the source stack's destructor + * (src->dtor); its destructor userdata is set to NULL. + * + * Params : 1. A pointer to the source stack, src. + * 2. A clone callback invoked once per element. + * + * Returns : A new stack containing the cloned elements of src, in the same + * order. If src or clone is NULL, an initialized empty stack is + * returned. The caller is responsible for releasing the returned + * stack with nr_stack_destroy_fields(). + */ +nr_stack_t nr_stack_copy(nr_stack_t* src, nr_stack_clone_elem_ptr_t clone); + #endif /* NR_STACK_HDR */ diff --git a/tests/include/newrelic-integration/src/Trace.php b/tests/include/newrelic-integration/src/Trace.php index 38a98c2f8..a4d15a3af 100644 --- a/tests/include/newrelic-integration/src/Trace.php +++ b/tests/include/newrelic-integration/src/Trace.php @@ -112,6 +112,20 @@ public function findSegmentsByName($name) }); } + /** + * Finds all segments within the transaction that contain the given name substring. + * + * @param string $name The substring to search for. + * @return Segment[] A traversable object which will yield each segment + * matching the given name. + */ + public function findSegmentsBySubstring($name) + { + return $this->findSegments(function (Segment $segment) use ($name) { + return strpos($segment->name, $name) !== false; + }); + } + /** * Finds all segments within the transaction that have datastore instance * metadata. diff --git a/tests/integration/external/curl_exec/test_http_fibers.php b/tests/integration/external/curl_exec/test_http_fibers.php new file mode 100644 index 000000000..d1a37c47f --- /dev/null +++ b/tests/integration/external/curl_exec/test_http_fibers.php @@ -0,0 +1,195 @@ += 8.1 required\n"); +} +if (version_compare(PHP_VERSION, "8.5", ">=")) { + die("skip: PHP >= 8.5.0 curl_close deprecated\n"); +} +if (!extension_loaded("curl")) { + die("skip: curl extension required"); +} +*/ + +/*INI +newrelic.fibers.disabled = false +*/ + +/*EXPECT +ok - simple hostname +Starting Func 'a' +ok - strip query string +ok - strip fragment +Ending Func 'a' +ok - strip credentials +*/ + +/*EXPECT_SPAN_EVENTS_LIKE +[ + [ + { + "traceId": "??", + "duration": "??", + "transactionId": "??", + "name": "OtherTransaction\/php__FILE__", + "guid": "ENV[GUID_ROOT]", + "type": "Span", + "category": "generic", + "priority": "??", + "sampled": true, + "nr.entryPoint": true, + "timestamp": "??", + "transaction.name": "OtherTransaction\/php__FILE__" + }, + {}, + {} + ], + [ + { + "category": "generic", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "Custom\/test_curl", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "ENV[GUID_ROOT]" + }, + {}, + {} + ], + [ + { + "category": "http", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "External\/127.0.0.1\/all", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "ENV[GUID_TEST_CURL]", + "span.kind": "client", + "component": "curl" + }, + {}, + { + "http.method": "GET", + "http.url": "http:\/\/ENV[EXTERNAL_HOST]\/", + "http.statusCode": 200 + } + ], + [ + { + "category": "generic", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "Custom\/a", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "ENV[GUID_ROOT]" + }, + {}, + {} + ] +] +*/ + +/*EXPECT_METRICS +[ + "?? agent run id", + "?? start time", + "?? stop time", + [ + [{"name":"DurationByCaller/Unknown/Unknown/Unknown/Unknown/all"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"DurationByCaller/Unknown/Unknown/Unknown/Unknown/allOther"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Supportability/TraceContext/Create/Success"}, [4, "??", "??", "??", "??", "??"]], + [{"name":"Supportability/DistributedTrace/CreatePayload/Success"},[4, "??", "??", "??", "??", "??"]], + [{"name":"External/all"}, [4, "??", "??", "??", "??", "??"]], + [{"name":"External/allOther"}, [4, "??", "??", "??", "??", "??"]], + [{"name":"External/127.0.0.1/all"}, [4, "??", "??", "??", "??", "??"]], + [{"name":"External/127.0.0.1/all", + "scope":"OtherTransaction/php__FILE__"}, [4, "??", "??", "??", "??", "??"]], + [{"name":"OtherTransaction/all"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"OtherTransaction/php__FILE__"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"OtherTransactionTotalTime"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"OtherTransactionTotalTime/php__FILE__"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Supportability/Logging/Forwarding/PHP/enabled"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Supportability/Logging/Metrics/PHP/enabled"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Supportability/Logging/LocalDecorating/PHP/disabled"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Supportability/Logging/Labels/PHP/disabled"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Supportability/api/get_linking_metadata"}, ["??", "??", "??", "??", "??", "??"]], + [{"name":"Supportability/PHP/Fiber/used"}, ["??", "??", "??", "??", "??", "??"]] + ] +] +*/ + +require_once(realpath(dirname(__FILE__)) . '/../../../include/helpers.php'); +require_once(realpath(dirname(__FILE__)) . '/../../../include/tap.php'); +require_once(realpath(dirname(__FILE__)) . '/../../../include/config.php'); + +function test_curl() +{ + global $EXTERNAL_HOST; + + env_var_for_expects("GUID_TEST_CURL", newrelic_get_linking_metadata()['span.id'] ?? ''); + $ch = curl_init(); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + + curl_setopt($ch, CURLOPT_URL, 'http://' . $EXTERNAL_HOST . ''); + tap_not_equal(false, curl_exec($ch), 'simple hostname'); + + /* Query string should be stripped. */ + Fiber::suspend(); + curl_setopt($ch, CURLOPT_URL, 'http://' . $EXTERNAL_HOST . '?a=1&b=2'); + tap_not_equal(false, curl_exec($ch), 'strip query string'); + + /* Fragment should be stripped. */ + curl_setopt($ch, CURLOPT_URL, 'http://' . $EXTERNAL_HOST . '/#fragment'); + tap_not_equal(false, curl_exec($ch), 'strip fragment'); + + /* Auth credentials should be stripped. */ + Fiber::suspend(); + curl_setopt($ch, CURLOPT_URL, 'http://user:pass@' . $EXTERNAL_HOST . ''); + tap_not_equal(false, curl_exec($ch), 'strip credentials'); + +} + +function a() +{ + echo "Starting Func 'a'\n"; + env_var_for_expects("GUID_A", newrelic_get_linking_metadata()['span.id'] ?? ''); + time_nanosleep(0, 100000000); + Fiber::suspend(); + echo "Ending Func 'a'\n"; +} + +env_var_for_expects("GUID_ROOT", newrelic_get_linking_metadata()['span.id'] ?? ''); + +$fiber_a = new Fiber('a'); +$fiber_curl = new Fiber('test_curl'); + +$fiber_curl->start(); +$fiber_a->start(); +$fiber_curl->resume(); +$fiber_a->resume(); +$fiber_curl->resume(); diff --git a/tests/integration/external/curl_multi_exec/test_exec_add_handles_fibers.php b/tests/integration/external/curl_multi_exec/test_exec_add_handles_fibers.php new file mode 100644 index 000000000..bb092c6db --- /dev/null +++ b/tests/integration/external/curl_multi_exec/test_exec_add_handles_fibers.php @@ -0,0 +1,212 @@ += 8.1 required\n"); +} +if (!extension_loaded("curl")) { + die("skip: curl extension required"); +} +*/ + +/*INI +newrelic.fibers.disabled = false +*/ + +/*EXPECT +Starting Func 'a' +traceparent=found tracestate=found newrelic=found X-NewRelic-ID=missing X-NewRelic-Transaction=missing tracing endpoint reached +traceparent=found tracestate=found newrelic=found X-NewRelic-ID=missing X-NewRelic-Transaction=missing tracing endpoint reached +traceparent=found tracestate=found newrelic=found X-NewRelic-ID=missing X-NewRelic-Transaction=missing tracing endpoint reached +Ending Func 'a' +*/ + +/*EXPECT_SPAN_EVENTS_LIKE +[ + [ + { + "traceId": "??", + "duration": "??", + "transactionId": "??", + "name": "OtherTransaction\/php__FILE__", + "guid": "ENV[GUID_ROOT]", + "type": "Span", + "category": "generic", + "priority": "??", + "sampled": true, + "nr.entryPoint": true, + "timestamp": "??", + "transaction.name": "OtherTransaction\/php__FILE__" + }, + {}, + {} + ], + [ + { + "category": "generic", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "Custom\/test_curl_multi_exec_add_handles", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "ENV[GUID_ROOT]" + }, + {}, + {} + ], + [ + { + "category": "generic", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "curl_multi_exec", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "ENV[GUID_TEST_ADD]" + }, + {}, + {} + ], + [ + { + "category": "http", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "External\/127.0.0.1\/all", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "??", + "span.kind": "client", + "component": "curl" + }, + {}, + { + "http.method": "GET", + "http.url": "http:\/\/ENV[EXTERNAL_HOST]\/cat", + "http.statusCode": 200 + } + ], + [ + { + "category": "generic", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "Custom\/a", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "ENV[GUID_ROOT]" + }, + {}, + {} + ] +] +*/ + +/*EXPECT_METRICS +[ + "?? agent run id", + "?? start time", + "?? stop time", + [ + [{"name":"DurationByCaller/Unknown/Unknown/Unknown/Unknown/all"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"DurationByCaller/Unknown/Unknown/Unknown/Unknown/allOther"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Supportability/TraceContext/Create/Success"}, [3, "??", "??", "??", "??", "??"]], + [{"name":"Supportability/DistributedTrace/CreatePayload/Success"},[3, "??", "??", "??", "??", "??"]], + [{"name":"External/all"}, [3, "??", "??", "??", "??", "??"]], + [{"name":"External/allOther"}, [3, "??", "??", "??", "??", "??"]], + [{"name":"External/127.0.0.1/all"}, [3, "??", "??", "??", "??", "??"]], + [{"name":"External/127.0.0.1/all", + "scope":"OtherTransaction/php__FILE__"}, [3, "??", "??", "??", "??", "??"]], + [{"name":"OtherTransaction/all"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"OtherTransaction/php__FILE__"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"OtherTransactionTotalTime"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"OtherTransactionTotalTime/php__FILE__"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Supportability/Logging/Forwarding/PHP/enabled"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Supportability/Logging/Metrics/PHP/enabled"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Supportability/Logging/LocalDecorating/PHP/disabled"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Supportability/Logging/Labels/PHP/disabled"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Supportability/api/get_linking_metadata"}, ["??", "??", "??", "??", "??", "??"]], + [{"name":"Supportability/PHP/Fiber/used"}, ["??", "??", "??", "??", "??", "??"]] + ] +] +*/ + +require_once(realpath(dirname(__FILE__)) . '/../../../include/helpers.php'); +require_once(realpath(dirname(__FILE__)) . '/../../../include/tap.php'); +require_once(realpath(dirname(__FILE__)) . '/../../../include/config.php'); + +function test_curl_multi_exec_add_handles() +{ + env_var_for_expects("GUID_TEST_ADD", newrelic_get_linking_metadata()['span.id'] ?? ''); + $url = make_tracing_url(realpath(dirname(__FILE__)) . '/../../../include/tracing_endpoint.php'); + + $ch1 = curl_init($url); + $ch2 = curl_init($url); + $ch3 = curl_init($url); + $mh = curl_multi_init(); + + curl_multi_add_handle($mh, $ch1); + curl_multi_add_handle($mh, $ch2); + + $active = 0; + curl_multi_exec($mh, $active); + + Fiber::suspend(); + curl_multi_add_handle($mh, $ch3); + + Fiber::suspend(); + do { + curl_multi_exec($mh, $active); + Fiber::suspend($active); + } while ($active > 0); + + curl_multi_close($mh); +} + +function a() +{ + echo "Starting Func 'a'\n"; + env_var_for_expects("GUID_A", newrelic_get_linking_metadata()['span.id'] ?? ''); + time_nanosleep(0, 100000000); + Fiber::suspend(); + echo "Ending Func 'a'\n"; +} + +env_var_for_expects("GUID_ROOT", newrelic_get_linking_metadata()['span.id'] ?? ''); + +$fiber_a = new Fiber('a'); +$fiber_add = new Fiber('test_curl_multi_exec_add_handles'); + +$fiber_add->start(); +$fiber_a->start(); +$result = $fiber_add->resume(); +do { + $result = $fiber_add->resume(); +} while ($result > 0); +$fiber_a->resume(); +$fiber_add->resume(); diff --git a/tests/integration/external/curl_multi_exec/test_exec_remove_handles_fibers.php b/tests/integration/external/curl_multi_exec/test_exec_remove_handles_fibers.php new file mode 100644 index 000000000..4147a815a --- /dev/null +++ b/tests/integration/external/curl_multi_exec/test_exec_remove_handles_fibers.php @@ -0,0 +1,203 @@ += 8.1 required\n"); +} +if (!extension_loaded("curl")) { + die("skip: curl extension required"); +} +*/ + +/*INI +newrelic.fibers.disabled = false +*/ + +/*EXPECT +Starting Func 'a' +traceparent=found tracestate=found newrelic=found X-NewRelic-ID=missing X-NewRelic-Transaction=missing tracing endpoint reached +traceparent=found tracestate=found newrelic=found X-NewRelic-ID=missing X-NewRelic-Transaction=missing tracing endpoint reached +traceparent=found tracestate=found newrelic=found X-NewRelic-ID=missing X-NewRelic-Transaction=missing tracing endpoint reached +Ending Func 'a' +*/ + +/*EXPECT_SPAN_EVENTS_LIKE +[ + [ + { + "traceId": "??", + "duration": "??", + "transactionId": "??", + "name": "OtherTransaction\/php__FILE__", + "guid": "ENV[GUID_ROOT]", + "type": "Span", + "category": "generic", + "priority": "??", + "sampled": true, + "nr.entryPoint": true, + "timestamp": "??", + "transaction.name": "OtherTransaction\/php__FILE__" + }, + {}, + {} + ], + [ + { + "category": "generic", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "Custom\/test_curl_multi_exec_remove_handles", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "ENV[GUID_ROOT]" + }, + {}, + {} + ], + [ + { + "category": "http", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "External\/127.0.0.1\/all", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "ENV[GUID_TEST_REMOVE]", + "span.kind": "client", + "component": "curl" + }, + {}, + { + "http.method": "GET", + "http.url": "http:\/\/ENV[EXTERNAL_HOST]\/cat", + "http.statusCode": 200 + } + ], + [ + { + "category": "generic", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "Custom\/a", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "ENV[GUID_ROOT]" + }, + {}, + {} + ] +] +*/ + +/*EXPECT_METRICS +[ + "?? agent run id", + "?? start time", + "?? stop time", + [ + [{"name":"DurationByCaller/Unknown/Unknown/Unknown/Unknown/all"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"DurationByCaller/Unknown/Unknown/Unknown/Unknown/allOther"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Supportability/TraceContext/Create/Success"}, [6, "??", "??", "??", "??", "??"]], + [{"name":"Supportability/DistributedTrace/CreatePayload/Success"},[6, "??", "??", "??", "??", "??"]], + [{"name":"External/all"}, [3, "??", "??", "??", "??", "??"]], + [{"name":"External/allOther"}, [3, "??", "??", "??", "??", "??"]], + [{"name":"External/127.0.0.1/all"}, [3, "??", "??", "??", "??", "??"]], + [{"name":"External/127.0.0.1/all", + "scope":"OtherTransaction/php__FILE__"}, [3, "??", "??", "??", "??", "??"]], + [{"name":"OtherTransaction/all"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"OtherTransaction/php__FILE__"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"OtherTransactionTotalTime"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"OtherTransactionTotalTime/php__FILE__"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Supportability/Logging/Forwarding/PHP/enabled"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Supportability/Logging/Metrics/PHP/enabled"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Supportability/Logging/LocalDecorating/PHP/disabled"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Supportability/Logging/Labels/PHP/disabled"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Supportability/api/get_linking_metadata"}, ["??", "??", "??", "??", "??", "??"]], + [{"name":"Supportability/PHP/Fiber/used"}, ["??", "??", "??", "??", "??", "??"]] + ] +] +*/ + +require_once(realpath(dirname(__FILE__)) . '/../../../include/helpers.php'); +require_once(realpath(dirname(__FILE__)) . '/../../../include/tap.php'); +require_once(realpath(dirname(__FILE__)) . '/../../../include/config.php'); + +function test_curl_multi_exec_remove_handles() +{ + env_var_for_expects("GUID_TEST_REMOVE", newrelic_get_linking_metadata()['span.id'] ?? ''); + $url = make_tracing_url(realpath(dirname(__FILE__)) . '/../../../include/tracing_endpoint.php'); + + $ch1 = curl_init($url); + $ch2 = curl_init($url); + $ch3 = curl_init($url); + $ch4 = curl_init($url); + $mh = curl_multi_init(); + + curl_multi_add_handle($mh, $ch1); + curl_multi_add_handle($mh, $ch2); + curl_multi_add_handle($mh, $ch3); + curl_multi_add_handle($mh, $ch4); + + $active = 0; + curl_multi_exec($mh, $active); + + Fiber::suspend(); + curl_multi_remove_handle($mh, $ch2); + curl_multi_remove_handle($mh, $ch3); + curl_multi_remove_handle($mh, $ch4); + curl_multi_add_handle($mh, $ch4); + + curl_exec($ch3); + + Fiber::suspend(); + do { + curl_multi_exec($mh, $active); + Fiber::suspend($active); + } while ($active > 0); + + curl_multi_close($mh); +} + +function a() +{ + echo "Starting Func 'a'\n"; + env_var_for_expects("GUID_A", newrelic_get_linking_metadata()['span.id'] ?? ''); + time_nanosleep(0, 100000000); + Fiber::suspend(); + echo "Ending Func 'a'\n"; +} + +env_var_for_expects("GUID_ROOT", newrelic_get_linking_metadata()['span.id'] ?? ''); + +$fiber_a = new Fiber('a'); +$fiber_remove = new Fiber('test_curl_multi_exec_remove_handles'); + +$fiber_remove->start(); +$fiber_a->start(); +$result = $fiber_remove->resume(); +do { + $result = $fiber_remove->resume(); +} while ($result > 0); +$fiber_a->resume(); +$fiber_remove->resume(); diff --git a/tests/integration/external/curl_multi_exec/test_http_fibers.php b/tests/integration/external/curl_multi_exec/test_http_fibers.php new file mode 100644 index 000000000..bd92bb8ba --- /dev/null +++ b/tests/integration/external/curl_multi_exec/test_http_fibers.php @@ -0,0 +1,250 @@ += 8.1 required\n"); +} +if (!extension_loaded("curl")) { + die("skip: curl extension required"); +} +*/ + +/*INI +newrelic.fibers.disabled = false +*/ + +/*EXPECT +ok - simple hostname +Starting Func 'a' +ok - strip query string +ok - strip fragment +Ending Func 'a' +ok - strip credentials +*/ + +/*EXPECT_SPAN_EVENTS_LIKE +[ + [ + { + "traceId": "??", + "duration": "??", + "transactionId": "??", + "name": "OtherTransaction\/php__FILE__", + "guid": "ENV[GUID_ROOT]", + "type": "Span", + "category": "generic", + "priority": "??", + "sampled": true, + "nr.entryPoint": true, + "timestamp": "??", + "transaction.name": "OtherTransaction\/php__FILE__" + }, + {}, + {} + ], + [ + { + "category": "generic", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "Custom\/test_curl", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "ENV[GUID_ROOT]" + }, + {}, + {} + ], + [ + { + "category": "generic", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "curl_multi_exec", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "ENV[GUID_TEST_CURL]" + }, + {}, + {} + ], + [ + { + "category": "http", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "External\/127.0.0.1\/all", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "??", + "span.kind": "client", + "component": "curl" + }, + {}, + { + "http.method": "GET", + "http.url": "http:\/\/ENV[EXTERNAL_HOST]\/", + "http.statusCode": 200 + } + ], + [ + { + "category": "generic", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "Custom\/a", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "ENV[GUID_ROOT]" + }, + {}, + {} + ] +] +*/ + +/*EXPECT_METRICS +[ + "?? agent run id", + "?? start time", + "?? stop time", + [ + [{"name":"DurationByCaller/Unknown/Unknown/Unknown/Unknown/all"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"DurationByCaller/Unknown/Unknown/Unknown/Unknown/allOther"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Supportability/TraceContext/Create/Success"}, [4, "??", "??", "??", "??", "??"]], + [{"name":"Supportability/DistributedTrace/CreatePayload/Success"},[4, "??", "??", "??", "??", "??"]], + [{"name":"External/all"}, [4, "??", "??", "??", "??", "??"]], + [{"name":"External/allOther"}, [4, "??", "??", "??", "??", "??"]], + [{"name":"External/127.0.0.1/all"}, [4, "??", "??", "??", "??", "??"]], + [{"name":"External/127.0.0.1/all", + "scope":"OtherTransaction/php__FILE__"}, [4, "??", "??", "??", "??", "??"]], + [{"name":"OtherTransaction/all"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"OtherTransaction/php__FILE__"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"OtherTransactionTotalTime"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"OtherTransactionTotalTime/php__FILE__"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Supportability/Logging/Forwarding/PHP/enabled"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Supportability/Logging/Metrics/PHP/enabled"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Supportability/Logging/LocalDecorating/PHP/disabled"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Supportability/Logging/Labels/PHP/disabled"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Supportability/api/get_linking_metadata"}, ["??", "??", "??", "??", "??", "??"]], + [{"name":"Supportability/PHP/Fiber/used"}, ["??", "??", "??", "??", "??", "??"]] + ] +] +*/ + +require_once(realpath(dirname(__FILE__)) . '/../../../include/helpers.php'); +require_once(realpath(dirname(__FILE__)) . '/../../../include/tap.php'); +require_once(realpath(dirname(__FILE__)) . '/../../../include/config.php'); + +function test_multi_url($url, $msg) +{ + $cm = curl_multi_init(); + + $ch = curl_init(); + curl_setopt($ch, CURLOPT_NOBODY, true); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_URL, $url); + curl_multi_add_handle($cm, $ch); + + $active = 0; + + do { + curl_multi_exec($cm, $active); + Fiber::suspend($active); + } while ($active > 0); + + /* No errors */ + $info = curl_multi_info_read($cm); + tap_ok($msg, $info["result"] == 0); + + curl_multi_close($cm); +} + +function test_curl() +{ + global $EXTERNAL_HOST; + + env_var_for_expects("GUID_TEST_CURL", newrelic_get_linking_metadata()['span.id'] ?? ''); + + $fiber_one = new Fiber('test_multi_url'); + $fiber_two = new Fiber('test_multi_url'); + $fiber_three = new Fiber('test_multi_url'); + $fiber_four = new Fiber('test_multi_url'); + + $result = $fiber_one->start('http://' . $EXTERNAL_HOST . '', 'simple hostname'); + do { + $result = $fiber_one->resume(); + } while ($result > 0); + $fiber_one->resume(); + + Fiber::suspend(); + + $result = $fiber_two->start('http://' . $EXTERNAL_HOST . '?a=1&b=2', 'strip query string'); + do { + $result = $fiber_two->resume(); + } while ($result > 0); + $fiber_two->resume(); + + $result = $fiber_three->start('http://' . $EXTERNAL_HOST . '/#fragment', 'strip fragment'); + + do { + $result = $fiber_three->resume(); + } while ($result > 0); + $fiber_three->resume(); + + Fiber::suspend(); + + $result = $fiber_four->start('http://user:pass@' . $EXTERNAL_HOST . '', 'strip credentials'); + do { + $result = $fiber_four->resume(); + } while ($result > 0); + $fiber_four->resume(); +} + +function a() +{ + echo "Starting Func 'a'\n"; + env_var_for_expects("GUID_A", newrelic_get_linking_metadata()['span.id'] ?? ''); + time_nanosleep(0, 100000000); + Fiber::suspend(); + echo "Ending Func 'a'\n"; +} + +env_var_for_expects("GUID_ROOT", newrelic_get_linking_metadata()['span.id'] ?? ''); + +$fiber_a = new Fiber('a'); +$fiber_curl_multi = new Fiber('test_curl'); + +$fiber_curl_multi->start(); +$fiber_a->start(); +$fiber_curl_multi->resume(); +$fiber_a->resume(); +$fiber_curl_multi->resume(); diff --git a/tests/integration/external/curl_multi_exec/test_multiple_fibers_handle_coordination.php b/tests/integration/external/curl_multi_exec/test_multiple_fibers_handle_coordination.php new file mode 100644 index 000000000..c4a2628b7 --- /dev/null +++ b/tests/integration/external/curl_multi_exec/test_multiple_fibers_handle_coordination.php @@ -0,0 +1,794 @@ += 8.1 required\n"); +} +if (!extension_loaded("curl")) { + die("skip: curl extension required\n"); +} +?> +*/ + +/*INI +newrelic.distributed_tracing_enabled = true +newrelic.cross_application_tracer.enabled = false +newrelic.fibers.disabled = false +*/ + +/*EXPECT +Fiber 1 adding curl handles +Fiber 2 adding curl handles +Fiber 3 adding curl handles +Fiber 4 adding curl handles +Fiber 5 adding curl handles +Fiber 6 executing curl_multi_exec for handle set 1 +Fiber 7 executing curl_multi_exec for handle set 2 +Fiber 8 executing curl_multi_exec for handle set 3 +Fiber 9 executing curl_multi_exec for handle set 4 +Fiber 10 executing curl_multi_exec for handle set 5 +Handle set 1 result: +Handle set 2 result: +Handle set 3 result: +Handle set 4 result: +Handle set 5 result: +All curl operations completed successfully +Fiber had 3 matching children +Fiber had 3 matching children +Fiber had 3 matching children +Fiber had 3 matching children +Fiber had 3 matching children +*/ + +/*EXPECT_METRICS_EXIST +External/url1_1_6/all, 1 +External/url1_2_6/all, 1 +External/url1_3_6/all, 1 +External/url2_1_7/all, 1 +External/url2_2_7/all, 1 +External/url2_3_7/all, 1 +External/url3_1_8/all, 1 +External/url3_2_8/all, 1 +External/url3_3_8/all, 1 +External/url4_1_9/all, 1 +External/url4_2_9/all, 1 +External/url4_3_9/all, 1 +External/url5_1_10/all, 1 +External/url5_2_10/all, 1 +External/url5_3_10/all, 1 +*/ + +/*EXPECT_ERROR_EVENTS +null +*/ + +/*EXPECT_SPAN_EVENTS_LIKE +[ + [ + { + "category": "generic", + "type": "Span", + "guid": "ENV[GUID_FIBER_EXEC_6]", + "traceId": "??", + "transactionId": "??", + "name": "Custom\/executing_fiber", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "??" + }, + { + "fiber_id": 6 + }, + {} + ], + [ + { + "category": "generic", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "curl_multi_exec", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "ENV[GUID_FIBER_EXEC_6]" + }, + {}, + {} + ], + [ + { + "category": "http", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "External\/url1_1_6\/all", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "??", + "span.kind": "client", + "component": "curl" + }, + {}, + { + "http.method": "GET", + "http.url": "http:\/\/url1_1_6\/", + "http.statusCode": 0 + } + ], + [ + { + "category": "http", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "External\/url1_2_6\/all", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "??", + "span.kind": "client", + "component": "curl" + }, + {}, + { + "http.method": "GET", + "http.url": "http:\/\/url1_2_6\/", + "http.statusCode": 0 + } + ], + [ + { + "category": "http", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "External\/url1_3_6\/all", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "??", + "span.kind": "client", + "component": "curl" + }, + {}, + { + "http.method": "GET", + "http.url": "http:\/\/url1_3_6\/", + "http.statusCode": 0 + } + ], + [ + { + "category": "generic", + "type": "Span", + "guid": "ENV[GUID_FIBER_EXEC_7]", + "traceId": "??", + "transactionId": "??", + "name": "Custom\/executing_fiber", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "??" + }, + { + "fiber_id": 7 + }, + {} + ], + [ + { + "category": "generic", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "curl_multi_exec", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "ENV[GUID_FIBER_EXEC_7]" + }, + {}, + {} + ], + [ + { + "category": "http", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "External\/url2_1_7\/all", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "??", + "span.kind": "client", + "component": "curl" + }, + {}, + { + "http.method": "GET", + "http.url": "http:\/\/url2_1_7\/", + "http.statusCode": 0 + } + ], + [ + { + "category": "http", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "External\/url2_2_7\/all", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "??", + "span.kind": "client", + "component": "curl" + }, + {}, + { + "http.method": "GET", + "http.url": "http:\/\/url2_2_7\/", + "http.statusCode": 0 + } + ], + [ + { + "category": "http", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "External\/url2_3_7\/all", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "??", + "span.kind": "client", + "component": "curl" + }, + {}, + { + "http.method": "GET", + "http.url": "http:\/\/url2_3_7\/", + "http.statusCode": 0 + } + ], + [ + { + "category": "generic", + "type": "Span", + "guid": "ENV[GUID_FIBER_EXEC_8]", + "traceId": "??", + "transactionId": "??", + "name": "Custom\/executing_fiber", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "??" + }, + { + "fiber_id": 8 + }, + {} + ], + [ + { + "category": "generic", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "curl_multi_exec", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "ENV[GUID_FIBER_EXEC_8]" + }, + {}, + {} + ], + [ + { + "category": "http", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "External\/url3_1_8\/all", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "??", + "span.kind": "client", + "component": "curl" + }, + {}, + { + "http.method": "GET", + "http.url": "http:\/\/url3_1_8\/", + "http.statusCode": 0 + } + ], + [ + { + "category": "http", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "External\/url3_2_8\/all", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "??", + "span.kind": "client", + "component": "curl" + }, + {}, + { + "http.method": "GET", + "http.url": "http:\/\/url3_2_8\/", + "http.statusCode": 0 + } + ], + [ + { + "category": "http", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "External\/url3_3_8\/all", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "??", + "span.kind": "client", + "component": "curl" + }, + {}, + { + "http.method": "GET", + "http.url": "http:\/\/url3_3_8\/", + "http.statusCode": 0 + } + ], + [ + { + "category": "generic", + "type": "Span", + "guid": "ENV[GUID_FIBER_EXEC_9]", + "traceId": "??", + "transactionId": "??", + "name": "Custom\/executing_fiber", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "??" + }, + { + "fiber_id": 9 + }, + {} + ], + [ + { + "category": "generic", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "curl_multi_exec", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "ENV[GUID_FIBER_EXEC_9]" + }, + {}, + {} + ], + [ + { + "category": "http", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "External\/url4_1_9\/all", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "??", + "span.kind": "client", + "component": "curl" + }, + {}, + { + "http.method": "GET", + "http.url": "http:\/\/url4_1_9\/", + "http.statusCode": 0 + } + ], + [ + { + "category": "http", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "External\/url4_2_9\/all", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "??", + "span.kind": "client", + "component": "curl" + }, + {}, + { + "http.method": "GET", + "http.url": "http:\/\/url4_2_9\/", + "http.statusCode": 0 + } + ], + [ + { + "category": "http", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "External\/url4_3_9\/all", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "??", + "span.kind": "client", + "component": "curl" + }, + {}, + { + "http.method": "GET", + "http.url": "http:\/\/url4_3_9\/", + "http.statusCode": 0 + } + ], + [ + { + "category": "generic", + "type": "Span", + "guid": "ENV[GUID_FIBER_EXEC_10]", + "traceId": "??", + "transactionId": "??", + "name": "Custom\/executing_fiber", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "??" + }, + { + "fiber_id": 10 + }, + {} + ], + [ + { + "category": "generic", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "curl_multi_exec", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "ENV[GUID_FIBER_EXEC_10]" + }, + {}, + {} + ], + [ + { + "category": "http", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "External\/url5_1_10\/all", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "??", + "span.kind": "client", + "component": "curl" + }, + {}, + { + "http.method": "GET", + "http.url": "http:\/\/url5_1_10\/", + "http.statusCode": 0 + } + ], + [ + { + "category": "http", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "External\/url5_2_10\/all", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "??", + "span.kind": "client", + "component": "curl" + }, + {}, + { + "http.method": "GET", + "http.url": "http:\/\/url5_2_10\/", + "http.statusCode": 0 + } + ], + [ + { + "category": "http", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "External\/url5_3_10\/all", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "??", + "span.kind": "client", + "component": "curl" + }, + {}, + { + "http.method": "GET", + "http.url": "http:\/\/url5_3_10\/", + "http.statusCode": 0 + } + ] +] +*/ + +require_once(realpath(dirname(__FILE__)) . '/../../../include/config.php'); +require_once(realpath(dirname(__FILE__)) . '/../../../include/helpers.php'); +require_once(realpath(dirname(__FILE__)) . '/../../../include/integration.php'); +require_once(realpath(dirname(__FILE__)) . '/../../../include/tap.php'); + + +use NewRelic\Integration\Transaction; + +// Global storage for multi handles and results +$multi_handles = []; +$curl_handles = []; +$results = []; + +function handle_adding_fiber($fiber_id) { + global $multi_handles, $curl_handles; + + echo "Fiber $fiber_id adding curl handles\n"; + + // Create multi handle + $mh = curl_multi_init(); + $handles = []; + + // Create 3 curl handles + for ($i = 1; $i <= 3; $i++) { + $url = "url" . $fiber_id . "_" . $i; + $ch = curl_init($url); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_multi_add_handle($mh, $ch); + $handles[] = $ch; + } + + // Store for the executing fiber to use + $multi_handles[$fiber_id] = $mh; + $curl_handles[$fiber_id] = $handles; + + Fiber::suspend(); + + return $mh; +} + +function executing_fiber($fiber_id, $handle_set_id) { + global $multi_handles, $curl_handles, $results; + + echo "Fiber $fiber_id executing curl_multi_exec for handle set $handle_set_id\n"; + env_var_for_expects("GUID_FIBER_EXEC_" . $fiber_id, newrelic_get_linking_metadata()['span.id'] ?? ''); + newrelic_add_custom_span_parameter("fiber_id", $fiber_id); + + + // Wait for the handle set to be available + while (!isset($multi_handles[$handle_set_id])) { + Fiber::suspend(); + } + + $mh = $multi_handles[$handle_set_id]; + $handles = $curl_handles[$handle_set_id]; + + // Update URLs for all handles to include the executing fiber ID + foreach ($handles as $i => $ch) { + $current_url = curl_getinfo($ch, CURLINFO_EFFECTIVE_URL); + curl_setopt($ch, CURLOPT_URL, $current_url . "_" . $fiber_id); + } + + Fiber::suspend(); + + // Execute the curl_multi_exec + $active = 0; + do { + curl_multi_exec($mh, $active); + Fiber::suspend($active); + } while ($active > 0); + + // Get the results - just take the first handle's result for simplicity + try { + $content = curl_multi_getcontent($handles[0]); + $results[$handle_set_id] = trim($content); + } catch (Exception $e) { + $results[$handle_set_id] = "Error: " . $e->getMessage(); + } + + // Clean up all handles + foreach ($handles as $ch) { + curl_multi_remove_handle($mh, $ch); + } + curl_multi_close($mh); + + Fiber::suspend(); +} + +function test_multiple_fibers_coordination() { + global $results; + + // Create 5 handle-adding fibers + $adding_fibers = []; + for ($i = 1; $i <= 5; $i++) { + $adding_fibers[$i] = new Fiber(function() use ($i) { + return handle_adding_fiber($i); + }); + } + + // Create 5 executing fibers + $executing_fibers = []; + for ($i = 6; $i <= 10; $i++) { + $handle_set_id = $i - 5; // Maps fiber 6->handle set 1, fiber 7->handle set 2, etc. + $executing_fibers[$i] = new Fiber(function() use ($i, $handle_set_id) { + return executing_fiber($i, $handle_set_id); + }); + } + + // Start all adding fibers + foreach ($adding_fibers as $fiber) { + $fiber->start(); + } + + // Start all executing fibers + foreach ($executing_fibers as $fiber) { + $fiber->start(); + } + + // Resume adding fibers to let them create handles + foreach ($adding_fibers as $fiber) { + if (!$fiber->isTerminated()) { + $fiber->resume(); + } + } + + // Resume executing fibers multiple times to let them process + for ($round = 0; $round < 10; $round++) { + foreach ($executing_fibers as $fiber) { + if (!$fiber->isTerminated()) { + $result = $fiber->resume(); + // Keep resuming while curl is active + while (isset($result) && $result > 0 && !$fiber->isTerminated()) { + $result = $fiber->resume(); + } + } + } + + // Give adding fibers a chance if they're still running + foreach ($adding_fibers as $fiber) { + if (!$fiber->isTerminated()) { + $fiber->resume(); + } + } + } + + // Wait a bit for any remaining async operations to complete + usleep(100000); // 100ms + + // Final cleanup round + foreach ($executing_fibers as $fiber) { + if (!$fiber->isTerminated()) { + $fiber->resume(); + } + } + + // Print results + for ($i = 1; $i <= 5; $i++) { + if (isset($results[$i])) { + echo "Handle set $i result: " . $results[$i] . "\n"; + } else { + echo "Handle set $i result: No result available\n"; + } + } + + echo "All curl operations completed successfully\n"; +} + +$main_fiber = new Fiber('test_multiple_fibers_coordination'); +$main_fiber->start(); + +$txn = new Transaction; +$curl_multi_execs = $txn->getTrace()->findSegmentsByName('curl_multi_exec'); +foreach ($curl_multi_execs as $segment) { + $matching_children = true; + $children = $segment->children; + $child_index = 0; + $prev_child_name = ''; + foreach ($children as $child) { + if (!empty($prev_child_name) && strncmp($prev_child_name, $child->name, 5) != 0) { + $matching_children = false; + } + $child_index++; + } + echo "Fiber had $child_index " . ($matching_children ? "matching" : "NOT matching") . " children\n"; +} diff --git a/tests/integration/external/file_get_contents/test_basic_fibers.php b/tests/integration/external/file_get_contents/test_basic_fibers.php new file mode 100644 index 000000000..987782316 --- /dev/null +++ b/tests/integration/external/file_get_contents/test_basic_fibers.php @@ -0,0 +1,187 @@ += 8.1 required\n"); +} +*/ + +/*INI +newrelic.distributed_tracing_enabled=1 +newrelic.cross_application_tracer.enabled = false +newrelic.fibers.disabled = false +*/ + +/*EXPECT_REGEX +Hello world!Starting Func 'a' +Hello world!Hello world!Ending Func 'a' +Hello world!Hello +*/ + +/*EXPECT_SPAN_EVENTS_LIKE +[ + [ + { + "traceId": "??", + "duration": "??", + "transactionId": "??", + "name": "OtherTransaction\/php__FILE__", + "guid": "ENV[GUID_ROOT]", + "type": "Span", + "category": "generic", + "priority": "??", + "sampled": true, + "nr.entryPoint": true, + "timestamp": "??", + "transaction.name": "OtherTransaction\/php__FILE__" + }, + {}, + {} + ], + [ + { + "category": "generic", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "Custom\/test_file_get_contents", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "ENV[GUID_ROOT]" + }, + {}, + {} + ], + [ + { + "category": "http", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "External\/127.0.0.1\/all", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "ENV[GUID_TEST_FGC]", + "span.kind": "client", + "component": "file_get_contents" + }, + {}, + { + "http.method": "GET", + "http.url": "http:\/\/ENV[EXTERNAL_HOST]", + "http.statusCode": 0 + } + ], + [ + { + "category": "generic", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "Custom\/a", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "ENV[GUID_ROOT]" + }, + {}, + {} + ] +] +*/ + +/*EXPECT_METRICS +[ + "?? agent run id", + "?? start time", + "?? stop time", + [ + [{"name":"DurationByCaller/Unknown/Unknown/Unknown/Unknown/all"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"DurationByCaller/Unknown/Unknown/Unknown/Unknown/allOther"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Supportability/TraceContext/Create/Success"}, [5, "??", "??", "??", "??", "??"]], + [{"name":"Supportability/DistributedTrace/CreatePayload/Success"},[5, "??", "??", "??", "??", "??"]], + [{"name":"External/all"}, [5, "??", "??", "??", "??", "??"]], + [{"name":"External/allOther"}, [5, "??", "??", "??", "??", "??"]], + [{"name":"External/127.0.0.1/all"}, [5, "??", "??", "??", "??", "??"]], + [{"name":"External/127.0.0.1/all", + "scope":"OtherTransaction/php__FILE__"}, [5, "??", "??", "??", "??", "??"]], + [{"name":"OtherTransaction/all"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"OtherTransaction/php__FILE__"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"OtherTransactionTotalTime"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"OtherTransactionTotalTime/php__FILE__"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Supportability/Logging/Forwarding/PHP/enabled"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Supportability/Logging/Metrics/PHP/enabled"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Supportability/Logging/LocalDecorating/PHP/disabled"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Supportability/Logging/Labels/PHP/disabled"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Supportability/api/get_linking_metadata"}, ["??", "??", "??", "??", "??", "??"]], + [{"name":"Supportability/PHP/Fiber/used"}, ["??", "??", "??", "??", "??", "??"]] + ] +] +*/ + +require_once(realpath(dirname(__FILE__)) . '/../../../include/helpers.php'); +require_once(realpath(dirname(__FILE__)) . '/../../../include/tap.php'); +require_once(realpath(dirname(__FILE__)) . '/../../../include/config.php'); + +function test_file_get_contents() +{ + global $EXTERNAL_HOST; + + env_var_for_expects("GUID_TEST_FGC", newrelic_get_linking_metadata()['span.id'] ?? ''); + $url = 'http://' . $EXTERNAL_HOST; + + + // only URL + echo file_get_contents ($url); + + // no context + Fiber::suspend(); + echo file_get_contents ($url, false); + + // NULL context + echo file_get_contents ($url, false, NULL); + + // NULL context with offset and maxlen + Fiber::suspend(); + echo file_get_contents ($url, false, NULL, 0, 50000); + + // small maxlen + echo file_get_contents ($url, false, NULL, 0, 5); +} + +function a() +{ + echo "Starting Func 'a'\n"; + env_var_for_expects("GUID_A", newrelic_get_linking_metadata()['span.id'] ?? ''); + time_nanosleep(0, 100000000); + Fiber::suspend(); + echo "Ending Func 'a'\n"; +} + +env_var_for_expects("GUID_ROOT", newrelic_get_linking_metadata()['span.id'] ?? ''); + +$fiber_a = new Fiber('a'); +$fiber_fgc = new Fiber('test_file_get_contents'); + +$fiber_fgc->start(); +$fiber_a->start(); +$fiber_fgc->resume(); +$fiber_a->resume(); +$fiber_fgc->resume(); diff --git a/tests/integration/external/guzzle7/test_bad_response_exception_async_fibers.php b/tests/integration/external/guzzle7/test_bad_response_exception_async_fibers.php new file mode 100644 index 000000000..5f735aa3a --- /dev/null +++ b/tests/integration/external/guzzle7/test_bad_response_exception_async_fibers.php @@ -0,0 +1,106 @@ += 8.1 required\n"); +} +require("skipif.inc"); +?> +*/ + +/*INI +newrelic.distributed_tracing_enabled = true +newrelic.transaction_tracer.threshold = 0 +newrelic.transaction_tracer.detail = 1 +newrelic.code_level_metrics.enabled = 0 +newrelic.fibers.disabled = false +*/ + +/*ENVIRONMENT +TEST_EXTERNAL_HOST=example.com +*/ + +/*EXPECT_METRICS_EXIST +External/ENV[TEST_EXTERNAL_HOST]/all +*/ + +/*EXPECT_SPAN_EVENTS_LIKE +[ + [ + { + "traceId": "??", + "duration": "??", + "transactionId": "??", + "name": "External/ENV[TEST_EXTERNAL_HOST]/all", + "guid": "??", + "type": "Span", + "category": "http", + "priority": "??", + "sampled": true, + "timestamp": "??", + "parentId": "??", + "span.kind": "client", + "component": "Guzzle 6" + }, + {}, + { + "http.url": "http://ENV[TEST_EXTERNAL_HOST]/resource", + "http.method": "GET", + "http.statusCode": 404 + } + ] +] +*/ + +/*EXPECT_ERROR_EVENTS null*/ + +/*EXPECT +all's well that ends well +*/ + +function test_async_bad_response() { + require_once(realpath(dirname(__FILE__)) . '/../../../include/config.php'); + require_once(realpath(dirname(__FILE__)) . '/../../../include/unpack_guzzle.php'); + require_guzzle(7); + + $TEST_EXTERNAL_HOST=getenv('TEST_EXTERNAL_HOST'); + + $request = new \GuzzleHttp\Psr7\Request('GET', "http://$TEST_EXTERNAL_HOST/resource"); + $response = new \GuzzleHttp\Psr7\Response(404, [], "Not found!"); + + $stack = GuzzleHttp\HandlerStack::create( + new GuzzleHttp\Handler\MockHandler([ + new \GuzzleHttp\Exception\BadResponseException( + "ClientException", + $request, + $response + ) + ])); + + $client = new GuzzleHttp\Client([ + 'handler' => $stack, + + ]); + + Fiber::suspend(); + $promise = $client->sendAsync($request); + Fiber::suspend(); + GuzzleHttp\Promise\Utils::settle($promise)->wait(); + + echo "all's well that ends well" . PHP_EOL; +} + +$fiber = new Fiber('test_async_bad_response'); +$fiber->start(); +$fiber->resume(); +$fiber->resume(); diff --git a/tests/integration/external/guzzle7/test_bad_response_exception_sync_fibers.php b/tests/integration/external/guzzle7/test_bad_response_exception_sync_fibers.php new file mode 100644 index 000000000..ee231eef0 --- /dev/null +++ b/tests/integration/external/guzzle7/test_bad_response_exception_sync_fibers.php @@ -0,0 +1,111 @@ += 8.1 required\n"); +} +require("skipif.inc"); +?> +*/ + +/*INI +newrelic.distributed_tracing_enabled = true +newrelic.transaction_tracer.threshold = 0 +newrelic.transaction_tracer.detail = 1 +newrelic.code_level_metrics.enabled = 0 +newrelic.fibers.disabled = false +*/ + +/*ENVIRONMENT +TEST_EXTERNAL_HOST=example.com +*/ + +/*EXPECT_METRICS_EXIST +External/ENV[TEST_EXTERNAL_HOST]/all +*/ + +/*EXPECT_SPAN_EVENTS_LIKE +[ + [ + { + "traceId": "??", + "duration": "??", + "transactionId": "??", + "name": "External/ENV[TEST_EXTERNAL_HOST]/all", + "guid": "??", + "type": "Span", + "category": "http", + "priority": "??", + "sampled": true, + "timestamp": "??", + "parentId": "??", + "span.kind": "client", + "component": "Guzzle 6" + }, + {}, + { + "http.url": "http://ENV[TEST_EXTERNAL_HOST]/resource", + "http.method": "GET", + "http.statusCode": 404 + } + ] +] +*/ + +/*EXPECT_ERROR_EVENTS null*/ + +/*EXPECT +all's well that ends well +*/ + +function test_sync_bad_response() { + require_once(realpath(dirname(__FILE__)) . '/../../../include/config.php'); + require_once(realpath(dirname(__FILE__)) . '/../../../include/unpack_guzzle.php'); + require_guzzle(7); + + $TEST_EXTERNAL_HOST=getenv('TEST_EXTERNAL_HOST'); + + $request = new \GuzzleHttp\Psr7\Request('GET', "http://$TEST_EXTERNAL_HOST/resource"); + $response = new \GuzzleHttp\Psr7\Response(404, [], "Not found!"); + + $stack = GuzzleHttp\HandlerStack::create( + new GuzzleHttp\Handler\MockHandler([ + new \GuzzleHttp\Exception\BadResponseException( + "ClientException", + $request, + $response + ) + ])); + + $client = new GuzzleHttp\Client([ + 'handler' => $stack, + + ]); + + Fiber::suspend(); + + try { + $response = $client->send($request); + } catch (\GuzzleHttp\Exception\BadResponseException $e) { + // Expected exception + } + + Fiber::suspend(); + + echo "all's well that ends well" . PHP_EOL; +} + +$fiber = new Fiber('test_sync_bad_response'); +$fiber->start(); +$fiber->resume(); +$fiber->resume(); diff --git a/tests/integration/external/guzzle7/test_dt_async_fibers.php b/tests/integration/external/guzzle7/test_dt_async_fibers.php new file mode 100644 index 000000000..443b0fa3d --- /dev/null +++ b/tests/integration/external/guzzle7/test_dt_async_fibers.php @@ -0,0 +1,242 @@ += 8.1 required\n"); +} +?> +*/ + +/*INI +newrelic.distributed_tracing_enabled = true +newrelic.fibers.disabled = false +*/ + +/*EXPECT +traceparent=found tracestate=found newrelic=found X-NewRelic-ID=missing X-NewRelic-Transaction=missing tracing endpoint reached +traceparent=found tracestate=found newrelic=found X-NewRelic-ID=missing X-NewRelic-Transaction=missing tracing endpoint reached +traceparent=found tracestate=found newrelic=found X-NewRelic-ID=missing X-NewRelic-Transaction=missing tracing endpoint reached +*/ + +/*EXPECT_METRICS +[ + "?? agent run id", + "?? start time", + "?? stop time", + [ + [{"name":"DurationByCaller/Unknown/Unknown/Unknown/Unknown/all"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"DurationByCaller/Unknown/Unknown/Unknown/Unknown/allOther"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"External/all"}, [3, "??", "??", "??", "??", "??"]], + [{"name":"External/allOther"}, [3, "??", "??", "??", "??", "??"]], + [{"name":"External/127.0.0.1/all"}, [3, "??", "??", "??", "??", "??"]], + [{"name":"External/127.0.0.1/all", + "scope":"OtherTransaction/php__FILE__"}, [3, "??", "??", "??", "??", "??"]], + [{"name":"OtherTransaction/all"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"OtherTransaction/php__FILE__"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"OtherTransactionTotalTime"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"OtherTransactionTotalTime/php__FILE__"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Supportability/DistributedTrace/CreatePayload/Success"}, [3, "??", "??", "??", "??", "??"]], + [{"name":"Supportability/TraceContext/Create/Success"}, [3, "??", "??", "??", "??", "??"]], + [{"name":"Supportability/PHP/package/guzzlehttp/guzzle/7/detected"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Supportability/library/Guzzle 6/detected"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Supportability/Unsupported/curl_setopt/CURLOPT_HEADERFUNCTION/closure"}, ["??", "??", "??", "??", "??", "??"]], + [{"name":"Supportability/Logging/Forwarding/PHP/enabled"}, [1, 0, 0, 0, 0, 0]], + [{"name":"Supportability/Logging/Metrics/PHP/enabled"}, [1, 0, 0, 0, 0, 0]], + [{"name":"Supportability/Logging/LocalDecorating/PHP/disabled"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Supportability/Logging/Labels/PHP/disabled"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Supportability/library/Autoloader/detected"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Supportability/library/Composer/detected"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Supportability/PHP/Fiber/used"}, ["??", "??", "??", "??", "??", "??"]] + ] +] +*/ + +/*EXPECT_SPAN_EVENTS_LIKE +[ + [ + { + "category": "http", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "??", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "??", + "span.kind": "client", + "component": "Guzzle 6" + }, + {}, + { + "http.method": "GET", + "http.url": "??", + "http.statusCode": 200 + } + ], + [ + { + "category": "http", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "??", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "??", + "span.kind": "client", + "component": "Guzzle 6" + }, + {}, + { + "http.method": "GET", + "http.url": "??", + "http.statusCode": 200 + } + ], + [ + { + "category": "http", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "??", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "??", + "span.kind": "client", + "component": "Guzzle 6" + }, + {}, + { + "http.method": "GET", + "http.url": "??", + "http.statusCode": 200 + } + ] +] +*/ + +/*EXPECT_TRACED_ERRORS null */ + +require_once(realpath(dirname(__FILE__)) . '/../../../include/config.php'); +require_once(realpath(dirname(__FILE__)) . '/../../../include/unpack_guzzle.php'); +require_guzzle(7); + +use GuzzleHttp\Client; +use GuzzleHttp\Promise; +use GuzzleHttp\Promise\Utils; +use GuzzleHttp\Exception\RequestException; +use Psr\Http\Message\ResponseInterface; + +// Global storage for promises and responses +$promises = []; +$responses = []; + +function async_request_fiber($fiber_id, $client, $url) { + global $promises, $responses; + + // Create async request + $promise = $client->getAsync($url); + + // Add promise callbacks (NO fiber suspension in callbacks) + $promise->then( + function (ResponseInterface $r) use ($fiber_id) { + global $responses; + $responses[$fiber_id] = $r->getBody(); + return $r; + }, + function (RequestException $e) use ($fiber_id) { + global $responses; + $responses[$fiber_id] = $e->getMessage(); + throw $e; + } + ); + + // Store promise for later resolution + $promises[$fiber_id] = $promise; + + // Suspend the fiber after creating the promise + // This allows other fibers to run while this one waits + Fiber::suspend(); + + return $promise; +} + +function test_concurrent_fibers() +{ + global $EXTERNAL_HOST, $promises, $responses; + + $client = new Client(['base_uri' => 'http://' . $EXTERNAL_HOST]); + $url = "http://" . make_tracing_url(realpath(dirname(__FILE__)) . '/../../../include/tracing_endpoint.php'); + + // Create fibers for async requests + $request_fibers = []; + for ($i = 0; $i < 3; $i++) { + $request_fibers[$i] = new Fiber(function() use ($i, $client, $url) { + return async_request_fiber($i, $client, $url); + }); + } + + // Start all request fibers + foreach ($request_fibers as $fiber) { + $fiber->start(); + } + + // Resume fibers to create promises + foreach ($request_fibers as $fiber) { + if (!$fiber->isTerminated()) { + $fiber->resume(); + } + } + + // Resume fibers to let them suspend after creating promises + foreach ($request_fibers as $fiber) { + if (!$fiber->isTerminated()) { + $fiber->resume(); + } + } + + // Wait for promises to settle using Guzzle's Utils::settle() + // This follows the original test pattern from the original test + if (!empty($promises)) { + $settled = Promise\Utils::settle($promises)->wait(); + + // Process settled promises and output results + foreach ($settled as $index => $result) { + if ($result['state'] === 'fulfilled') { + echo $responses[$index]; + } else { + echo "Request $index failed\n"; + } + } + } +} + +function test_main_fiber() { + // Run the concurrent fiber test + test_concurrent_fibers(); +} + +$main_fiber = new Fiber('test_main_fiber'); +$main_fiber->start(); diff --git a/tests/integration/external/guzzle7/test_dt_newrelic_header_disabled_fibers.php b/tests/integration/external/guzzle7/test_dt_newrelic_header_disabled_fibers.php new file mode 100644 index 000000000..2ed21e229 --- /dev/null +++ b/tests/integration/external/guzzle7/test_dt_newrelic_header_disabled_fibers.php @@ -0,0 +1,73 @@ += 8.1 required\n"); +} +?> +*/ + +/*INI +newrelic.distributed_tracing_enabled = true +newrelic.cross_application_tracer.enabled = false +newrelic.distributed_tracing_exclude_newrelic_header = true +newrelic.fibers.disabled = false +*/ + +/*EXPECT +traceparent=found tracestate=found X-NewRelic-ID=missing X-NewRelic-Transaction=missing tracing endpoint reached +traceparent=found tracestate=found X-NewRelic-ID=missing X-NewRelic-Transaction=missing tracing endpoint reached +traceparent=found tracestate=found X-NewRelic-ID=missing X-NewRelic-Transaction=missing Customer-Header=found tracing endpoint reached +*/ + +/*EXPECT_RESPONSE_HEADERS +*/ + +/*EXPECT_METRICS_EXIST +External/127.0.0.1/all, 3 +Supportability/TraceContext/Create/Success, 3 +*/ + +require_once(realpath(dirname(__FILE__)) . '/../../../include/config.php'); +require_once(realpath(dirname(__FILE__)) . '/../../../include/unpack_guzzle.php'); +require_guzzle(7); + +use GuzzleHttp\Client; + +function test_guzzle_dt_newrelic_header_disabled() { + /* Create URL. */ + $url = "http://" . make_tracing_url(realpath(dirname(__FILE__)) . '/../../../include/tracing_endpoint.php'); + + $client = new Client(); + + $response = $client->get($url); + echo $response->getBody(); + Fiber::suspend(); + + $response = $client->get($url, [ + 'headers' => [ + 'zip' => 'zap']]); + echo $response->getBody(); + Fiber::suspend(); + + $response = $client->get($url, [ + 'headers' => [ + 'zip' => 'zap', + CUSTOMER_HEADER => 'zap']]); + echo $response->getBody(); +} + +$fiber = new Fiber('test_guzzle_dt_newrelic_header_disabled'); +$fiber->start(); +$fiber->resume(); +$fiber->resume(); diff --git a/tests/integration/external/guzzle7/test_multiple_fibers_promise_coordination.php b/tests/integration/external/guzzle7/test_multiple_fibers_promise_coordination.php new file mode 100644 index 000000000..25fb9d23b --- /dev/null +++ b/tests/integration/external/guzzle7/test_multiple_fibers_promise_coordination.php @@ -0,0 +1,605 @@ += 8.1 required\n"); +} +require("skipif.inc"); +?> +*/ + +/*INI +newrelic.distributed_tracing_enabled = true +newrelic.cross_application_tracer.enabled = false +newrelic.fibers.disabled = false +*/ + +/*EXPECT +Fiber 1 initiating promises +Fiber 2 initiating promises +Fiber 3 initiating promises +Fiber 4 initiating promises +Fiber 5 initiating promises +Fiber 6 fulfilling promises 1 +Fiber 7 fulfilling promises 2 +Fiber 8 fulfilling promises 3 +Fiber 9 fulfilling promises 4 +Fiber 10 fulfilling promises 5 +All promises completed successfully +ok - External segment has correct Guzzle context. +ok - External segment has correct Guzzle context. +ok - External segment has correct Guzzle context. +Fiber had 3 matching children +ok - External segment has correct Guzzle context. +ok - External segment has correct Guzzle context. +ok - External segment has correct Guzzle context. +Fiber had 3 matching children +ok - External segment has correct Guzzle context. +ok - External segment has correct Guzzle context. +ok - External segment has correct Guzzle context. +Fiber had 3 matching children +ok - External segment has correct Guzzle context. +ok - External segment has correct Guzzle context. +ok - External segment has correct Guzzle context. +Fiber had 3 matching children +ok - External segment has correct Guzzle context. +ok - External segment has correct Guzzle context. +ok - External segment has correct Guzzle context. +Fiber had 3 matching children +*/ + +/*EXPECT_METRICS_EXIST +External/url5_1/all +External/url5_2/all +External/url5_3/all +External/url4_1/all +External/url4_2/all +External/url4_3/all +External/url3_1/all +External/url3_2/all +External/url3_3/all +External/url2_1/all +External/url2_2/all +External/url2_3/all +External/url1_1/all +External/url1_2/all +External/url1_3/all +Supportability/TraceContext/Create/Success, 15 +Supportability/DistributedTrace/CreatePayload/Success, 15 +*/ + +/*EXPECT_SPAN_EVENTS_LIKE +[ + [ + { + "category": "http", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "External\/url1_1\/all", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "??", + "span.kind": "client", + "component": "Guzzle 6" + }, + {}, + { + "http.method": "GET", + "http.url": "??", + "http.statusCode": 0 + } + ], + [ + { + "category": "http", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "External\/url1_2\/all", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "??", + "span.kind": "client", + "component": "Guzzle 6" + }, + {}, + { + "http.method": "GET", + "http.url": "??", + "http.statusCode": 0 + } + ], + [ + { + "category": "http", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "External\/url1_3\/all", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "??", + "span.kind": "client", + "component": "Guzzle 6" + }, + {}, + { + "http.method": "GET", + "http.url": "??", + "http.statusCode": 0 + } + ], + [ + { + "category": "http", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "External\/url2_1\/all", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "??", + "span.kind": "client", + "component": "Guzzle 6" + }, + {}, + { + "http.method": "GET", + "http.url": "??", + "http.statusCode": 0 + } + ], + [ + { + "category": "http", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "External\/url2_2\/all", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "??", + "span.kind": "client", + "component": "Guzzle 6" + }, + {}, + { + "http.method": "GET", + "http.url": "??", + "http.statusCode": 0 + } + ], + [ + { + "category": "http", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "External\/url2_3\/all", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "??", + "span.kind": "client", + "component": "Guzzle 6" + }, + {}, + { + "http.method": "GET", + "http.url": "??", + "http.statusCode": 0 + } + ], + [ + { + "category": "http", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "External\/url3_1\/all", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "??", + "span.kind": "client", + "component": "Guzzle 6" + }, + {}, + { + "http.method": "GET", + "http.url": "??", + "http.statusCode": 0 + } + ], + [ + { + "category": "http", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "External\/url3_2\/all", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "??", + "span.kind": "client", + "component": "Guzzle 6" + }, + {}, + { + "http.method": "GET", + "http.url": "??", + "http.statusCode": 0 + } + ], + [ + { + "category": "http", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "External\/url3_3\/all", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "??", + "span.kind": "client", + "component": "Guzzle 6" + }, + {}, + { + "http.method": "GET", + "http.url": "??", + "http.statusCode": 0 + } + ], + [ + { + "category": "http", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "External\/url4_1\/all", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "??", + "span.kind": "client", + "component": "Guzzle 6" + }, + {}, + { + "http.method": "GET", + "http.url": "??", + "http.statusCode": 0 + } + ], + [ + { + "category": "http", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "External\/url4_2\/all", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "??", + "span.kind": "client", + "component": "Guzzle 6" + }, + {}, + { + "http.method": "GET", + "http.url": "??", + "http.statusCode": 0 + } + ], + [ + { + "category": "http", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "External\/url4_3\/all", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "??", + "span.kind": "client", + "component": "Guzzle 6" + }, + {}, + { + "http.method": "GET", + "http.url": "??", + "http.statusCode": 0 + } + ], + [ + { + "category": "http", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "External\/url5_1\/all", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "??", + "span.kind": "client", + "component": "Guzzle 6" + }, + {}, + { + "http.method": "GET", + "http.url": "??", + "http.statusCode": 0 + } + ], + [ + { + "category": "http", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "External\/url5_2\/all", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "??", + "span.kind": "client", + "component": "Guzzle 6" + }, + {}, + { + "http.method": "GET", + "http.url": "??", + "http.statusCode": 0 + } + ], + [ + { + "category": "http", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "External\/url5_3\/all", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "??", + "span.kind": "client", + "component": "Guzzle 6" + }, + {}, + { + "http.method": "GET", + "http.url": "??", + "http.statusCode": 0 + } + ] +] +*/ + +require_once(realpath(dirname(__FILE__)) . '/../../../include/config.php'); +require_once(realpath(dirname(__FILE__)) . '/../../../include/helpers.php'); +require_once(realpath(dirname(__FILE__)) . '/../../../include/unpack_guzzle.php'); +require_once(realpath(dirname(__FILE__)) . '/../../../include/integration.php'); +require_once(realpath(dirname(__FILE__)) . '/../../../include/tap.php'); + +require_guzzle(7); + +use NewRelic\Integration\Transaction; + +use GuzzleHttp\Client; +use GuzzleHttp\Promise; + +newrelic_add_custom_tracer('initiating_fiber'); + +// Global storage for promises and results +$promises = []; +$results = []; + +function initiating_fiber($fiber_id) { + global $promises; + + echo "Fiber $fiber_id initiating promises\n"; + + $client = new Client(); + $fiber_promises = []; + + // Create 3 async request promises + for ($i = 1; $i <= 3; $i++) { + $url = "http://url" . $fiber_id . "_" . $i; + $promise = $client->getAsync($url); + $fiber_promises[] = $promise; + } + + $promises[$fiber_id] = $fiber_promises; + + Fiber::suspend(); + + return $fiber_promises; +} + +function fulfilling_fiber($fiber_id, $promise_id) { + global $promises, $results; + + echo "Fiber $fiber_id fulfilling promises $promise_id\n"; + + // Wait for the promises to be available + while (!isset($promises[$promise_id])) { + Fiber::suspend(); + } + + $fiber_promises = $promises[$promise_id]; + + Fiber::suspend(); + + // Fulfill all promises by waiting for their results + // Using Guzzle's recommended promise handling pattern + try { + // Wait for all promises to complete and collect results + $responses = []; + foreach ($fiber_promises as $promise) { + $response = $promise->wait(); + $responses[] = trim($response->getBody()); + } + // Use the first response for display (all should be similar) + $results[$promise_id] = $responses[0]; + } catch (Exception $e) { + $results[$promise_id] = "Error: " . $e->getMessage(); + } + + Fiber::suspend(); +} + +function test_multiple_fibers_coordination() { + global $results, $promises; + + // Create 5 initiating fibers + $initiating_fibers = []; + for ($i = 1; $i <= 5; $i++) { + $initiating_fibers[$i] = new Fiber(function() use ($i) { + return initiating_fiber($i); + }); + } + + // Create 5 fulfilling fibers + $fulfilling_fibers = []; + for ($i = 6; $i <= 10; $i++) { + $promise_id = $i - 5; // Maps fiber 6->promise 1, fiber 7->promise 2, etc. + $fulfilling_fibers[$i] = new Fiber(function() use ($i, $promise_id) { + return fulfilling_fiber($i, $promise_id); + }); + } + + // Start all initiating fibers + foreach ($initiating_fibers as $fiber) { + $fiber->start(); + } + + // Start all fulfilling fibers + foreach ($fulfilling_fibers as $fiber) { + $fiber->start(); + } + + // Resume initiating fibers to let them create promises + foreach ($initiating_fibers as $fiber) { + if (!$fiber->isTerminated()) { + $fiber->resume(); + } + } + + // Resume fulfilling fibers multiple times to let them process + for ($round = 0; $round < 5; $round++) { + foreach ($fulfilling_fibers as $fiber) { + if (!$fiber->isTerminated()) { + $fiber->resume(); + } + } + + // Give initiating fibers a chance if they're still running + foreach ($initiating_fibers as $fiber) { + if (!$fiber->isTerminated()) { + $fiber->resume(); + } + } + } + + // Wait a bit for any remaining async operations to complete + usleep(100000); // 100ms + + // Final cleanup round + foreach ($fulfilling_fibers as $fiber) { + while (!$fiber->isTerminated()) { + $fiber->resume(); + } + } + + echo "All promises completed successfully\n"; +} + +$main_fiber = new Fiber('test_multiple_fibers_coordination'); +$main_fiber->start(); + +$txn = new Transaction; +if (version_compare(phpversion(), '8.4', '<')) { + $middleware_segments = $txn->getTrace()->findSegmentsBySubstring('closure'); +} else { + $middleware_segments = $txn->getTrace()->findSegmentsBySubstring('newrelic\\Guzzle6\\middleware'); + +} +$initiating_fiber_segments = $txn->getTrace()->findSegmentsByName('Custom/initiating_fiber'); + +foreach ($initiating_fiber_segments as $fiber_segment) { + $fiber_segment_context = $fiber_segment->attributes->async_context; + $child_index = 0; + $matching_children = true; + $prev_child_name = ''; + foreach ($middleware_segments as $parent_segment) { + $parent_context = $parent_segment->attributes->async_context; + if ($parent_context === $fiber_segment_context) { + $children = $parent_segment->children; + foreach ($children as $child) { + if (isset($child->attributes->uri)) { + if (!empty($prev_child_name) && strncmp($prev_child_name, $child->name, 5) != 0) { + $matching_children = false; + } + $child_index++; + $prev_child_name = $child->name; + $child_context = $child->attributes->async_context; + tap_equal(true, str_contains($child_context, "Guzzle"), 'External segment has correct Guzzle context.'); + } + } + } + } + echo "Fiber had $child_index " . ($matching_children ? "matching" : "NOT matching") . " children\n"; +} diff --git a/tests/integration/external/guzzle7/test_no_cat_no_dt_fibers.php b/tests/integration/external/guzzle7/test_no_cat_no_dt_fibers.php new file mode 100644 index 000000000..c36d2c7f4 --- /dev/null +++ b/tests/integration/external/guzzle7/test_no_cat_no_dt_fibers.php @@ -0,0 +1,79 @@ += 8.1 required\n"); +} +?> +*/ + +/*INI +newrelic.distributed_tracing_enabled=0 +newrelic.cross_application_tracer.enabled = false +newrelic.fibers.disabled = false +*/ + +/*EXPECT_SPAN_EVENTS +null +*/ + +/*EXPECT_ERROR_EVENTS +null +*/ + +/*EXPECT +X-NewRelic-ID=missing X-NewRelic-Transaction=missing tracing endpoint reached +X-NewRelic-ID=missing X-NewRelic-Transaction=missing tracing endpoint reached +X-NewRelic-ID=missing X-NewRelic-Transaction=missing Customer-Header=found tracing endpoint reached +*/ + +/*EXPECT_RESPONSE_HEADERS +*/ + +/*EXPECT_METRICS_EXIST +External/127.0.0.1/all, 3 +*/ + +require_once(realpath(dirname(__FILE__)) . '/../../../include/config.php'); +require_once(realpath(dirname(__FILE__)) . '/../../../include/unpack_guzzle.php'); +require_guzzle(7); + +use GuzzleHttp\Client; + +function test_guzzle_no_cat_no_dt() { + /* Create URL. */ + $url = "http://" . make_tracing_url(realpath(dirname(__FILE__)) . '/../../../include/tracing_endpoint.php'); + + $client = new Client(); + + $response = $client->get($url); + echo $response->getBody(); + Fiber::suspend(); + + $response = $client->get($url, [ + 'headers' => [ + 'zip' => 'zap']]); + echo $response->getBody(); + Fiber::suspend(); + + $response = $client->get($url, [ + 'headers' => [ + 'zip' => 'zap', + CUSTOMER_HEADER => 'zap']]); + echo $response->getBody(); +} + +$fiber = new Fiber('test_guzzle_no_cat_no_dt'); +$fiber->start(); +$fiber->resume(); +$fiber->resume(); diff --git a/tests/integration/external/guzzle7/test_spans_are_created_correctly_fibers.php b/tests/integration/external/guzzle7/test_spans_are_created_correctly_fibers.php new file mode 100644 index 000000000..1fa31fdbe --- /dev/null +++ b/tests/integration/external/guzzle7/test_spans_are_created_correctly_fibers.php @@ -0,0 +1,132 @@ += 8.1 required\n"); +} +?> +*/ + +/*INI +newrelic.distributed_tracing_enabled = true +newrelic.transaction_tracer.threshold = 0 +newrelic.transaction_tracer.detail = 1 +newrelic.code_level_metrics.enabled = 0 +newrelic.fibers.disabled = false +*/ + +/*EXPECT_SPAN_EVENTS_LIKE +[ + [ + { + "traceId": "??", + "duration": "??", + "transactionId": "??", + "name": "OtherTransaction/php__FILE__", + "guid": "??", + "type": "Span", + "category": "generic", + "priority": "??", + "sampled": true, + "nr.entryPoint": true, + "timestamp": "??", + "transaction.name": "OtherTransaction/php__FILE__" + }, + {}, + {} + ], + [ + { + "traceId": "??", + "duration": "??", + "transactionId": "??", + "name": "External/127.0.0.1/all", + "guid": "??", + "type": "Span", + "category": "http", + "priority": "??", + "sampled": true, + "timestamp": "??", + "parentId": "??", + "span.kind": "client", + "component": "Guzzle 6" + }, + {}, + { + "http.url": "??", + "http.method": "GET", + "http.statusCode": 200 + } + ] +] +*/ + +/*EXPECT_METRICS +[ + "?? agent run id", + "?? timeframe start", + "?? timeframe stop", + [ + [{"name":"External/127.0.0.1/all"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"External/127.0.0.1/all", + "scope":"OtherTransaction/php__FILE__"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"External/all"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"External/allOther"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"OtherTransaction/all"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"OtherTransaction/php__FILE__"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"OtherTransactionTotalTime"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"OtherTransactionTotalTime/php__FILE__"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Supportability/PHP/package/guzzlehttp/guzzle/7/detected"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Supportability/library/Guzzle 6/detected"}, [1, 0, 0, 0, 0, 0]], + [{"name":"Supportability/library/Autoloader/detected"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Supportability/library/Composer/detected"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Supportability/Unsupported/curl_setopt/CURLOPT_HEADERFUNCTION/closure"}, [1, 0, 0, 0, 0, 0]], + [{"name":"DurationByCaller/Unknown/Unknown/Unknown/Unknown/all"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"DurationByCaller/Unknown/Unknown/Unknown/Unknown/allOther"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Supportability/TraceContext/Create/Success"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Supportability/DistributedTrace/CreatePayload/Success"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Supportability/Logging/Forwarding/PHP/enabled"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Supportability/Logging/Metrics/PHP/enabled"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Supportability/Logging/LocalDecorating/PHP/disabled"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Supportability/Logging/Labels/PHP/disabled"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Supportability/PHP/Fiber/used"}, ["??", "??", "??", "??", "??", "??"]] + ] +] +*/ + +/*EXPECT +traceparent=found tracestate=found newrelic=found X-NewRelic-ID=missing X-NewRelic-Transaction=missing tracing endpoint reached +*/ + +require_once(realpath(dirname(__FILE__)) . '/../../../include/config.php'); +require_once(realpath(dirname(__FILE__)) . '/../../../include/unpack_guzzle.php'); +require_once(realpath(dirname(__FILE__)) . '/../../../include/tap.php'); +require_guzzle(7); + +use GuzzleHttp\Client; + +function test_spans() { + /* Create URL. */ + $url = "http://" . make_tracing_url(realpath(dirname(__FILE__)) . '/../../../include/tracing_endpoint.php'); + + $client = new Client(); + + Fiber::suspend(); + + $response = $client->get($url); + echo $response->getBody(); +} + +$fiber = new Fiber('test_spans'); +$fiber->start(); +$fiber->resume(); diff --git a/tests/integration/external/guzzle7/test_timeout_async_fibers.php b/tests/integration/external/guzzle7/test_timeout_async_fibers.php new file mode 100644 index 000000000..cd4c9c208 --- /dev/null +++ b/tests/integration/external/guzzle7/test_timeout_async_fibers.php @@ -0,0 +1,82 @@ += 8.1 required\n"); +} +require("skipif.inc"); +?> +*/ + +/*INI +newrelic.distributed_tracing_enabled = true +newrelic.transaction_tracer.threshold = 0 +newrelic.transaction_tracer.detail = 1 +newrelic.code_level_metrics.enabled = 0 +newrelic.fibers.disabled = false +*/ + +/*EXPECT_METRICS_EXIST +External/127.0.0.1/all +*/ + +/*EXPECT_SPAN_EVENTS_LIKE +[ + [ + { + "traceId": "??", + "duration": "??", + "transactionId": "??", + "name": "External\/127.0.0.1\/all", + "guid": "??", + "type": "Span", + "category": "http", + "priority": "??", + "sampled": true, + "timestamp": "??", + "parentId": "??", + "span.kind": "client", + "component": "Guzzle 6" + }, + {}, + { + "http.url": "http://ENV[EXTERNAL_HOST]/delay", + "http.method": "GET", + "http.statusCode": 0 + } + ] +] +*/ + +function test_timeout_async() { + require_once(realpath(dirname(__FILE__)) . '/../../../include/config.php'); + require_once(realpath(dirname(__FILE__)) . '/../../../include/unpack_guzzle.php'); + require_guzzle(7); + + $client = new GuzzleHttp\Client([ + // Base URI is used with relative requests + 'base_uri' => "$EXTERNAL_HOST", + // Set a short timeout, shorter than delay duration in the request + 'timeout' => 0.01, + ]); + + Fiber::suspend(); + + $promise = $client->getAsync('/delay?duration=100ms'); + Fiber::suspend(); + GuzzleHttp\Promise\Utils::settle($promise)->wait(); +} + +$fiber = new Fiber('test_timeout_async'); +$fiber->start(); +$fiber->resume(); +$fiber->resume(); diff --git a/tests/integration/external/guzzle7/test_timeout_sync_fibers.php b/tests/integration/external/guzzle7/test_timeout_sync_fibers.php new file mode 100644 index 000000000..808da3af2 --- /dev/null +++ b/tests/integration/external/guzzle7/test_timeout_sync_fibers.php @@ -0,0 +1,83 @@ += 8.1 required\n"); +} +require("skipif.inc"); +?> +*/ + +/*INI +newrelic.distributed_tracing_enabled = true +newrelic.transaction_tracer.threshold = 0 +newrelic.transaction_tracer.detail = 1 +newrelic.code_level_metrics.enabled = 0 +newrelic.fibers.disabled = false +*/ + +/*EXPECT_METRICS_EXIST +External/127.0.0.1/all +*/ + +/*EXPECT_SPAN_EVENTS_LIKE +[ + [ + { + "traceId": "??", + "duration": "??", + "transactionId": "??", + "name": "External\/127.0.0.1\/all", + "guid": "??", + "type": "Span", + "category": "http", + "priority": "??", + "sampled": true, + "timestamp": "??", + "parentId": "??", + "span.kind": "client", + "component": "Guzzle 6" + }, + {}, + { + "http.url": "http://ENV[EXTERNAL_HOST]/delay", + "http.method": "GET", + "http.statusCode": 0 + } + ] +] +*/ + +function test_timeout_sync() { + require_once(realpath(dirname(__FILE__)) . '/../../../include/config.php'); + require_once(realpath(dirname(__FILE__)) . '/../../../include/unpack_guzzle.php'); + require_guzzle(7); + + $client = new GuzzleHttp\Client([ + // Base URI is used with relative requests + 'base_uri' => "$EXTERNAL_HOST", + // Set a short timeout, shorter than delay duration in the request + 'timeout' => 0.01, + ]); + + Fiber::suspend(); + + try { + $response = $client->get('/delay?duration=100ms'); + } catch (Exception $e) { + // Expected timeout exception + } +} + +$fiber = new Fiber('test_timeout_sync'); +$fiber->start(); +$fiber->resume(); diff --git a/tests/integration/external/guzzle7/test_transfer_exception_async_fibers.php b/tests/integration/external/guzzle7/test_transfer_exception_async_fibers.php new file mode 100644 index 000000000..7c861e09c --- /dev/null +++ b/tests/integration/external/guzzle7/test_transfer_exception_async_fibers.php @@ -0,0 +1,101 @@ += 8.1 required\n"); +} +?> +*/ + +/*INI +newrelic.distributed_tracing_enabled = true +newrelic.transaction_tracer.threshold = 0 +newrelic.transaction_tracer.detail = 1 +newrelic.code_level_metrics.enabled = 0 +newrelic.fibers.disabled = false +*/ + +/*ENVIRONMENT +TEST_EXTERNAL_HOST=example.com +*/ + +/*EXPECT_METRICS_EXIST +External/ENV[TEST_EXTERNAL_HOST]/all +*/ + +/*EXPECT_SPAN_EVENTS_LIKE +[ + [ + { + "traceId": "??", + "duration": "??", + "transactionId": "??", + "name": "External/ENV[TEST_EXTERNAL_HOST]/all", + "guid": "??", + "type": "Span", + "category": "http", + "priority": "??", + "sampled": true, + "timestamp": "??", + "parentId": "??", + "span.kind": "client", + "component": "Guzzle 6" + }, + {}, + { + "http.url": "http://ENV[TEST_EXTERNAL_HOST]/resource", + "http.method": "GET", + "http.statusCode": 0 + } + ] +] +*/ + +/*EXPECT_ERROR_EVENTS null*/ + +/*EXPECT +all's well that ends well +*/ + +function test_async_transfer_exception() { + require_once(realpath(dirname(__FILE__)) . '/../../../include/config.php'); + require_once(realpath(dirname(__FILE__)) . '/../../../include/unpack_guzzle.php'); + require_guzzle(7); + + $TEST_EXTERNAL_HOST=getenv('TEST_EXTERNAL_HOST'); + + $request = new \GuzzleHttp\Psr7\Request('GET', "http://$TEST_EXTERNAL_HOST/resource"); + + $stack = GuzzleHttp\HandlerStack::create( + new GuzzleHttp\Handler\MockHandler([ + new \GuzzleHttp\Exception\TransferException("Transfer exception") + ])); + + $client = new GuzzleHttp\Client([ + 'handler' => $stack, + + ]); + + Fiber::suspend(); + $promise = $client->sendAsync($request); + Fiber::suspend(); + GuzzleHttp\Promise\Utils::settle($promise)->wait(); + + echo "all's well that ends well" . PHP_EOL; +} + +$fiber = new Fiber('test_async_transfer_exception'); +$fiber->start(); +$fiber->resume(); +$fiber->resume(); diff --git a/tests/integration/external/guzzle7/test_transfer_exception_sync_fibers.php b/tests/integration/external/guzzle7/test_transfer_exception_sync_fibers.php new file mode 100644 index 000000000..36fa3690c --- /dev/null +++ b/tests/integration/external/guzzle7/test_transfer_exception_sync_fibers.php @@ -0,0 +1,106 @@ += 8.1 required\n"); +} +?> +*/ + +/*INI +newrelic.distributed_tracing_enabled = true +newrelic.transaction_tracer.threshold = 0 +newrelic.transaction_tracer.detail = 1 +newrelic.code_level_metrics.enabled = 0 +newrelic.fibers.disabled = false +*/ + +/*ENVIRONMENT +TEST_EXTERNAL_HOST=example.com +*/ + +/*EXPECT_METRICS_EXIST +External/ENV[TEST_EXTERNAL_HOST]/all +*/ + +/*EXPECT_SPAN_EVENTS_LIKE +[ + [ + { + "traceId": "??", + "duration": "??", + "transactionId": "??", + "name": "External/ENV[TEST_EXTERNAL_HOST]/all", + "guid": "??", + "type": "Span", + "category": "http", + "priority": "??", + "sampled": true, + "timestamp": "??", + "parentId": "??", + "span.kind": "client", + "component": "Guzzle 6" + }, + {}, + { + "http.url": "http://ENV[TEST_EXTERNAL_HOST]/resource", + "http.method": "GET", + "http.statusCode": 0 + } + ] +] +*/ + +/*EXPECT_ERROR_EVENTS null*/ + +/*EXPECT +all's well that ends well +*/ + +function test_sync_transfer_exception() { + require_once(realpath(dirname(__FILE__)) . '/../../../include/config.php'); + require_once(realpath(dirname(__FILE__)) . '/../../../include/unpack_guzzle.php'); + require_guzzle(7); + + $TEST_EXTERNAL_HOST=getenv('TEST_EXTERNAL_HOST'); + + $request = new \GuzzleHttp\Psr7\Request('GET', "http://$TEST_EXTERNAL_HOST/resource"); + + $stack = GuzzleHttp\HandlerStack::create( + new GuzzleHttp\Handler\MockHandler([ + new \GuzzleHttp\Exception\TransferException("Transfer exception") + ])); + + $client = new GuzzleHttp\Client([ + 'handler' => $stack, + + ]); + + Fiber::suspend(); + + try { + $response = $client->send($request); + } catch (\GuzzleHttp\Exception\TransferException $e) { + // Expected exception + } + + Fiber::suspend(); + + echo "all's well that ends well" . PHP_EOL; +} + +$fiber = new Fiber('test_sync_transfer_exception'); +$fiber->start(); +$fiber->resume(); +$fiber->resume(); diff --git a/tests/integration/external/guzzle7/test_txn_ignore_fibers.php b/tests/integration/external/guzzle7/test_txn_ignore_fibers.php new file mode 100644 index 000000000..629e7ac40 --- /dev/null +++ b/tests/integration/external/guzzle7/test_txn_ignore_fibers.php @@ -0,0 +1,109 @@ += 8.1 required\n"); +} +?> +*/ + +/*INI +newrelic.distributed_tracing_enabled = true +newrelic.cross_application_tracer.enabled = false +newrelic.fibers.disabled = false +*/ + +/*EXPECT +X-NewRelic-ID=missing X-NewRelic-Transaction=missing tracing endpoint reached +X-NewRelic-ID=missing X-NewRelic-Transaction=missing tracing endpoint reached +X-NewRelic-ID=missing X-NewRelic-Transaction=missing Customer-Header=found tracing endpoint reached +*/ + +/*EXPECT_RESPONSE_HEADERS +*/ + +/*EXPECT_METRICS_DONT_EXIST +External/127.0.0.1/all, 3 +*/ + +/*EXPECT_SPAN_EVENTS +[ + "?? agent run id", + { + "reservoir_size": 10000, + "events_seen": 1 + }, + [ + [ + { + "category": "generic", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "OtherTransaction\/php__FILE__", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "nr.entryPoint": true, + "transaction.name": "OtherTransaction\/php__FILE__" + }, + {}, + {} + ] + ] +]*/ + +require_once(realpath(dirname(__FILE__)) . '/../../../include/config.php'); +require_once(realpath(dirname(__FILE__)) . '/../../../include/unpack_guzzle.php'); +require_guzzle(7); + +use GuzzleHttp\Client; + +function test_guzzle_txn_ignore() { + // Ignore this transaction + newrelic_ignore_transaction(); + + /* Create URL. */ + $url = "http://" . make_tracing_url(realpath(dirname(__FILE__)) . '/../../../include/tracing_endpoint.php'); + + $client = new Client(); + + $response = $client->get($url); + echo $response->getBody(); + Fiber::suspend(); + + $response = $client->get($url, [ + 'headers' => [ + 'zip' => 'zap']]); + echo $response->getBody(); + Fiber::suspend(); + + $response = $client->get($url, [ + 'headers' => [ + 'zip' => 'zap', + CUSTOMER_HEADER => 'zap']]); + echo $response->getBody(); +} + +$fiber = new Fiber('test_guzzle_txn_ignore'); +$fiber->start(); +$fiber->resume(); +$fiber->resume(); + + +// End/Start a new transaction to ensure some data is added to the harvest. +// This is required by the integration test runner. +newrelic_end_transaction(); +newrelic_start_transaction(ini_get("newrelic.appname")); diff --git a/tests/integration/external/guzzle7/test_uncaught_bad_response_exception_sync_fibers.php b/tests/integration/external/guzzle7/test_uncaught_bad_response_exception_sync_fibers.php new file mode 100644 index 000000000..136cf3207 --- /dev/null +++ b/tests/integration/external/guzzle7/test_uncaught_bad_response_exception_sync_fibers.php @@ -0,0 +1,152 @@ += 8.1 required\n"); +} +?> +*/ + +/*INI +newrelic.distributed_tracing_enabled = true +newrelic.transaction_tracer.threshold = 0 +newrelic.transaction_tracer.detail = 1 +newrelic.code_level_metrics.enabled = 0 +newrelic.fibers.disabled = false +*/ + +/*ENVIRONMENT +TEST_EXTERNAL_HOST=example.com +*/ + +/*EXPECT_METRICS_EXIST +External/ENV[TEST_EXTERNAL_HOST]/all +*/ + +/*EXPECT_SPAN_EVENTS_LIKE +[ + [ + { + "traceId": "??", + "duration": "??", + "transactionId": "??", + "name": "External/ENV[TEST_EXTERNAL_HOST]/all", + "guid": "??", + "type": "Span", + "category": "http", + "priority": "??", + "sampled": true, + "timestamp": "??", + "parentId": "??", + "span.kind": "client", + "component": "Guzzle 6" + }, + {}, + { + "http.url": "http://ENV[TEST_EXTERNAL_HOST]/resource", + "http.method": "GET", + "http.statusCode": 404 + } + ] +] +*/ + +/*EXPECT_ERROR_EVENTS +[ + "?? agent run id", + { + "reservoir_size": "??", + "events_seen": 1 + }, + [ + [ + { + "type": "TransactionError", + "timestamp": "??", + "error.class": "GuzzleHttp\\Exception\\BadResponseException", + "error.message": "Uncaught exception 'GuzzleHttp\\Exception\\BadResponseException' with message 'ClientException' in __FILE__:??", + "transactionName": "OtherTransaction\/php__FILE__", + "duration": "??", + "externalDuration": "??", + "externalCallCount": 1, + "nr.transactionGuid": "??", + "guid": "??", + "sampled": true, + "priority": "??", + "traceId": "??", + "spanId": "??" + }, + {}, + {} + ] + ] +] +*/ + +/*EXPECT_TRACED_ERRORS +[ + "?? agent run id", + [ + [ + "?? when", + "OtherTransaction/php__FILE__", + "Uncaught exception 'GuzzleHttp\\Exception\\BadResponseException' with message 'ClientException' in __FILE__:??", + "GuzzleHttp\\Exception\\BadResponseException", + { + "stack_trace": [ + " in test_uncaught_sync_bad_response called at ? (?)", + " in Fiber::start called at __FILE__ (??)" + ], + "agentAttributes": "??", + "intrinsics": "??" + }, + "?? transaction ID" + ] + ] +] +*/ + +function test_uncaught_sync_bad_response() { + require_once(realpath(dirname(__FILE__)) . '/../../../include/config.php'); + require_once(realpath(dirname(__FILE__)) . '/../../../include/unpack_guzzle.php'); + require_guzzle(7); + + $TEST_EXTERNAL_HOST=getenv('TEST_EXTERNAL_HOST'); + + $request = new \GuzzleHttp\Psr7\Request('GET', "http://$TEST_EXTERNAL_HOST/resource"); + $response = new \GuzzleHttp\Psr7\Response(404, [], "Not found!"); + + $stack = GuzzleHttp\HandlerStack::create( + new GuzzleHttp\Handler\MockHandler([ + new \GuzzleHttp\Exception\BadResponseException( + "ClientException", + $request, + $response + ) + ])); + + $client = new GuzzleHttp\Client([ + 'handler' => $stack, + + ]); + + Fiber::suspend(); + + // This will throw an uncaught exception + $response = $client->send($request); +} + +$fiber = new Fiber('test_uncaught_sync_bad_response'); +$fiber->start(); +$fiber->resume(); diff --git a/tests/integration/external/guzzle7/test_uncaught_transfer_exception_async_fibers.php b/tests/integration/external/guzzle7/test_uncaught_transfer_exception_async_fibers.php new file mode 100644 index 000000000..9f0818859 --- /dev/null +++ b/tests/integration/external/guzzle7/test_uncaught_transfer_exception_async_fibers.php @@ -0,0 +1,154 @@ += 8.1 required\n"); +} +?> +*/ + +/*INI +newrelic.distributed_tracing_enabled = true +newrelic.transaction_tracer.threshold = 0 +newrelic.transaction_tracer.detail = 1 +newrelic.code_level_metrics.enabled = 0 +newrelic.fibers.disabled = false +*/ + +/*ENVIRONMENT +TEST_EXTERNAL_HOST=example.com +*/ + +/*EXPECT_METRICS_EXIST +External/ENV[TEST_EXTERNAL_HOST]/all +*/ + +/*EXPECT_SPAN_EVENTS_LIKE +[ + [ + { + "traceId": "??", + "duration": "??", + "transactionId": "??", + "name": "External/ENV[TEST_EXTERNAL_HOST]/all", + "guid": "??", + "type": "Span", + "category": "http", + "priority": "??", + "sampled": true, + "timestamp": "??", + "parentId": "??", + "span.kind": "client", + "component": "Guzzle 6" + }, + {}, + { + "http.url": "http://ENV[TEST_EXTERNAL_HOST]/resource", + "http.method": "GET", + "http.statusCode": 0 + } + ] +] +*/ + +/*EXPECT_ERROR_EVENTS +[ + "?? agent run id", + { + "reservoir_size": "??", + "events_seen": 1 + }, + [ + [ + { + "type": "TransactionError", + "timestamp": "??", + "error.class": "GuzzleHttp\\Exception\\TransferException", + "error.message": "Uncaught exception 'GuzzleHttp\\Exception\\TransferException' with message 'I'm covered in bees!!!' in __FILE__:??", + "transactionName": "OtherTransaction\/php__FILE__", + "duration": "??", + "externalDuration": "??", + "externalCallCount": 1, + "nr.transactionGuid": "??", + "guid": "??", + "sampled": true, + "priority": "??", + "traceId": "??", + "spanId": "??" + }, + {}, + {} + ] + ] +] +*/ + +/*EXPECT_REGEX +^\s*Fatal error: Uncaught GuzzleHttp\\Exception\\TransferException: I'm covered in bees!!! +*/ + +/*EXPECT_TRACED_ERRORS +[ + "?? agent run id", + [ + [ + "?? when", + "OtherTransaction/php__FILE__", + "Uncaught exception 'GuzzleHttp\\Exception\\TransferException' with message 'I'm covered in bees!!!' in __FILE__:??", + "GuzzleHttp\\Exception\\TransferException", + { + "stack_trace": [ + " in test_uncaught_async_transfer_exception called at ? (?)", + " in Fiber::start called at __FILE__ (??)" + ], + "agentAttributes": "??", + "intrinsics": "??" + }, + "?? transaction ID" + ] + ] +] +*/ + +function test_uncaught_async_transfer_exception() { + require_once(realpath(dirname(__FILE__)) . '/../../../include/config.php'); + require_once(realpath(dirname(__FILE__)) . '/../../../include/unpack_guzzle.php'); + require_guzzle(7); + + $TEST_EXTERNAL_HOST=getenv('TEST_EXTERNAL_HOST'); + + $request = new \GuzzleHttp\Psr7\Request('GET', "http://$TEST_EXTERNAL_HOST/resource"); + + $stack = GuzzleHttp\HandlerStack::create( + new GuzzleHttp\Handler\MockHandler([ + new \GuzzleHttp\Exception\TransferException("I'm covered in bees!!!") + ])); + + $client = new GuzzleHttp\Client([ + 'handler' => $stack, + + ]); + + Fiber::suspend(); + + // This will cause an uncaught exception + $promise = $client->sendAsync($request); + Fiber::suspend(); + $promise->wait(); +} + +$fiber = new Fiber('test_uncaught_async_transfer_exception'); +$fiber->start(); +$fiber->resume(); +$fiber->resume(); diff --git a/tests/integration/memcached/test_basic_fibers.php b/tests/integration/memcached/test_basic_fibers.php new file mode 100644 index 000000000..8691f498d --- /dev/null +++ b/tests/integration/memcached/test_basic_fibers.php @@ -0,0 +1,279 @@ += 8.1 required\n"); +} +require('skipif.inc'); +?> +*/ + +/*INI +newrelic.fibers.disabled = false +*/ + +/*EXPECT +ok - add +ok - set +Starting Func 'a' +ok - replace +ok - assertEqual +ok - assertEqual +ok - delete +Ending Func 'a' +ok - delete +ok - delete +ok - add +ok - increment +ok - increment +ok - decrement +ok - decrement +ok - assertEqual +ok - delete +*/ + +/*EXPECT_SPAN_EVENTS_LIKE +[ + [ + { + "traceId": "??", + "duration": "??", + "transactionId": "??", + "name": "OtherTransaction\/php__FILE__", + "guid": "ENV[GUID_ROOT]", + "type": "Span", + "category": "generic", + "priority": "??", + "sampled": true, + "nr.entryPoint": true, + "timestamp": "??", + "transaction.name": "OtherTransaction\/php__FILE__" + }, + {}, + {} + ], + [ + { + "category": "generic", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "Custom\/main", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "ENV[GUID_ROOT]" + }, + {}, + {} + ], + [ + { + "category": "datastore", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "Datastore\/operation\/Memcached\/add", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "ENV[GUID_MAIN]", + "span.kind": "client", + "component": "Memcached" + }, + {}, + { + "peer.address": "unknown:unknown" + } + ], + [ + { + "category": "datastore", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "Datastore\/operation\/Memcached\/delete", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "ENV[GUID_MAIN]", + "span.kind": "client", + "component": "Memcached" + }, + {}, + { + "peer.address": "unknown:unknown" + } + ], + [ + { + "category": "datastore", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "Datastore\/operation\/Memcached\/decr", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "ENV[GUID_MAIN]", + "span.kind": "client", + "component": "Memcached" + }, + {}, + { + "peer.address": "unknown:unknown" + } + ], + [ + { + "category": "generic", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "Custom\/a", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "ENV[GUID_ROOT]" + }, + {}, + {} + ] +] +*/ + +/*EXPECT_METRICS +[ + "?? agent run id", + "?? start time", + "?? stop time", + [ + [{"name":"DurationByCaller/Unknown/Unknown/Unknown/Unknown/all"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"DurationByCaller/Unknown/Unknown/Unknown/Unknown/allOther"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/all"}, [15, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/allOther"}, [15, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/Memcached/all"}, [15, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/Memcached/allOther"}, [15, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/instance/Memcached/ENV[MEMCACHE_HOST]/11211"},[1, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/operation/Memcached/add"}, [2, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/operation/Memcached/add", + "scope":"OtherTransaction/php__FILE__"}, [2, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/operation/Memcached/decr"}, [2, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/operation/Memcached/decr", + "scope":"OtherTransaction/php__FILE__"}, [2, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/operation/Memcached/delete"}, [4, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/operation/Memcached/delete", + "scope":"OtherTransaction/php__FILE__"}, [4, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/operation/Memcached/get"}, [3, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/operation/Memcached/get", + "scope":"OtherTransaction/php__FILE__"}, [3, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/operation/Memcached/incr"}, [2, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/operation/Memcached/incr", + "scope":"OtherTransaction/php__FILE__"}, [2, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/operation/Memcached/replace"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/operation/Memcached/replace", + "scope":"OtherTransaction/php__FILE__"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/operation/Memcached/set"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/operation/Memcached/set", + "scope":"OtherTransaction/php__FILE__"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"OtherTransaction/all"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"OtherTransaction/php__FILE__"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"OtherTransactionTotalTime"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"OtherTransactionTotalTime/php__FILE__"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Supportability/Logging/Forwarding/PHP/enabled"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Supportability/Logging/Metrics/PHP/enabled"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Supportability/Logging/LocalDecorating/PHP/disabled"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Supportability/Logging/Labels/PHP/disabled"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Supportability/api/get_linking_metadata"}, ["??", "??", "??", "??", "??", "??"]], + [{"name":"Supportability/PHP/Fiber/used"}, ["??", "??", "??", "??", "??", "??"]] + ] +] +*/ + +require_once(realpath (dirname ( __FILE__ )) . '/../../include/helpers.php'); +require_once(realpath (dirname ( __FILE__ )) . '/../../include/tap.php'); +require_once(realpath (dirname ( __FILE__ )) . '/memcache.inc'); + +define('KEYLEN', 8); + + +function main() +{ + global $MEMCACHE_HOST, $MEMCACHE_PORT; + + env_var_for_expects("GUID_MAIN", newrelic_get_linking_metadata()['span.id'] ?? ''); + $memcached = new Memcached(); + $memcached->addServer($MEMCACHE_HOST, $MEMCACHE_PORT); + + $key1 = randstr(KEYLEN); + $key2 = randstr(KEYLEN); + + // basic operations + $test = new TestCase($memcached); + $test->add($key1, 'foo'); + $test->set($key2, 'bar'); + Fiber::suspend(); + $test->replace($key2, 'baz'); + $test->assertEqual('foo', $key1); + $test->assertEqual('baz', $key2); + $test->delete($key1); + Fiber::suspend(); + $test->delete($key2); + $test->refuteDelete($key2); + + $key3 = randstr(KEYLEN); + + // increment/decrement + $test = new TestCase($memcached); + $test->add($key3, 0); + $test->increment($key3); + $test->increment($key3, 2); + Fiber::suspend(); + $test->decrement($key3); + $test->decrement($key3, 2); + $test->assertEqual(0, $key3); + $test->delete($key3); + + $memcached->quit(); +} + +function a() +{ + echo "Starting Func 'a'\n"; + env_var_for_expects("GUID_A", newrelic_get_linking_metadata()['span.id'] ?? ''); + time_nanosleep(0, 100000000); + Fiber::suspend(); + echo "Ending Func 'a'\n"; +} + +env_var_for_expects("GUID_ROOT", newrelic_get_linking_metadata()['span.id'] ?? ''); + +$fiber_a = new Fiber('a'); +$fiber_memcached = new Fiber('main'); + +$fiber_memcached->start(); +$fiber_a->start(); +$fiber_memcached->resume(); +$fiber_a->resume(); +$fiber_memcached->resume(); +$fiber_memcached->resume(); diff --git a/tests/integration/mysqli/test_explain_fibers.php b/tests/integration/mysqli/test_explain_fibers.php new file mode 100644 index 000000000..5070bb2ab --- /dev/null +++ b/tests/integration/mysqli/test_explain_fibers.php @@ -0,0 +1,272 @@ += 8.1 required\n"); +} +*/ + +/*INI +error_reporting = E_ALL & ~E_DEPRECATED +newrelic.transaction_tracer.explain_enabled = true +newrelic.transaction_tracer.explain_threshold = 0 +newrelic.transaction_tracer.record_sql = obfuscated +newrelic.fibers.disabled = false +*/ + +/*EXPECT +STATISTICS +TABLES +*/ + +/*EXPECT_METRICS_EXIST +Datastore/all, 2 +Datastore/allOther, 2 +Datastore/MySQL/all, 2 +Datastore/MySQL/allOther, 2 +Datastore/statement/MySQL/tables/select, 2 +Datastore/operation/MySQL/select, 2 +*/ + +/*EXPECT_SPAN_EVENTS_LIKE +[ + [ + { + "traceId": "??", + "duration": "??", + "transactionId": "??", + "name": "OtherTransaction\/php__FILE__", + "guid": "ENV[GUID_ROOT]", + "type": "Span", + "category": "generic", + "priority": "??", + "sampled": true, + "nr.entryPoint": true, + "timestamp": "??", + "transaction.name": "OtherTransaction\/php__FILE__" + }, + {}, + {} + ], + [ + { + "category": "generic", + "type": "Span", + "guid": "ENV[GUID_TEST_FIBER_1]", + "traceId": "??", + "transactionId": "??", + "name": "Custom\/test_fiber_query", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "ENV[GUID_FIBER_1]" + }, + {}, + {} + ], + [ + { + "category": "datastore", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "Datastore\/statement\/MySQL\/tables\/select", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "ENV[GUID_TEST_FIBER_1]", + "span.kind": "client", + "component": "MySQL" + }, + {}, + { + "peer.address": "unknown:unknown", + "db.statement": "SELECT TABLE_NAME FROM information_schema.tables WHERE table_name=?" + } + ], + [ + { + "category": "generic", + "type": "Span", + "guid": "ENV[GUID_TEST_FIBER_2]", + "traceId": "??", + "transactionId": "??", + "name": "Custom\/test_fiber_query", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "ENV[GUID_FIBER_2]" + }, + {}, + {} + ], + [ + { + "category": "datastore", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "Datastore\/statement\/MySQL\/tables\/select", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "ENV[GUID_TEST_FIBER_2]", + "span.kind": "client", + "component": "MySQL" + }, + {}, + { + "peer.address": "unknown:unknown", + "db.statement": "SELECT TABLE_NAME FROM information_schema.tables WHERE table_name=?" + } + ] +] +*/ + +/*EXPECT_ERROR_EVENTS +null +*/ + +/*EXPECT_SLOW_SQLS +[ + [ + [ + "OtherTransaction/php__FILE__", + "", + "?? SQL ID", + "SELECT TABLE_NAME FROM information_schema.tables WHERE table_name=?", + "Datastore/statement/MySQL/tables/select", + 2, + "?? total time", + "?? min time", + "?? max time", + { + "explain_plan": [ + [ + "id", + "select_type", + "table", + "type", + "possible_keys", + "key", + "key_len", + "ref", + "rows", + "Extra" + ], + [ + [ + 1, + "SIMPLE", + "tables", + "ALL", + null, + "TABLE_NAME", + null, + null, + null, + "Using where; Skip_open_table; Scanned 1 database" + ] + ] + ], + "backtrace": [ + " in mysqli_stmt_execute called at __FILE__ (??)", + " in test_fiber_query called at __FILE__ (??)", + "??", + " in Fiber::resume called at __FILE__ (??)" + ] + } + ] + ] +] +*/ + +/*EXPECT_TRACED_ERRORS +null +*/ + +require_once(realpath(dirname(__FILE__)) . '/../../include/config.php'); +require_once(realpath(dirname(__FILE__)) . '/../../include/helpers.php'); + +env_var_for_expects("GUID_ROOT", newrelic_get_linking_metadata()['span.id'] ?? ''); + +function test_fiber_query($link, $table_name, $fiber_id) +{ + + env_var_for_expects("GUID_TEST_FIBER_" . $fiber_id, newrelic_get_linking_metadata()['span.id'] ?? ''); + $query = "SELECT TABLE_NAME FROM information_schema.tables WHERE table_name=?"; + + $stmt = mysqli_prepare($link, $query); + if (FALSE === $stmt) { + echo mysqli_error($link) . "\n"; + return; + } + + if (FALSE === mysqli_stmt_bind_param($stmt, 's', $table_name)) { + echo mysqli_stmt_error($stmt) . "\n"; + return; + } + + // Suspend within the fiber before executing the query + Fiber::suspend(); + + if (FALSE === mysqli_stmt_execute($stmt)) { + echo mysqli_stmt_error($stmt) . "\n"; + return; + } + + if (FALSE === mysqli_stmt_bind_result($stmt, $result)) { + echo mysqli_stmt_error($stmt) . "\n"; + return; + } + + while (mysqli_stmt_fetch($stmt)) { + echo $result . "\n"; + } + + mysqli_stmt_close($stmt); +} + +// Create connection +$link = mysqli_connect($MYSQL_HOST, $MYSQL_USER, $MYSQL_PASSWD, $MYSQL_DB, $MYSQL_PORT, $MYSQL_SOCKET); +if (mysqli_connect_errno()) { + echo mysqli_connect_error() . "\n"; + exit(1); +} + +// Create fibers for database queries +$fiber1 = new Fiber(function() use ($link) { + env_var_for_expects("GUID_FIBER_1", newrelic_get_linking_metadata()['span.id'] ?? ''); + return test_fiber_query($link, 'STATISTICS', 1); +}); + +$fiber2 = new Fiber(function() use ($link) { + env_var_for_expects("GUID_FIBER_2", newrelic_get_linking_metadata()['span.id'] ?? ''); + return test_fiber_query($link, 'TABLES', 2); +}); + +// Start and execute fibers +$fiber1->start(); +$fiber2->start(); + +// Resume fibers to complete the queries +$fiber1->resume(); +$fiber2->resume(); + +mysqli_close($link); diff --git a/tests/integration/mysqli/test_explain_multiple_queries_fibers.php b/tests/integration/mysqli/test_explain_multiple_queries_fibers.php new file mode 100644 index 000000000..0f3623d9f --- /dev/null +++ b/tests/integration/mysqli/test_explain_multiple_queries_fibers.php @@ -0,0 +1,421 @@ += 8.1 required\n"); +} +*/ + +/*INI +error_reporting = E_ALL & ~E_DEPRECATED +newrelic.transaction_tracer.explain_enabled = true +newrelic.transaction_tracer.explain_threshold = 0 +newrelic.transaction_tracer.record_sql = obfuscated +newrelic.fibers.disabled = false +*/ + +/*EXPECT_REGEX +Fiber A: Checking for STATISTICS table +Fiber B: Checking for TABLES table +Fiber C: Counting information_schema tables +Found .* tables +Found table: TABLES +Found table: STATISTICS +All fiber queries completed successfully +*/ + +/*EXPECT_METRICS_EXIST +Datastore/all, 3 +Datastore/allOther, 3 +Datastore/MySQL/all, 3 +Datastore/MySQL/allOther, 3 +Datastore/statement/MySQL/tables/select, 3 +Datastore/operation/MySQL/select, 3 +Supportability/PHP/Fiber/used +*/ + +/*EXPECT_SPAN_EVENTS_LIKE +[ + [ + { + "traceId": "??", + "duration": "??", + "transactionId": "??", + "name": "OtherTransaction\/php__FILE__", + "guid": "ENV[GUID_ROOT]", + "type": "Span", + "category": "generic", + "priority": "??", + "sampled": true, + "nr.entryPoint": true, + "timestamp": "??", + "transaction.name": "OtherTransaction\/php__FILE__" + }, + {}, + {} + ], + [ + { + "category": "generic", + "type": "Span", + "guid": "ENV[GUID_FIBER_CHECK_A]", + "traceId": "??", + "transactionId": "??", + "name": "Custom\/fiber_table_check", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "ENV[GUID_FIBER_A]" + }, + {}, + {} + ], + [ + { + "category": "datastore", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "Datastore\/statement\/MySQL\/tables\/select", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "ENV[GUID_FIBER_CHECK_A]", + "span.kind": "client", + "component": "MySQL" + }, + {}, + { + "peer.address": "unknown:unknown", + "db.statement": "SELECT TABLE_NAME FROM information_schema.tables WHERE table_name=?" + } + ], + [ + { + "category": "generic", + "type": "Span", + "guid": "ENV[GUID_FIBER_CHECK_B]", + "traceId": "??", + "transactionId": "??", + "name": "Custom\/fiber_table_check", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "ENV[GUID_FIBER_B]" + }, + {}, + {} + ], + [ + { + "category": "datastore", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "Datastore\/statement\/MySQL\/tables\/select", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "ENV[GUID_FIBER_CHECK_B]", + "span.kind": "client", + "component": "MySQL" + }, + {}, + { + "peer.address": "unknown:unknown", + "db.statement": "SELECT TABLE_NAME FROM information_schema.tables WHERE table_name=?" + } + ], + [ + { + "category": "generic", + "type": "Span", + "guid": "ENV[GUID_FIBER_COUNT_C]", + "traceId": "??", + "transactionId": "??", + "name": "Custom\/fiber_table_count", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "ENV[GUID_FIBER_C]" + }, + {}, + {} + ], + [ + { + "category": "datastore", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "Datastore\/statement\/MySQL\/tables\/select", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "ENV[GUID_FIBER_COUNT_C]", + "span.kind": "client", + "component": "MySQL" + }, + {}, + { + "peer.address": "unknown:unknown", + "db.statement": "SELECT COUNT(*) as table_count FROM information_schema.tables WHERE table_schema=?" + } + ] +] +*/ + +/*EXPECT_ERROR_EVENTS +null +*/ + +/*EXPECT_SLOW_SQLS +[ + [ + [ + "OtherTransaction/php__FILE__", + "", + "?? SQL ID", + "SELECT TABLE_NAME FROM information_schema.tables WHERE table_name=?", + "Datastore/statement/MySQL/tables/select", + 2, + "?? total time", + "?? min time", + "?? max time", + { + "explain_plan": [ + [ + "id", + "select_type", + "table", + "type", + "possible_keys", + "key", + "key_len", + "ref", + "rows", + "Extra" + ], + [ + [ + 1, + "SIMPLE", + "tables", + "ALL", + null, + "TABLE_NAME", + null, + null, + null, + "Using where; Skip_open_table; Scanned 1 database" + ] + ] + ], + "backtrace": [ + " in mysqli_stmt_execute called at __FILE__ (??)", + " in fiber_table_check called at __FILE__ (??)", + "??", + " in Fiber::resume called at __FILE__ (??)" + ] + } + ], + [ + "OtherTransaction/php__FILE__", + "", + "?? SQL ID", + "SELECT COUNT(*) as table_count FROM information_schema.tables WHERE table_schema=?", + "Datastore/statement/MySQL/tables/select", + 1, + "?? total time", + "?? min time", + "?? max time", + { + "explain_plan": [ + [ + "id", + "select_type", + "table", + "type", + "possible_keys", + "key", + "key_len", + "ref", + "rows", + "Extra" + ], + [ + [ + 1, + "SIMPLE", + "tables", + "ALL", + null, + "TABLE_SCHEMA", + null, + null, + null, + "Using where; Skip_open_table; Scanned 1 database" + ] + ] + ], + "backtrace": [ + " in mysqli_stmt_execute called at __FILE__ (??)", + " in fiber_table_count called at __FILE__ (??)", + "??", + " in Fiber::resume called at __FILE__ (??)" + ] + } + ] + ] +] +*/ + +/*EXPECT_TRACED_ERRORS +null +*/ + +require_once(realpath(dirname(__FILE__)) . '/../../include/config.php'); +require_once(realpath(dirname(__FILE__)) . '/../../include/helpers.php'); + +env_var_for_expects("GUID_ROOT", newrelic_get_linking_metadata()['span.id'] ?? ''); + +function fiber_table_check($link, $fiber_name, $table_name) +{ + + env_var_for_expects("GUID_FIBER_CHECK_". $fiber_name, newrelic_get_linking_metadata()['span.id'] ?? ''); + + echo "Fiber $fiber_name: Checking for $table_name table\n"; + + $query = "SELECT TABLE_NAME FROM information_schema.tables WHERE table_name=?"; + $stmt = mysqli_prepare($link, $query); + if (FALSE === $stmt) { + echo "$fiber_name prepare error: " . mysqli_error($link) . "\n"; + return; + } + + if (FALSE === mysqli_stmt_bind_param($stmt, 's', $table_name)) { + echo "$fiber_name bind error: " . mysqli_stmt_error($stmt) . "\n"; + return; + } + + // Suspend before executing query + Fiber::suspend(); + + if (FALSE === mysqli_stmt_execute($stmt)) { + echo "$fiber_name execute error: " . mysqli_stmt_error($stmt) . "\n"; + return; + } + + if (FALSE === mysqli_stmt_bind_result($stmt, $result)) { + echo "$fiber_name bind result error: " . mysqli_stmt_error($stmt) . "\n"; + return; + } + + if (mysqli_stmt_fetch($stmt)) { + echo "Found table: " . $result . "\n"; + } else { + echo "Table $table_name not found\n"; + } + + mysqli_stmt_close($stmt); +} + +function fiber_table_count($link, $fiber_name, $schema_name) +{ + env_var_for_expects("GUID_FIBER_COUNT_" . $fiber_name, newrelic_get_linking_metadata()['span.id'] ?? ''); + + echo "Fiber $fiber_name: Counting $schema_name tables\n"; + + $query = "SELECT COUNT(*) as table_count FROM information_schema.tables WHERE table_schema=?"; + $stmt = mysqli_prepare($link, $query); + if (FALSE === $stmt) { + echo "$fiber_name prepare error: " . mysqli_error($link) . "\n"; + return; + } + + if (FALSE === mysqli_stmt_bind_param($stmt, 's', $schema_name)) { + echo "$fiber_name bind error: " . mysqli_stmt_error($stmt) . "\n"; + return; + } + + // Suspend before executing query + Fiber::suspend(); + + if (FALSE === mysqli_stmt_execute($stmt)) { + echo "$fiber_name execute error: " . mysqli_stmt_error($stmt) . "\n"; + return; + } + + if (FALSE === mysqli_stmt_bind_result($stmt, $count)) { + echo "$fiber_name bind result error: " . mysqli_stmt_error($stmt) . "\n"; + return; + } + + if (mysqli_stmt_fetch($stmt)) { + echo "Found $count tables\n"; + } + + mysqli_stmt_close($stmt); +} + +// Create database connection +$link = mysqli_connect($MYSQL_HOST, $MYSQL_USER, $MYSQL_PASSWD, $MYSQL_DB, $MYSQL_PORT, $MYSQL_SOCKET); +if (mysqli_connect_errno()) { + echo mysqli_connect_error() . "\n"; + exit(1); +} + +// Create multiple fibers with different query types +$fiber_a = new Fiber(function() use ($link) { + env_var_for_expects("GUID_FIBER_A", newrelic_get_linking_metadata()['span.id'] ?? ''); + return fiber_table_check($link, 'A', 'STATISTICS'); +}); + +$fiber_b = new Fiber(function() use ($link) { + env_var_for_expects("GUID_FIBER_B", newrelic_get_linking_metadata()['span.id'] ?? ''); + return fiber_table_check($link, 'B', 'TABLES'); +}); + +$fiber_c = new Fiber(function() use ($link) { + env_var_for_expects("GUID_FIBER_C", newrelic_get_linking_metadata()['span.id'] ?? ''); + return fiber_table_count($link, 'C', 'information_schema'); +}); + +// Start all fibers +$fiber_a->start(); +$fiber_b->start(); +$fiber_c->start(); + +// Resume all fibers in sequence +$fibers = [$fiber_c, $fiber_b, $fiber_a]; +for ($round = 0; $round < 3; $round++) { + foreach ($fibers as $fiber) { + if ($fiber->isSuspended()) { + $fiber->resume(); + } + } +} + +echo "All fiber queries completed successfully\n"; + +mysqli_close($link); diff --git a/tests/integration/mysqli/test_explain_nested_fibers.php b/tests/integration/mysqli/test_explain_nested_fibers.php new file mode 100644 index 000000000..9f8e8fed2 --- /dev/null +++ b/tests/integration/mysqli/test_explain_nested_fibers.php @@ -0,0 +1,390 @@ += 8.1 required\n"); +} +*/ + +/*INI +error_reporting = E_ALL & ~E_DEPRECATED +newrelic.transaction_tracer.explain_enabled = true +newrelic.transaction_tracer.explain_threshold = 0 +newrelic.transaction_tracer.record_sql = obfuscated +newrelic.fibers.disabled = false +*/ + +/*EXPECT +Level 1 fiber: STATISTICS +Level 2 fiber: TABLES +Level 3 fiber: COLUMNS +Level 4 fiber: KEY_COLUMN_USAGE +Level 5 fiber: TABLE_CONSTRAINTS +Nested fiber query execution completed +*/ + +/*EXPECT_METRICS_EXIST +Datastore/all, 5 +Datastore/allOther, 5 +Datastore/MySQL/all, 5 +Datastore/MySQL/allOther, 5 +Datastore/statement/MySQL/tables/select, 5 +Datastore/operation/MySQL/select, 5 +Supportability/PHP/Fiber/used +*/ + +/*EXPECT_SPAN_EVENTS_LIKE +[ + [ + { + "traceId": "??", + "duration": "??", + "transactionId": "??", + "name": "OtherTransaction\/php__FILE__", + "guid": "ENV[GUID_ROOT]", + "type": "Span", + "category": "generic", + "priority": "??", + "sampled": true, + "nr.entryPoint": true, + "timestamp": "??", + "transaction.name": "OtherTransaction\/php__FILE__" + }, + {}, + {} + ], + [ + { + "category": "generic", + "type": "Span", + "guid": "ENV[GUID_NESTED_FIBER_1]", + "traceId": "??", + "transactionId": "??", + "name": "Custom\/execute_nested_query", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "ENV[GUID_MAIN_FIBER]" + }, + {}, + {} + ], + [ + { + "category": "datastore", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "Datastore\/statement\/MySQL\/tables\/select", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "ENV[GUID_NESTED_FIBER_1]", + "span.kind": "client", + "component": "MySQL" + }, + {}, + { + "peer.address": "unknown:unknown", + "db.statement": "SELECT TABLE_NAME FROM information_schema.tables WHERE table_name=?" + } + ], + [ + { + "category": "generic", + "type": "Span", + "guid": "ENV[GUID_NESTED_FIBER_2]", + "traceId": "??", + "transactionId": "??", + "name": "Custom\/execute_nested_query", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "ENV[GUID_NESTED_FIBER_CLOSURE_1]" + }, + {}, + {} + ], + [ + { + "category": "datastore", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "Datastore\/statement\/MySQL\/tables\/select", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "ENV[GUID_NESTED_FIBER_2]", + "span.kind": "client", + "component": "MySQL" + }, + {}, + { + "peer.address": "unknown:unknown", + "db.statement": "SELECT TABLE_NAME FROM information_schema.tables WHERE table_name=?" + } + ], + [ + { + "category": "generic", + "type": "Span", + "guid": "ENV[GUID_NESTED_FIBER_3]", + "traceId": "??", + "transactionId": "??", + "name": "Custom\/execute_nested_query", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "ENV[GUID_NESTED_FIBER_CLOSURE_2]" + }, + {}, + {} + ], + [ + { + "category": "datastore", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "Datastore\/statement\/MySQL\/tables\/select", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "ENV[GUID_NESTED_FIBER_3]", + "span.kind": "client", + "component": "MySQL" + }, + {}, + { + "peer.address": "unknown:unknown", + "db.statement": "SELECT TABLE_NAME FROM information_schema.tables WHERE table_name=?" + } + ], + [ + { + "category": "generic", + "type": "Span", + "guid": "ENV[GUID_NESTED_FIBER_4]", + "traceId": "??", + "transactionId": "??", + "name": "Custom\/execute_nested_query", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "ENV[GUID_NESTED_FIBER_CLOSURE_3]" + }, + {}, + {} + ], + [ + { + "category": "datastore", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "Datastore\/statement\/MySQL\/tables\/select", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "ENV[GUID_NESTED_FIBER_4]", + "span.kind": "client", + "component": "MySQL" + }, + {}, + { + "peer.address": "unknown:unknown", + "db.statement": "SELECT TABLE_NAME FROM information_schema.tables WHERE table_name=?" + } + ], + [ + { + "category": "generic", + "type": "Span", + "guid": "ENV[GUID_NESTED_FIBER_5]", + "traceId": "??", + "transactionId": "??", + "name": "Custom\/execute_nested_query", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "ENV[GUID_NESTED_FIBER_CLOSURE_4]" + }, + {}, + {} + ], + [ + { + "category": "datastore", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "Datastore\/statement\/MySQL\/tables\/select", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "ENV[GUID_NESTED_FIBER_5]", + "span.kind": "client", + "component": "MySQL" + }, + {}, + { + "peer.address": "unknown:unknown", + "db.statement": "SELECT TABLE_NAME FROM information_schema.tables WHERE table_name=?" + } + ] +] +*/ + +/*EXPECT_ERROR_EVENTS +null +*/ + + +/*EXPECT_TRACED_ERRORS +null +*/ + +require_once(realpath(dirname(__FILE__)) . '/../../include/config.php'); +require_once(realpath(dirname(__FILE__)) . '/../../include/helpers.php'); + +env_var_for_expects("GUID_ROOT", newrelic_get_linking_metadata()['span.id'] ?? ''); + +function execute_nested_query($link, $level, $max_level) +{ + // Capture GUID for first level for span event verification + env_var_for_expects("GUID_NESTED_FIBER_" . $level, newrelic_get_linking_metadata()['span.id'] ?? ''); + + + // Define different queries for each level + $queries = [ + 1 => [ + 'query' => "SELECT TABLE_NAME FROM information_schema.tables WHERE table_name=?", + 'param' => 'STATISTICS', + 'column' => 'TABLE_NAME' + ], + 2 => [ + 'query' => "SELECT TABLE_NAME FROM information_schema.tables WHERE table_name=?", + 'param' => 'TABLES', + 'column' => 'TABLE_NAME' + ], + 3 => [ + 'query' => "SELECT TABLE_NAME FROM information_schema.tables WHERE table_name=?", + 'param' => 'COLUMNS', + 'column' => 'TABLE_NAME' + ], + 4 => [ + 'query' => "SELECT TABLE_NAME FROM information_schema.tables WHERE table_name=?", + 'param' => 'KEY_COLUMN_USAGE', + 'column' => 'TABLE_NAME' + ], + 5 => [ + 'query' => "SELECT TABLE_NAME FROM information_schema.tables WHERE table_name=?", + 'param' => 'TABLE_CONSTRAINTS', + 'column' => 'TABLE_NAME' + ] + ]; + + if ($level <= $max_level && isset($queries[$level])) { + $query_info = $queries[$level]; + + // Prepare and execute query for this level + $stmt = mysqli_prepare($link, $query_info['query']); + if (FALSE === $stmt) { + echo "Level $level prepare error: " . mysqli_error($link) . "\n"; + return; + } + + if (FALSE === mysqli_stmt_bind_param($stmt, 's', $query_info['param'])) { + echo "Level $level bind error: " . mysqli_stmt_error($stmt) . "\n"; + return; + } + + // Suspend fiber before query execution + Fiber::suspend(); + + if (FALSE === mysqli_stmt_execute($stmt)) { + echo "Level $level execute error: " . mysqli_stmt_error($stmt) . "\n"; + return; + } + + if (FALSE === mysqli_stmt_bind_result($stmt, $result)) { + echo "Level $level bind result error: " . mysqli_stmt_error($stmt) . "\n"; + return; + } + + // Display results for this level + if (mysqli_stmt_fetch($stmt)) { + echo "Level $level fiber: " . $result . "\n"; + } + + mysqli_stmt_close($stmt); + + // If we haven't reached max level, create nested fiber + if ($level < $max_level) { + $nested_fiber = new Fiber(function() use ($link, $level, $max_level) { + env_var_for_expects("GUID_NESTED_FIBER_CLOSURE_" . $level, newrelic_get_linking_metadata()['span.id'] ?? ''); + return execute_nested_query($link, $level + 1, $max_level); + }); + + // Start nested fiber + $nested_fiber->start(); + + // Resume nested fiber immediately if suspended + if ($nested_fiber->isSuspended()) { + $nested_fiber->resume(); + } + } + } +} + +// Create database connection +$link = mysqli_connect($MYSQL_HOST, $MYSQL_USER, $MYSQL_PASSWD, $MYSQL_DB, $MYSQL_PORT, $MYSQL_SOCKET); +if (mysqli_connect_errno()) { + echo mysqli_connect_error() . "\n"; + exit(1); +} + +// Create main fiber that starts the nested chain +$main_fiber = new Fiber(function() use ($link) { + env_var_for_expects("GUID_MAIN_FIBER", newrelic_get_linking_metadata()['span.id'] ?? ''); + return execute_nested_query($link, 1, 5); +}); + +// Start the main fiber +$main_fiber->start(); + +// Resume the main fiber chain - keep resuming until complete +while ($main_fiber->isSuspended()) { + $main_fiber->resume(); +} + +echo "Nested fiber query execution completed\n"; + +mysqli_close($link); diff --git a/tests/integration/mysqli/test_prepare_oo_fibers.php b/tests/integration/mysqli/test_prepare_oo_fibers.php new file mode 100644 index 000000000..a0fecfab8 --- /dev/null +++ b/tests/integration/mysqli/test_prepare_oo_fibers.php @@ -0,0 +1,260 @@ += 8.1 required\n"); +} +require("skipif.inc"); +*/ + +/*INI +newrelic.transaction_tracer.explain_enabled = false +newrelic.fibers.disabled = false +*/ + +/*EXPECT +ok - execute select 3*0 +ok - iteration 0 +Starting Func 'a' +ok - execute select 3*1 +ok - iteration 1 +Ending Func 'a' +ok - execute select 3*2 +ok - iteration 2 +ok - execute select 3*3 +ok - iteration 3 +ok - execute select 3*4 +ok - iteration 4 +ok - execute select 3*5 +ok - iteration 5 +ok - execute select 3*6 +ok - iteration 6 +ok - execute select 3*7 +ok - iteration 7 +ok - execute select 3*8 +ok - iteration 8 +ok - execute select 3*9 +ok - iteration 9 +ok - execute select 3*10 +ok - iteration 10 +ok - execute select 3*11 +ok - iteration 11 +ok - execute select 3*12 +ok - iteration 12 +ok - execute select 3*13 +ok - iteration 13 +ok - execute select 3*14 +ok - iteration 14 +ok - execute select 3*15 +ok - iteration 15 +*/ + +/*EXPECT_SPAN_EVENTS_LIKE +[ + [ + { + "traceId": "??", + "duration": "??", + "transactionId": "??", + "name": "OtherTransaction\/php__FILE__", + "guid": "ENV[GUID_ROOT]", + "type": "Span", + "category": "generic", + "priority": "??", + "sampled": true, + "nr.entryPoint": true, + "timestamp": "??", + "transaction.name": "OtherTransaction\/php__FILE__" + }, + {}, + {} + ], + [ + { + "category": "generic", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "Custom\/test_prepare_oo", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "ENV[GUID_ROOT]" + }, + {}, + {} + ], + [ + { + "category": "datastore", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "Datastore\/operation\/MySQL\/select", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "ENV[GUID_TEST_PREPARE]", + "span.kind": "client", + "component": "MySQL" + }, + {}, + { + "peer.address": "unknown:unknown", + "db.statement": "select ?*?" + } + ], + [ + { + "category": "datastore", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "Datastore\/operation\/MySQL\/select", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "ENV[GUID_TEST_PREPARE]", + "span.kind": "client", + "component": "MySQL" + }, + {}, + { + "peer.address": "unknown:unknown", + "db.statement": "select ?*?" + } + ], + [ + { + "category": "generic", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "Custom\/a", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "ENV[GUID_ROOT]" + }, + {}, + {} + ] +] +*/ + +/*EXPECT_METRICS +[ + "?? agent run id", + "?? start time", + "?? stop time", + [ + [{"name":"DurationByCaller/Unknown/Unknown/Unknown/Unknown/all"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"DurationByCaller/Unknown/Unknown/Unknown/Unknown/allOther"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/operation/MySQL/select"}, [16, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/operation/MySQL/select", + "scope":"OtherTransaction/php__FILE__"}, [16, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/all"}, [16, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/allOther"}, [16, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/MySQL/all"}, [16, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/MySQL/allOther"}, [16, "??", "??", "??", "??", "??"]], + [{"name":"OtherTransaction/all"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"OtherTransaction/php__FILE__"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"OtherTransactionTotalTime"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"OtherTransactionTotalTime/php__FILE__"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Supportability/Logging/Forwarding/PHP/enabled"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Supportability/Logging/Metrics/PHP/enabled"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Supportability/Logging/LocalDecorating/PHP/disabled"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Supportability/Logging/Labels/PHP/disabled"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Supportability/api/get_linking_metadata"}, ["??", "??", "??", "??", "??", "??"]], + [{"name":"Supportability/PHP/Fiber/used"}, ["??", "??", "??", "??", "??", "??"]] + ] +] +*/ + +require_once(realpath(dirname(__FILE__)) . '/../../include/helpers.php'); +require_once(realpath(dirname(__FILE__)) . '/../../include/tap.php'); +require_once(realpath(dirname(__FILE__)) . '/../../include/config.php'); + +function test_prepare_oo() +{ + global $MYSQL_HOST, $MYSQL_USER, $MYSQL_PASSWD, $MYSQL_DB, $MYSQL_PORT, $MYSQL_SOCKET; + + env_var_for_expects("GUID_TEST_PREPARE", newrelic_get_linking_metadata()['span.id'] ?? ''); + $N = 16; + $queries = array(); + $mysqlis = array(); + + for ($i = 0; $i < $N; $i++) { + /* + * Note: Each iteration of this loop creates a new connection. + */ + $query = "select 3*" . $i; + $queries[$i] = $query; + + $mysqlis[$i] = new mysqli ($MYSQL_HOST, $MYSQL_USER, $MYSQL_PASSWD, $MYSQL_DB, $MYSQL_PORT, $MYSQL_SOCKET); + if (mysqli_connect_errno ()) { + printf ("Connect failed: %s\n", mysqli_connect_error ()); + exit (); + } + + /* + * Note: Sometimes the prepare doesn't always work. + */ + if ($stmt = $mysqlis[$i]->prepare ($queries[$i])) { + $execute_result = $stmt->execute (); + tap_assert ($execute_result, sprintf ("execute %s", $queries[$i])); + if ($execute_result) { + $stmt->bind_result ($value); + $stmt->fetch (); + tap_equal (3*$i, $value, sprintf("iteration %2d", $i)); + } + } + + Fiber::suspend($N - $i); + + } + + for ($i = 0; $i < $N; $i++) { + $mysqlis[$i]->close (); + } +} + +function a() +{ + echo "Starting Func 'a'\n"; + env_var_for_expects("GUID_A", newrelic_get_linking_metadata()['span.id'] ?? ''); + time_nanosleep(0, 100000000); + Fiber::suspend(); + echo "Ending Func 'a'\n"; +} + +env_var_for_expects("GUID_ROOT", newrelic_get_linking_metadata()['span.id'] ?? ''); + +$fiber_a = new Fiber('a'); +$fiber_prepare = new Fiber('test_prepare_oo'); + +$fiber_prepare->start(); +$fiber_a->start(); +$fiber_prepare->resume(); +$fiber_a->resume(); + +do { + $result = $fiber_prepare->resume(); +} while ($result > 0); diff --git a/tests/integration/mysqli/test_query_oo_fibers.php b/tests/integration/mysqli/test_query_oo_fibers.php new file mode 100644 index 000000000..1e2358349 --- /dev/null +++ b/tests/integration/mysqli/test_query_oo_fibers.php @@ -0,0 +1,284 @@ +query() is synchronous, the entire PHP script blocks on that line until the database responds. +To achieve true, non-blocking asynchronous execution where the Fiber pauses while the database is processing, +you must use MySQL's asynchronous flag (MYSQLI_ASYNC) along with reap_async_query(). +*/ + +/*SKIPIF += 8.1 required\n"); +} +require("skipif.inc"); +*/ + +/*INI +newrelic.datastore_tracer.instance_reporting.enabled = 0 +newrelic.transaction_tracer.explain_enabled = false +newrelic.fibers.disabled = false +*/ + +/*EXPECT +ok - test_query1 (query) +ok - test_query1 (fetch) +Starting Func 'a' +ok - test_query2 (query) +ok - test_query2 (fetch) +Ending Func 'a' +*/ + +/*EXPECT_SPAN_EVENTS +[ + "?? agent run id", + { + "reservoir_size": 10000, + "events_seen": 7 + }, + [ + [ + { + "traceId": "??", + "duration": "??", + "transactionId": "??", + "name": "OtherTransaction\/php__FILE__", + "guid": "ENV[GUID_ROOT]", + "type": "Span", + "category": "generic", + "priority": "??", + "sampled": true, + "nr.entryPoint": true, + "timestamp": "??", + "transaction.name": "OtherTransaction\/php__FILE__" + }, + {}, + {} + ], + [ + { + "category": "generic", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "Custom\/test_mysqli", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "ENV[GUID_ROOT]" + }, + {}, + {} + ], + [ + { + "category": "generic", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "Custom\/test_query1", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "ENV[GUID_TEST_MYSQLI]" + }, + {}, + {} + ], + [ + { + "category": "datastore", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "Datastore\/statement\/MySQL\/tables\/select", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "ENV[GUID_TEST_MYSQLI_Q1]", + "span.kind": "client", + "component": "MySQL" + }, + {}, + { + "peer.address": "unknown:unknown", + "db.statement": "SELECT TABLE_NAME FROM information_schema.tables WHERE table_name=?" + } + ], + [ + { + "category": "generic", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "Custom\/test_query2", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "ENV[GUID_TEST_MYSQLI]" + }, + {}, + {} + ], + [ + { + "category": "datastore", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "Datastore\/statement\/MySQL\/tables\/select", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "ENV[GUID_TEST_MYSQLI_Q2]", + "span.kind": "client", + "component": "MySQL" + }, + {}, + { + "peer.address": "unknown:unknown", + "db.statement": "SELECT TABLE_NAME FROM information_schema.tables WHERE table_name=?" + } + ], + [ + { + "category": "generic", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "Custom\/a", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "ENV[GUID_ROOT]" + }, + {}, + {} + ] + ] +] +*/ + +/*EXPECT_METRICS +[ + "?? agent run id", + "?? start time", + "?? stop time", + [ + [{"name":"DurationByCaller/Unknown/Unknown/Unknown/Unknown/all"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"DurationByCaller/Unknown/Unknown/Unknown/Unknown/allOther"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/statement/MySQL/tables/select"}, [2, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/statement/MySQL/tables/select", + "scope":"OtherTransaction/php__FILE__"}, [2, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/operation/MySQL/select"}, [2, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/all"}, [2, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/allOther"}, [2, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/MySQL/all"}, [2, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/MySQL/allOther"}, [2, "??", "??", "??", "??", "??"]], + [{"name":"OtherTransaction/all"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"OtherTransaction/php__FILE__"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"OtherTransactionTotalTime"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"OtherTransactionTotalTime/php__FILE__"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Supportability/Logging/Forwarding/PHP/enabled"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Supportability/Logging/Metrics/PHP/enabled"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Supportability/Logging/LocalDecorating/PHP/disabled"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Supportability/Logging/Labels/PHP/disabled"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Supportability/api/get_linking_metadata"}, ["??", "??", "??", "??", "??", "??"]], + [{"name":"Supportability/PHP/Fiber/used"}, ["??", "??", "??", "??", "??", "??"]] + ] +] +*/ + +require_once(realpath(dirname(__FILE__)) . '/../../include/helpers.php'); +require_once(realpath(dirname(__FILE__)) . '/../../include/tap.php'); +require_once(realpath(dirname(__FILE__)) . '/../../include/config.php'); + +function test_query1($link) +{ + env_var_for_expects("GUID_TEST_MYSQLI_Q1", newrelic_get_linking_metadata()['span.id'] ?? ''); + $query = "SELECT TABLE_NAME FROM information_schema.tables WHERE table_name='STATISTICS'"; + $result = $link->query($query); + tap_not_equal(FALSE, $result, "test_query1 (query)"); + + $expected = array(0 => "STATISTICS", "TABLE_NAME" => "STATISTICS"); + + if (FALSE !== $result) { + while ($row = $result->fetch_array()) { + tap_equal($expected, $row, "test_query1 (fetch)"); + } + $result->close(); + } +} + +function test_query2($link) +{ + env_var_for_expects("GUID_TEST_MYSQLI_Q2", newrelic_get_linking_metadata()['span.id'] ?? ''); + $query = "SELECT TABLE_NAME FROM information_schema.tables WHERE table_name='STATISTICS'"; + $result = $link->query($query, MYSQLI_USE_RESULT); + tap_not_equal(FALSE, $result, "test_query2 (query)"); + + $expected = array(0 => "STATISTICS", "TABLE_NAME" => "STATISTICS"); + Fiber::suspend(); + if (FALSE !== $result) { + while ($row = $result->fetch_array()) { + tap_equal($expected, $row, "test_query2 (fetch)"); + } + $result->close(); + } +} + +function test_mysqli() +{ + global $MYSQL_HOST, $MYSQL_USER, $MYSQL_PASSWD, $MYSQL_DB, $MYSQL_PORT, $MYSQL_SOCKET; + + env_var_for_expects("GUID_TEST_MYSQLI", newrelic_get_linking_metadata()['span.id'] ?? ''); + $link = new mysqli($MYSQL_HOST, $MYSQL_USER, $MYSQL_PASSWD, $MYSQL_DB, $MYSQL_PORT, $MYSQL_SOCKET); + if ($link->connect_errno) { + echo $link->connect_error . "\n"; + exit(1); + } + + test_query1($link); + $fiber_q2 = new Fiber('test_query2'); + Fiber::suspend(); + $fiber_q2->start($link); + $fiber_q2->resume(); + $link->close(); +} + +function a() +{ + echo "Starting Func 'a'\n"; + env_var_for_expects("GUID_A", newrelic_get_linking_metadata()['span.id'] ?? ''); + time_nanosleep(0, 100000000); + Fiber::suspend(); + echo "Ending Func 'a'\n"; +} + +env_var_for_expects("GUID_ROOT", newrelic_get_linking_metadata()['span.id'] ?? ''); + +$fiber_a = new Fiber('a'); +$fiber_mysqli = new Fiber('test_mysqli'); + +$fiber_mysqli->start(); +$fiber_a->start(); +$fiber_mysqli->resume(); +$fiber_a->resume(); diff --git a/tests/integration/pdo/mysql/base-class/test_instance_reporting_port_fibers.php b/tests/integration/pdo/mysql/base-class/test_instance_reporting_port_fibers.php new file mode 100644 index 000000000..8b1397ced --- /dev/null +++ b/tests/integration/pdo/mysql/base-class/test_instance_reporting_port_fibers.php @@ -0,0 +1,245 @@ +", + "?? SQL id", + "DROP TABLE ENV[DATASTORE_COLLECTION];", + "Datastore/statement/ENV[DATASTORE_PRODUCT]/ENV[DATASTORE_COLLECTION]/drop", + 1, + "?? total time", + "?? min time", + "?? max time", + { + "backtrace": [ + "/ in PDO::exec called at .*\/", + " in test_instance_reporting called at ? (?)", + " in Fiber::resume called at __FILE__ (??)" + ], + "host": "ENV[MYSQL_HOST]", + "port_path_or_id": "ENV[MYSQL_PORT]", + "database_name": "ENV[MYSQL_DB]" + } + ], + [ + "OtherTransaction/php__FILE__", + "", + "?? SQL id", + "CREATE TABLE ENV[DATASTORE_COLLECTION] (id INT, description VARCHAR(?));", + "Datastore/statement/ENV[DATASTORE_PRODUCT]/ENV[DATASTORE_COLLECTION]/create", + 1, + "?? total time", + "?? min time", + "?? max time", + { + "backtrace": [ + "/ in PDO::exec called at .*\/", + " in test_instance_reporting called at ? (?)", + " in Fiber::resume called at __FILE__ (??)" + ], + "host": "ENV[MYSQL_HOST]", + "port_path_or_id": "ENV[MYSQL_PORT]", + "database_name": "ENV[MYSQL_DB]" + } + ] + ] +] +*/ + +require_once(realpath(dirname(__FILE__)) . '/../../../../include/helpers.php'); +require_once(realpath(dirname(__FILE__)) . '/../../../../include/tap.php'); +require_once(realpath(dirname(__FILE__)) . '/../../../../include/config.php'); +require_once(realpath(dirname(__FILE__)) . '/../../test_instance_reporting.inc'); + +function a() +{ + echo "Starting Func 'a'\n"; + env_var_for_expects("GUID_A", newrelic_get_linking_metadata()['span.id'] ?? ''); + time_nanosleep(0, 100000000); + Fiber::suspend(); + echo "Ending Func 'a'\n"; +} + +env_var_for_expects("GUID_ROOT", newrelic_get_linking_metadata()['span.id'] ?? ''); + +$fiber_a = new Fiber('a'); +$fiber_pdo = new Fiber('test_instance_reporting'); + +$fiber_pdo->start(new PDO($PDO_MYSQL_DSN, $MYSQL_USER, $MYSQL_PASSWD), 0, true); +$fiber_a->start(); +$fiber_pdo->resume(); +$fiber_a->resume(); +$fiber_pdo->resume(); \ No newline at end of file diff --git a/tests/integration/pdo/mysql/base-class/test_instance_reporting_socket_fibers.php b/tests/integration/pdo/mysql/base-class/test_instance_reporting_socket_fibers.php new file mode 100644 index 000000000..d5d245826 --- /dev/null +++ b/tests/integration/pdo/mysql/base-class/test_instance_reporting_socket_fibers.php @@ -0,0 +1,248 @@ +", + "?? SQL id", + "DROP TABLE ENV[DATASTORE_COLLECTION];", + "Datastore/statement/ENV[DATASTORE_PRODUCT]/ENV[DATASTORE_COLLECTION]/drop", + 1, + "?? total time", + "?? min time", + "?? max time", + { + "backtrace": [ + "/ in PDO::exec called at .*\/", + " in test_instance_reporting called at ? (?)", + " in Fiber::resume called at __FILE__ (??)" + ], + "host": "__HOST__", + "port_path_or_id": "ENV[MYSQL_SOCKET]", + "database_name": "ENV[MYSQL_DB]" + } + ], + [ + "OtherTransaction/php__FILE__", + "", + "?? SQL id", + "CREATE TABLE ENV[DATASTORE_COLLECTION] (id INT, description VARCHAR(?));", + "Datastore/statement/ENV[DATASTORE_PRODUCT]/ENV[DATASTORE_COLLECTION]/create", + 1, + "?? total time", + "?? min time", + "?? max time", + { + "backtrace": [ + "/ in PDO::exec called at .*\/", + " in test_instance_reporting called at ? (?)", + " in Fiber::resume called at __FILE__ (??)" + ], + "host": "__HOST__", + "port_path_or_id": "ENV[MYSQL_SOCKET]", + "database_name": "ENV[MYSQL_DB]" + } + ] + ] +] +*/ + +require_once(realpath(dirname(__FILE__)) . '/../../../../include/helpers.php'); +require_once(realpath(dirname(__FILE__)) . '/../../../../include/tap.php'); +require_once(realpath(dirname(__FILE__)) . '/../../../../include/config.php'); +require_once(realpath(dirname(__FILE__)) . '/../../test_instance_reporting.inc'); + +function a() +{ + echo "Starting Func 'a'\n"; + env_var_for_expects("GUID_A", newrelic_get_linking_metadata()['span.id'] ?? ''); + time_nanosleep(0, 100000000); + Fiber::suspend(); + echo "Ending Func 'a'\n"; +} + +env_var_for_expects("GUID_ROOT", newrelic_get_linking_metadata()['span.id'] ?? ''); + +$DSN = 'mysql:'; +$DSN .= 'unix_socket=' . $MYSQL_SOCKET . ';'; +$DSN .= 'dbname=' . $MYSQL_DB . ';'; + +$fiber_a = new Fiber('a'); +$fiber_pdo = new Fiber('test_instance_reporting'); + +$fiber_pdo->start(new PDO($DSN, $MYSQL_USER, $MYSQL_PASSWD), 0, true); +$fiber_a->start(); +$fiber_pdo->resume(); +$fiber_a->resume(); +$fiber_pdo->resume(); \ No newline at end of file diff --git a/tests/integration/pdo/mysql/base-class/test_prepared_stmt_basic_fibers.php b/tests/integration/pdo/mysql/base-class/test_prepared_stmt_basic_fibers.php new file mode 100644 index 000000000..a0c615507 --- /dev/null +++ b/tests/integration/pdo/mysql/base-class/test_prepared_stmt_basic_fibers.php @@ -0,0 +1,195 @@ +", + "?? SQL id", + "select * from information_schema.tables limit ?;", + "Datastore/statement/ENV[DATASTORE_PRODUCT]/ENV[DATASTORE_COLLECTION]/select", + 1, + "?? total time", + "?? min time", + "?? max time", + { + "backtrace": [ + "/ in PDOStatement::execute called at .*\/", + " in test_prepared_stmt called at ? (?)", + " in Fiber::resume called at __FILE__ (??)" + ], + "explain_plan": "??" + } + ] + ] +] +*/ + +require_once(realpath(dirname(__FILE__)) . '/../../../../include/helpers.php'); +require_once(realpath(dirname(__FILE__)) . '/../../../../include/tap.php'); +require_once(realpath(dirname(__FILE__)) . '/../../../../include/config.php'); +require_once(realpath(dirname(__FILE__)) . '/../../test_prepared_stmt_basic.inc'); + +function a() +{ + echo "Starting Func 'a'\n"; + env_var_for_expects("GUID_A", newrelic_get_linking_metadata()['span.id'] ?? ''); + time_nanosleep(0, 100000000); + Fiber::suspend(); + echo "Ending Func 'a'\n"; +} + +env_var_for_expects("GUID_ROOT", newrelic_get_linking_metadata()['span.id'] ?? ''); + +$query = 'select * from information_schema.tables limit 1;'; +$fiber_a = new Fiber('a'); +$fiber_pdo = new Fiber('test_prepared_stmt'); + +$fiber_pdo->start(new PDO($PDO_MYSQL_DSN, $MYSQL_USER, $MYSQL_PASSWD), $query, true); +$fiber_a->start(); +$fiber_pdo->resume(); +$fiber_a->resume(); +$fiber_pdo->resume(); \ No newline at end of file diff --git a/tests/integration/pdo/mysql/base-class/test_prepared_stmt_bind_value_fibers.php b/tests/integration/pdo/mysql/base-class/test_prepared_stmt_bind_value_fibers.php new file mode 100644 index 000000000..9091acf0f --- /dev/null +++ b/tests/integration/pdo/mysql/base-class/test_prepared_stmt_bind_value_fibers.php @@ -0,0 +1,200 @@ +", + "?? SQL id", + "select * from information_schema.tables where table_name = ? limit ?;", + "Datastore/statement/ENV[DATASTORE_PRODUCT]/ENV[DATASTORE_COLLECTION]/select", + 1, + "?? total time", + "?? min time", + "?? max time", + { + "backtrace": [ + "/ in PDOStatement::execute called at .*\/", + " in test_prepared_stmt called at ? (?)", + " in Fiber::resume called at __FILE__ (??)" + ], + "explain_plan": "??" + } + ] + ] +] +*/ + +require_once(realpath(dirname(__FILE__)) . '/../../../../include/helpers.php'); +require_once(realpath(dirname(__FILE__)) . '/../../../../include/tap.php'); +require_once(realpath(dirname(__FILE__)) . '/../../../../include/config.php'); +require_once(realpath(dirname(__FILE__)) . '/../../test_prepared_stmt_bind_value.inc'); + +function a() +{ + echo "Starting Func 'a'\n"; + env_var_for_expects("GUID_A", newrelic_get_linking_metadata()['span.id'] ?? ''); + time_nanosleep(0, 100000000); + Fiber::suspend(); + echo "Ending Func 'a'\n"; +} + +env_var_for_expects("GUID_ROOT", newrelic_get_linking_metadata()['span.id'] ?? ''); + +require_once(realpath (dirname ( __FILE__ )) . '/../../test_prepared_stmt_bind_value.inc'); +require_once(realpath (dirname ( __FILE__ )) . '/../../../../include/config.php'); + +$query = 'select * from information_schema.tables where table_name = ? limit 1;'; + +$fiber_a = new Fiber('a'); +$fiber_pdo = new Fiber('test_prepared_stmt'); + +$fiber_pdo->start(new PDO($PDO_MYSQL_DSN, $MYSQL_USER, $MYSQL_PASSWD), $query, true); +$fiber_a->start(); +$fiber_pdo->resume(); +$fiber_a->resume(); +$fiber_pdo->resume(); diff --git a/tests/integration/pdo/mysql/base-class/test_query_1_arg_fibers.php b/tests/integration/pdo/mysql/base-class/test_query_1_arg_fibers.php new file mode 100644 index 000000000..c9a894db2 --- /dev/null +++ b/tests/integration/pdo/mysql/base-class/test_query_1_arg_fibers.php @@ -0,0 +1,277 @@ +start(new PDO($PDO_MYSQL_DSN, $MYSQL_USER, $MYSQL_PASSWD), 0, true); +$fiber_a->start(); +$fiber_pdo->resume(); +$fiber_a->resume(); +$fiber_pdo->resume(); \ No newline at end of file diff --git a/tests/integration/pdo/mysql/base-class/test_query_fetch_class_fibers.php b/tests/integration/pdo/mysql/base-class/test_query_fetch_class_fibers.php new file mode 100644 index 000000000..56e869b2c --- /dev/null +++ b/tests/integration/pdo/mysql/base-class/test_query_fetch_class_fibers.php @@ -0,0 +1,229 @@ +start(new PDO($PDO_MYSQL_DSN, $MYSQL_USER, $MYSQL_PASSWD), 0, true); +$fiber_a->start(); +$fiber_pdo->resume(); +$fiber_a->resume(); +$fiber_pdo->resume(); diff --git a/tests/integration/pdo/mysql/base-class/test_query_fetch_column_fibers.php b/tests/integration/pdo/mysql/base-class/test_query_fetch_column_fibers.php new file mode 100644 index 000000000..cbfd85f20 --- /dev/null +++ b/tests/integration/pdo/mysql/base-class/test_query_fetch_column_fibers.php @@ -0,0 +1,277 @@ +start(new PDO($PDO_MYSQL_DSN, $MYSQL_USER, $MYSQL_PASSWD), 0, true); +$fiber_a->start(); +$fiber_pdo->resume(); +$fiber_a->resume(); +$fiber_pdo->resume(); diff --git a/tests/integration/pdo/mysql/base-class/test_query_fetch_into_fibers.php b/tests/integration/pdo/mysql/base-class/test_query_fetch_into_fibers.php new file mode 100644 index 000000000..4bbc5e7de --- /dev/null +++ b/tests/integration/pdo/mysql/base-class/test_query_fetch_into_fibers.php @@ -0,0 +1,231 @@ +start(new PDO($PDO_MYSQL_DSN, $MYSQL_USER, $MYSQL_PASSWD), 0, true); +$fiber_a->start(); +$fiber_pdo->resume(); +$fiber_a->resume(); +$fiber_pdo->resume(); diff --git a/tests/integration/pdo/pgsql/base-class/test_instance_reporting_port_fibers.php b/tests/integration/pdo/pgsql/base-class/test_instance_reporting_port_fibers.php new file mode 100644 index 000000000..4569e7f48 --- /dev/null +++ b/tests/integration/pdo/pgsql/base-class/test_instance_reporting_port_fibers.php @@ -0,0 +1,244 @@ +", + "?? SQL id", + "DROP TABLE ENV[DATASTORE_COLLECTION];", + "Datastore/statement/ENV[DATASTORE_PRODUCT]/ENV[DATASTORE_COLLECTION]/drop", + 1, + "?? total time", + "?? min time", + "?? max time", + { + "backtrace": [ + "/ in PDO::exec called at .*\/", + " in test_instance_reporting called at ? (?)", + " in Fiber::resume called at __FILE__ (??)" + ], + "host": "ENV[PG_HOST]", + "port_path_or_id": "ENV[PG_PORT]", + "database_name": "postgres" + } + ], + [ + "OtherTransaction/php__FILE__", + "", + "?? SQL id", + "CREATE TABLE ENV[DATASTORE_COLLECTION] (id INT, description VARCHAR(?));", + "Datastore/statement/ENV[DATASTORE_PRODUCT]/ENV[DATASTORE_COLLECTION]/create", + 1, + "?? total time", + "?? min time", + "?? max time", + { + "backtrace": [ + "/ in PDO::exec called at .*\/", + " in test_instance_reporting called at ? (?)", + " in Fiber::resume called at __FILE__ (??)" + ], + "host": "ENV[PG_HOST]", + "port_path_or_id": "ENV[PG_PORT]", + "database_name": "postgres" + } + ] + ] +] +*/ + +require_once(realpath(dirname(__FILE__)) . '/../../../../include/helpers.php'); +require_once(realpath(dirname(__FILE__)) . '/../../../../include/tap.php'); +require_once(realpath(dirname(__FILE__)) . '/../../../../include/config.php'); +require_once(realpath(dirname(__FILE__)) . '/../../test_instance_reporting.inc'); + +function a() +{ + echo "Starting Func 'a'\n"; + env_var_for_expects("GUID_A", newrelic_get_linking_metadata()['span.id'] ?? ''); + time_nanosleep(0, 100000000); + Fiber::suspend(); + echo "Ending Func 'a'\n"; +} + +env_var_for_expects("GUID_ROOT", newrelic_get_linking_metadata()['span.id'] ?? ''); + +$fiber_a = new Fiber('a'); +$fiber_pdo = new Fiber('test_instance_reporting'); + +$fiber_pdo->start(new PDO($PDO_PGSQL_DSN, $PG_USER, $PG_PW), 0, true); +$fiber_a->start(); +$fiber_pdo->resume(); +$fiber_a->resume(); +$fiber_pdo->resume(); \ No newline at end of file diff --git a/tests/integration/pdo/pgsql/base-class/test_prepared_stmt_basic_fibers.php b/tests/integration/pdo/pgsql/base-class/test_prepared_stmt_basic_fibers.php new file mode 100644 index 000000000..cea627d06 --- /dev/null +++ b/tests/integration/pdo/pgsql/base-class/test_prepared_stmt_basic_fibers.php @@ -0,0 +1,191 @@ +", + "?? SQL id", + "select * from information_schema.tables limit ?;", + "Datastore/statement/ENV[DATASTORE_PRODUCT]/ENV[DATASTORE_COLLECTION]/select", + 1, + "?? total time", + "?? min time", + "?? max time", + { + "backtrace": [ + "/ in PDOStatement::execute called at .*\/", + " in test_prepared_stmt called at ? (?)", + " in Fiber::resume called at __FILE__ (??)" + ] + } + ] + ] +] +*/ + +require_once(realpath(dirname(__FILE__)) . '/../../../../include/helpers.php'); +require_once(realpath(dirname(__FILE__)) . '/../../../../include/tap.php'); +require_once(realpath(dirname(__FILE__)) . '/../../../../include/config.php'); +require_once(realpath(dirname(__FILE__)) . '/../../test_prepared_stmt_basic.inc'); + +function a() +{ + echo "Starting Func 'a'\n"; + env_var_for_expects("GUID_A", newrelic_get_linking_metadata()['span.id'] ?? ''); + time_nanosleep(0, 100000000); + Fiber::suspend(); + echo "Ending Func 'a'\n"; +} + +env_var_for_expects("GUID_ROOT", newrelic_get_linking_metadata()['span.id'] ?? ''); + +$query = 'select * from information_schema.tables limit 1;'; +$fiber_a = new Fiber('a'); +$fiber_pdo = new Fiber('test_prepared_stmt'); + +$fiber_pdo->start(new PDO($PDO_PGSQL_DSN, $PG_USER, $PG_PW), $query, true); +$fiber_a->start(); +$fiber_pdo->resume(); +$fiber_a->resume(); +$fiber_pdo->resume(); \ No newline at end of file diff --git a/tests/integration/pdo/pgsql/base-class/test_prepared_stmt_bind_value_fibers.php b/tests/integration/pdo/pgsql/base-class/test_prepared_stmt_bind_value_fibers.php new file mode 100644 index 000000000..61d698e0b --- /dev/null +++ b/tests/integration/pdo/pgsql/base-class/test_prepared_stmt_bind_value_fibers.php @@ -0,0 +1,192 @@ +", + "?? SQL id", + "select * from information_schema.tables where table_name = ? limit ?;", + "Datastore/statement/ENV[DATASTORE_PRODUCT]/ENV[DATASTORE_COLLECTION]/select", + 1, + "?? total time", + "?? min time", + "?? max time", + { + "backtrace": [ + "/ in PDOStatement::execute called at .*\/", + " in test_prepared_stmt called at ? (?)", + " in Fiber::resume called at __FILE__ (??)" + ] + } + ] + ] +] +*/ + +require_once(realpath(dirname(__FILE__)) . '/../../../../include/helpers.php'); +require_once(realpath(dirname(__FILE__)) . '/../../../../include/tap.php'); +require_once(realpath(dirname(__FILE__)) . '/../../../../include/config.php'); +require_once(realpath(dirname(__FILE__)) . '/../../test_prepared_stmt_bind_value.inc'); + +function a() +{ + echo "Starting Func 'a'\n"; + env_var_for_expects("GUID_A", newrelic_get_linking_metadata()['span.id'] ?? ''); + time_nanosleep(0, 100000000); + Fiber::suspend(); + echo "Ending Func 'a'\n"; +} + +env_var_for_expects("GUID_ROOT", newrelic_get_linking_metadata()['span.id'] ?? ''); + +$query = 'select * from information_schema.tables where table_name = ? limit 1;'; +$fiber_a = new Fiber('a'); +$fiber_pdo = new Fiber('test_prepared_stmt'); + +$fiber_pdo->start(new PDO($PDO_PGSQL_DSN, $PG_USER, $PG_PW), $query, true); +$fiber_a->start(); +$fiber_pdo->resume(); +$fiber_a->resume(); +$fiber_pdo->resume(); diff --git a/tests/integration/pdo/pgsql/base-class/test_query_1_arg_fibers.php b/tests/integration/pdo/pgsql/base-class/test_query_1_arg_fibers.php new file mode 100644 index 000000000..0eed1a767 --- /dev/null +++ b/tests/integration/pdo/pgsql/base-class/test_query_1_arg_fibers.php @@ -0,0 +1,277 @@ +start(new PDO($PDO_PGSQL_DSN, $PG_USER, $PG_PW), 0, true); +$fiber_a->start(); +$fiber_pdo->resume(); +$fiber_a->resume(); +$fiber_pdo->resume(); diff --git a/tests/integration/pdo/pgsql/base-class/test_query_fetch_class_fibers.php b/tests/integration/pdo/pgsql/base-class/test_query_fetch_class_fibers.php new file mode 100644 index 000000000..cd8e3252a --- /dev/null +++ b/tests/integration/pdo/pgsql/base-class/test_query_fetch_class_fibers.php @@ -0,0 +1,229 @@ +start(new PDO($PDO_PGSQL_DSN, $PG_USER, $PG_PW), 0, true); +$fiber_a->start(); +$fiber_pdo->resume(); +$fiber_a->resume(); +$fiber_pdo->resume(); diff --git a/tests/integration/pdo/pgsql/base-class/test_query_fetch_column_fibers.php b/tests/integration/pdo/pgsql/base-class/test_query_fetch_column_fibers.php new file mode 100644 index 000000000..52f98d98b --- /dev/null +++ b/tests/integration/pdo/pgsql/base-class/test_query_fetch_column_fibers.php @@ -0,0 +1,277 @@ +start(new PDO($PDO_PGSQL_DSN, $PG_USER, $PG_PW), 0, true); +$fiber_a->start(); +$fiber_pdo->resume(); +$fiber_a->resume(); +$fiber_pdo->resume(); diff --git a/tests/integration/pdo/pgsql/base-class/test_query_fetch_into_fibers.php b/tests/integration/pdo/pgsql/base-class/test_query_fetch_into_fibers.php new file mode 100644 index 000000000..e4a577c5b --- /dev/null +++ b/tests/integration/pdo/pgsql/base-class/test_query_fetch_into_fibers.php @@ -0,0 +1,231 @@ +start(new PDO($PDO_PGSQL_DSN, $PG_USER, $PG_PW), 0, true); +$fiber_a->start(); +$fiber_pdo->resume(); +$fiber_a->resume(); +$fiber_pdo->resume(); diff --git a/tests/integration/pdo/sqlite/base-class/test_prepared_stmt_basic_fibers.php b/tests/integration/pdo/sqlite/base-class/test_prepared_stmt_basic_fibers.php new file mode 100644 index 000000000..a48362fb6 --- /dev/null +++ b/tests/integration/pdo/sqlite/base-class/test_prepared_stmt_basic_fibers.php @@ -0,0 +1,190 @@ +", + "?? SQL id", + "select * from sqlite_master limit ?;", + "Datastore/statement/ENV[DATASTORE_PRODUCT]/ENV[DATASTORE_COLLECTION]/select", + 1, + "?? total time", + "?? min time", + "?? max time", + { + "backtrace": [ + "/ in PDOStatement::execute called at .*\/", + " in test_prepared_stmt called at ? (?)", + " in Fiber::resume called at __FILE__ (??)" + ] + } + ] + ] +] +*/ + +require_once(realpath(dirname(__FILE__)) . '/../../../../include/helpers.php'); +require_once(realpath(dirname(__FILE__)) . '/../../../../include/tap.php'); +require_once(realpath(dirname(__FILE__)) . '/../../test_prepared_stmt_basic.inc'); + +function a() +{ + echo "Starting Func 'a'\n"; + env_var_for_expects("GUID_A", newrelic_get_linking_metadata()['span.id'] ?? ''); + time_nanosleep(0, 100000000); + Fiber::suspend(); + echo "Ending Func 'a'\n"; +} + +env_var_for_expects("GUID_ROOT", newrelic_get_linking_metadata()['span.id'] ?? ''); + +$query = 'select * from sqlite_master limit 1;'; +$fiber_a = new Fiber('a'); +$fiber_pdo = new Fiber('test_prepared_stmt'); + +$fiber_pdo->start(new PDO('sqlite::memory:'), $query, is_fiber:true); +$fiber_a->start(); +$fiber_pdo->resume(); +$fiber_a->resume(); +$fiber_pdo->resume(); diff --git a/tests/integration/pdo/sqlite/base-class/test_prepared_stmt_bind_value_fibers.php b/tests/integration/pdo/sqlite/base-class/test_prepared_stmt_bind_value_fibers.php new file mode 100644 index 000000000..ab4a80263 --- /dev/null +++ b/tests/integration/pdo/sqlite/base-class/test_prepared_stmt_bind_value_fibers.php @@ -0,0 +1,193 @@ +", + "?? SQL id", + "select * from ENV[DATASTORE_COLLECTION] where tbl_name = ? limit ?;", + "Datastore/statement/ENV[DATASTORE_PRODUCT]/ENV[DATASTORE_COLLECTION]/select", + 1, + "?? total time", + "?? min time", + "?? max time", + { + "backtrace": [ + "/ in PDOStatement::execute called at .*\/", + " in test_prepared_stmt called at ? (?)", + " in Fiber::resume called at __FILE__ (??)" + ] + } + ] + ] +] +*/ + +require_once(realpath(dirname(__FILE__)) . '/../../../../include/helpers.php'); +require_once(realpath (dirname ( __FILE__ )) . '/../../test_prepared_stmt_bind_value.inc'); +require_once(realpath (dirname ( __FILE__ )) . '/../../../../include/config.php'); + +function a() +{ + echo "Starting Func 'a'\n"; + env_var_for_expects("GUID_A", newrelic_get_linking_metadata()['span.id'] ?? ''); + time_nanosleep(0, 100000000); + Fiber::suspend(); + echo "Ending Func 'a'\n"; +} + +env_var_for_expects("GUID_ROOT", newrelic_get_linking_metadata()['span.id'] ?? ''); + +$query = 'select * from sqlite_master where tbl_name = ? limit 1;'; +$fiber_a = new Fiber('a'); +$fiber_pdo = new Fiber('test_prepared_stmt'); + +$fiber_pdo->start(new PDO('sqlite::memory:'), $query, is_fiber:true); +$fiber_a->start(); +$fiber_pdo->resume(); +$fiber_a->resume(); +$fiber_pdo->resume(); diff --git a/tests/integration/pdo/sqlite/base-class/test_query_1_arg_fibers.php b/tests/integration/pdo/sqlite/base-class/test_query_1_arg_fibers.php new file mode 100644 index 000000000..9ccffd811 --- /dev/null +++ b/tests/integration/pdo/sqlite/base-class/test_query_1_arg_fibers.php @@ -0,0 +1,277 @@ +start(new PDO('sqlite::memory:'), is_fiber:true); +$fiber_a->start(); +$fiber_pdo->resume(); +$fiber_a->resume(); +$fiber_pdo->resume(); diff --git a/tests/integration/pdo/sqlite/base-class/test_query_fetch_class_fibers.php b/tests/integration/pdo/sqlite/base-class/test_query_fetch_class_fibers.php new file mode 100644 index 000000000..5f7f0a66b --- /dev/null +++ b/tests/integration/pdo/sqlite/base-class/test_query_fetch_class_fibers.php @@ -0,0 +1,228 @@ +start(new PDO('sqlite::memory:'), is_fiber:true); +$fiber_a->start(); +$fiber_pdo->resume(); +$fiber_a->resume(); +$fiber_pdo->resume(); diff --git a/tests/integration/pdo/sqlite/base-class/test_query_fetch_column_fibers.php b/tests/integration/pdo/sqlite/base-class/test_query_fetch_column_fibers.php new file mode 100644 index 000000000..b922b56a4 --- /dev/null +++ b/tests/integration/pdo/sqlite/base-class/test_query_fetch_column_fibers.php @@ -0,0 +1,277 @@ +start(new PDO('sqlite::memory:'), is_fiber:true); +$fiber_a->start(); +$fiber_pdo->resume(); +$fiber_a->resume(); +$fiber_pdo->resume(); diff --git a/tests/integration/pdo/sqlite/base-class/test_query_fetch_into_fibers.php b/tests/integration/pdo/sqlite/base-class/test_query_fetch_into_fibers.php new file mode 100644 index 000000000..d2860fca5 --- /dev/null +++ b/tests/integration/pdo/sqlite/base-class/test_query_fetch_into_fibers.php @@ -0,0 +1,231 @@ +start(new PDO('sqlite::memory:'), is_fiber:true); +$fiber_a->start(); +$fiber_pdo->resume(); +$fiber_a->resume(); +$fiber_pdo->resume(); diff --git a/tests/integration/pdo/test_instance_reporting.inc b/tests/integration/pdo/test_instance_reporting.inc index 0ae7fb6a8..84f993aec 100644 --- a/tests/integration/pdo/test_instance_reporting.inc +++ b/tests/integration/pdo/test_instance_reporting.inc @@ -23,11 +23,25 @@ - ./base-class - ./constructor - ./factory + + NOTE: The function here can be used as a fiber. + The caller will need 2 fiber resumes to fully exercise the function. */ require_once(dirname(__FILE__).'/../../include/tap.php'); +require_once(dirname(__FILE__).'/../../include/helpers.php'); + +function test_instance_reporting($conn, $want_drop_table_result=1, $is_fiber=false) { + if ($is_fiber) { + env_var_for_expects("GUID_TEST_INSTANCE_REPORTING", newrelic_get_linking_metadata()['span.id'] ?? ''); + } -function test_instance_reporting($conn, $want_drop_table_result=1) { + if ($is_fiber) { + Fiber::suspend(); + } tap_equal(0, $conn->exec("CREATE TABLE test (id INT, description VARCHAR(10));"), 'create table'); + if ($is_fiber) { + Fiber::suspend(); + } tap_equal($want_drop_table_result, $conn->exec("DROP TABLE test;"), 'drop table'); } diff --git a/tests/integration/pdo/test_prepared_stmt_basic.inc b/tests/integration/pdo/test_prepared_stmt_basic.inc index 4db6fd87b..9e9d280f3 100644 --- a/tests/integration/pdo/test_prepared_stmt_basic.inc +++ b/tests/integration/pdo/test_prepared_stmt_basic.inc @@ -21,11 +21,25 @@ - ./base-class - ./constructor - ./factory + + NOTE: The function here can be used as a fiber. + The caller will need 2 fiber resumes to fully exercise the function. */ require_once(dirname(__FILE__).'/../../include/tap.php'); +require_once(dirname(__FILE__).'/../../include/helpers.php'); -function test_prepared_stmt($conn, $query) { +function test_prepared_stmt($conn, $query, $is_fiber=false) { + if ($is_fiber) { + env_var_for_expects("GUID_TEST_PREPARED_STMT", newrelic_get_linking_metadata()['span.id'] ?? ''); + } + + if ($is_fiber) { + Fiber::suspend(); + } $stmt = $conn->prepare($query); + if ($is_fiber) { + Fiber::suspend(); + } tap_assert($stmt->execute(), 'execute prepared statement'); } diff --git a/tests/integration/pdo/test_prepared_stmt_bind_value.inc b/tests/integration/pdo/test_prepared_stmt_bind_value.inc index 6ba2ffd82..965d833b3 100644 --- a/tests/integration/pdo/test_prepared_stmt_bind_value.inc +++ b/tests/integration/pdo/test_prepared_stmt_bind_value.inc @@ -21,12 +21,26 @@ - ./base-class - ./constructor - ./factory + + NOTE: The function here can be used as a fiber. + The caller will need 2 fiber resumes to fully exercise the function. */ require_once(dirname(__FILE__).'/../../include/tap.php'); +require_once(dirname(__FILE__).'/../../include/helpers.php'); -function test_prepared_stmt($conn, $query) { +function test_prepared_stmt($conn, $query, $is_fiber=false) { + if ($is_fiber) { + env_var_for_expects("GUID_TEST_PREPARED_STMT", newrelic_get_linking_metadata()['span.id'] ?? ''); + } + $stmt = $conn->prepare($query); + if ($is_fiber) { + Fiber::suspend(); + } $stmt->bindValue(1, "missing"); + if ($is_fiber) { + Fiber::suspend(); + } tap_assert($stmt->execute(), 'execute prepared statement'); } diff --git a/tests/integration/pdo/test_query_1_arg.inc b/tests/integration/pdo/test_query_1_arg.inc index 104405786..b3a84aa3d 100644 --- a/tests/integration/pdo/test_query_1_arg.inc +++ b/tests/integration/pdo/test_query_1_arg.inc @@ -21,17 +21,29 @@ - ./constructor - ./factory + NOTE: The function here can be used as a fiber. + The caller will need 2 fiber resumes to fully exercise the function. */ require_once(dirname(__FILE__).'/../../include/tap.php'); +require_once(dirname(__FILE__).'/../../include/helpers.php'); + +function test_pdo_query($conn, $want_drop_table_result=1, $is_fiber=false) { + + if ($is_fiber) { + env_var_for_expects("GUID_TEST_PDO_QUERY", newrelic_get_linking_metadata()['span.id'] ?? ''); + } -function test_pdo_query($conn, $want_drop_table_result=1) { tap_equal(0, $conn->exec("CREATE TABLE test (id INT, description VARCHAR(10));"), 'create table'); tap_equal(1, $conn->exec("INSERT INTO test VALUES (1, 'one');"), 'insert one'); tap_equal(1, $conn->exec("INSERT INTO test VALUES (2, 'two');"), 'insert two'); tap_equal(1, $conn->exec("INSERT INTO test VALUES (3, 'three');"), 'insert three'); + if ($is_fiber) { + Fiber::suspend(); + } + $expected = array( array('id' => 1, 'description' => 'one'), array('id' => 2, 'description' => 'two'), @@ -47,5 +59,9 @@ function test_pdo_query($conn, $want_drop_table_result=1) { }); tap_equal($expected, $actual, 'query (1-arg)'); + if ($is_fiber) { + Fiber::suspend(); + } + tap_equal($want_drop_table_result, $conn->exec("DROP TABLE test;"), 'drop table'); } diff --git a/tests/integration/pdo/test_query_fetch_class.inc b/tests/integration/pdo/test_query_fetch_class.inc index 4aef4c76f..966e19684 100644 --- a/tests/integration/pdo/test_query_fetch_class.inc +++ b/tests/integration/pdo/test_query_fetch_class.inc @@ -20,17 +20,27 @@ - ./base-class - ./constructor - ./factory + + NOTE: The function here can be used as a fiber. + The caller will need 2 fiber resumes to fully exercise the function. */ require_once(dirname(__FILE__).'/../../include/tap.php'); +require_once(dirname(__FILE__).'/../../include/helpers.php'); class Row { public $id; public $description; } -function test_pdo_query($conn, $want_drop_table_result=1) { +function test_pdo_query($conn, $want_drop_table_result=1, $is_fiber=false) { + if ($is_fiber) { + env_var_for_expects("GUID_TEST_PDO_QUERY", newrelic_get_linking_metadata()['span.id'] ?? ''); + } tap_equal(0, $conn->exec("CREATE TABLE test (id INT, description VARCHAR(10));"), 'create table'); + if ($is_fiber) { + Fiber::suspend(); + } tap_equal(1, $conn->exec("INSERT INTO test VALUES (1, 'one');"), 'insert row'); $expected = new Row(); @@ -40,5 +50,9 @@ function test_pdo_query($conn, $want_drop_table_result=1) { $actual = $conn->query('SELECT * FROM test;', PDO::FETCH_CLASS, 'Row')->fetch(); tap_assert($expected == $actual, 'fetch row as object'); + if ($is_fiber) { + Fiber::suspend(); + } + tap_equal($want_drop_table_result, $conn->exec("DROP TABLE test;"), 'drop table'); } diff --git a/tests/integration/pdo/test_query_fetch_column.inc b/tests/integration/pdo/test_query_fetch_column.inc index df1c590e5..e9f95c8e2 100644 --- a/tests/integration/pdo/test_query_fetch_column.inc +++ b/tests/integration/pdo/test_query_fetch_column.inc @@ -20,13 +20,25 @@ - ./base-class - ./constructor - ./factory + + NOTE: The function here can be used as a fiber. + The caller will need 2 fiber resumes to fully exercise the function. */ require_once(dirname(__FILE__).'/../../include/tap.php'); +require_once(dirname(__FILE__).'/../../include/helpers.php'); + +function test_pdo_query($conn, $want_drop_table_result=1, $is_fiber=false) { + if ($is_fiber) { + env_var_for_expects("GUID_TEST_PDO_QUERY", newrelic_get_linking_metadata()['span.id'] ?? ''); + } -function test_pdo_query($conn, $want_drop_table_result=1) { tap_equal(0, $conn->exec("CREATE TABLE test (id INT, description VARCHAR(10));"), 'create table'); + if ($is_fiber) { + Fiber::suspend(); + } + tap_equal(1, $conn->exec("INSERT INTO test VALUES (1, 'one');"), 'insert one'); tap_equal(1, $conn->exec("INSERT INTO test VALUES (2, 'two');"), 'insert two'); tap_equal(1, $conn->exec("INSERT INTO test VALUES (3, 'three');"), 'insert three'); @@ -34,6 +46,11 @@ function test_pdo_query($conn, $want_drop_table_result=1) { $result = $conn->query('SELECT * FROM test;', PDO::FETCH_COLUMN, 1); $actual = $result->fetchAll(PDO::FETCH_ASSOC); $result->closeCursor(); + + if ($is_fiber) { + Fiber::suspend(); + } + tap_equal(3, count($actual), 'fetch column'); tap_equal($want_drop_table_result, $conn->exec("DROP TABLE test;"), 'drop table'); diff --git a/tests/integration/pdo/test_query_fetch_into.inc b/tests/integration/pdo/test_query_fetch_into.inc index 2cc144881..2e1c7d740 100644 --- a/tests/integration/pdo/test_query_fetch_into.inc +++ b/tests/integration/pdo/test_query_fetch_into.inc @@ -20,19 +20,31 @@ - ./base-class - ./constructor - ./factory + + NOTE: The function here can be used as a fiber. + The caller will need 2 fiber resumes to fully exercise the function. */ require_once(dirname(__FILE__).'/../../include/tap.php'); +require_once(dirname(__FILE__).'/../../include/helpers.php'); class Row { public $id; public $description; } -function test_pdo_query($conn, $want_drop_table_result=1) { +function test_pdo_query($conn, $want_drop_table_result=1, $is_fiber=false) { + if ($is_fiber) { + env_var_for_expects("GUID_TEST_PDO_QUERY", newrelic_get_linking_metadata()['span.id'] ?? ''); + } + tap_equal(0, $conn->exec("CREATE TABLE test (id INT, description VARCHAR(10));"), 'create table'); tap_equal(1, $conn->exec("INSERT INTO test VALUES (1, 'one');"), 'insert row'); + if ($is_fiber) { + Fiber::suspend(); + } + $expected = new Row(); $expected->id = '1'; $expected->description = 'one'; @@ -41,5 +53,9 @@ function test_pdo_query($conn, $want_drop_table_result=1) { $conn->query('SELECT * FROM test;', PDO::FETCH_INTO, $actual)->fetch(); tap_assert($expected == $actual, 'fetch row into object'); + if ($is_fiber) { + Fiber::suspend(); + } + tap_equal($want_drop_table_result, $conn->exec("DROP TABLE test;"), 'drop table'); } diff --git a/tests/integration/redis/test_basic_fibers.php b/tests/integration/redis/test_basic_fibers.php new file mode 100644 index 000000000..27eb0c1e9 --- /dev/null +++ b/tests/integration/redis/test_basic_fibers.php @@ -0,0 +1,1457 @@ += 8.1 required\n"); +} +require("skipif.inc"); +*/ + +/*INI +newrelic.distributed_tracing_enabled=1 +newrelic.fibers.disabled = false + +*/ + +/*EXPECT +ok - ping redis +ok - set key +ok - get key +ok - get first character of key +ok - verify keys command returns known key +ok - ensure randomkey returns a key +ok - GET via rawcommand returns key +ok - set first character of key +ok - append to key +ok - get key +ok - ask redis for key length +ok - ask redis for key type +ok - execute getset command +ok - verify getset updated key +ok - delete key +ok - delete missing key +ok - mset key +ok - mget key +ok - msetnx existing key +ok - delete key +ok - msetnx non existing key +ok - unlink key +ok - reuse deleted key +ok - set duplicate key +ok - rename a key +ok - verify key was properly renamed +ok - renamenx to a nonexistent key +ok - create a second key +ok - renamenx to an existing key +Starting Func 'a' +ok - verify time command works +Ending Func 'a' +ok - we can EVAL a lua script +ok - we can EVAL a lua script with an SHA hash +ok - we can start a pipeline +ok - we can execute commands in a pipeline +ok - exec returns the correct response +ok - delete our temp keys +ok - pfadd reports new elements +ok - pfadd reports no new elements +ok - pfcount reports correct cardinality +ok - we can pfmerge into a destination key +ok - our merged key has the correct count +ok - there are no listners to a random channel +ok - verify a non-zero number of keys +ok - delete keys +ok - verify reduced redis db key count +*/ + +/*EXPECT_METRICS +[ + "?? agent run id", + "?? start time", + "?? stop time", + [ + [{"name":"DurationByCaller/Unknown/Unknown/Unknown/Unknown/all"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"DurationByCaller/Unknown/Unknown/Unknown/Unknown/allOther"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/all"}, [47, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/allOther"}, [47, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/Redis/all"}, [47, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/Redis/allOther"}, [47, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/operation/Redis/append"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/operation/Redis/append", + "scope":"OtherTransaction/php__FILE__"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/operation/Redis/connect"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/operation/Redis/connect", + "scope":"OtherTransaction/php__FILE__"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/operation/Redis/dbsize"}, [2, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/operation/Redis/dbsize", + "scope":"OtherTransaction/php__FILE__"}, [2, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/operation/Redis/del"}, [5, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/operation/Redis/del", + "scope":"OtherTransaction/php__FILE__"}, [5, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/operation/Redis/eval"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/operation/Redis/eval", + "scope":"OtherTransaction/php__FILE__"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/operation/Redis/evalsha"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/operation/Redis/evalsha", + "scope":"OtherTransaction/php__FILE__"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/operation/Redis/exec"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/operation/Redis/exec", + "scope":"OtherTransaction/php__FILE__"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/operation/Redis/exists"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/operation/Redis/exists", + "scope":"OtherTransaction/php__FILE__"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/operation/Redis/get"}, [5, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/operation/Redis/get", + "scope":"OtherTransaction/php__FILE__"}, [5, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/operation/Redis/getrange"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/operation/Redis/getrange", + "scope":"OtherTransaction/php__FILE__"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/operation/Redis/getset"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/operation/Redis/getset", + "scope":"OtherTransaction/php__FILE__"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/operation/Redis/keys"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/operation/Redis/keys", + "scope":"OtherTransaction/php__FILE__"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/operation/Redis/mget"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/operation/Redis/mget", + "scope":"OtherTransaction/php__FILE__"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/operation/Redis/mset"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/operation/Redis/mset", + "scope":"OtherTransaction/php__FILE__"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/operation/Redis/msetnx"}, [2, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/operation/Redis/msetnx", + "scope":"OtherTransaction/php__FILE__"}, [2, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/operation/Redis/pfadd"}, [2, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/operation/Redis/pfadd", + "scope":"OtherTransaction/php__FILE__"}, [2, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/operation/Redis/pfcount"}, [2, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/operation/Redis/pfcount", + "scope":"OtherTransaction/php__FILE__"}, [2, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/operation/Redis/pfmerge"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/operation/Redis/pfmerge", + "scope":"OtherTransaction/php__FILE__"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/operation/Redis/ping"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/operation/Redis/ping", + "scope":"OtherTransaction/php__FILE__"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/operation/Redis/publish"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/operation/Redis/publish", + "scope":"OtherTransaction/php__FILE__"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/operation/Redis/randomkey"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/operation/Redis/randomkey", + "scope":"OtherTransaction/php__FILE__"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/operation/Redis/rawcommand"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/operation/Redis/rawcommand", + "scope":"OtherTransaction/php__FILE__"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/operation/Redis/rename"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/operation/Redis/rename", + "scope":"OtherTransaction/php__FILE__"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/operation/Redis/renamenx"}, [2, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/operation/Redis/renamenx", + "scope":"OtherTransaction/php__FILE__"}, [2, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/operation/Redis/set"}, [3, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/operation/Redis/set", + "scope":"OtherTransaction/php__FILE__"}, [3, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/operation/Redis/setnx"}, [2, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/operation/Redis/setnx", + "scope":"OtherTransaction/php__FILE__"}, [2, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/operation/Redis/setrange"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/operation/Redis/setrange", + "scope":"OtherTransaction/php__FILE__"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/operation/Redis/strlen"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/operation/Redis/strlen", + "scope":"OtherTransaction/php__FILE__"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/operation/Redis/time"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/operation/Redis/time", + "scope":"OtherTransaction/php__FILE__"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/operation/Redis/type"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/operation/Redis/type", + "scope":"OtherTransaction/php__FILE__"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/operation/Redis/unlink"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/operation/Redis/unlink", + "scope":"OtherTransaction/php__FILE__"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"OtherTransaction/all"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"OtherTransaction/php__FILE__"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"OtherTransactionTotalTime"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"OtherTransactionTotalTime/php__FILE__"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Supportability/Logging/Forwarding/PHP/enabled"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Supportability/Logging/Metrics/PHP/enabled"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Supportability/Logging/LocalDecorating/PHP/disabled"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Supportability/Logging/Labels/PHP/disabled"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Supportability/api/get_linking_metadata"}, ["??", "??", "??", "??", "??", "??"]], + [{"name":"Supportability/PHP/Fiber/used"}, ["??", "??", "??", "??", "??", "??"]], + [{"name":"Datastore/instance/Redis/redisdb/6379"}, ["??", "??", "??", "??", "??", "??"]] + ] +] +*/ + +/*EXPECT_SPAN_EVENTS +[ + "?? agent run id", + { + "reservoir_size": 10000, + "events_seen": 50 + }, + [ + [ + { + "traceId": "??", + "duration": "??", + "transactionId": "??", + "name": "OtherTransaction\/php__FILE__", + "guid": "ENV[GUID_ROOT]", + "type": "Span", + "category": "generic", + "priority": "??", + "sampled": true, + "nr.entryPoint": true, + "timestamp": "??", + "transaction.name": "OtherTransaction\/php__FILE__" + }, + {}, + {} + ], + [ + { + "category": "generic", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "Custom\/test_basic", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "ENV[GUID_ROOT]" + }, + {}, + {} + ], + [ + { + "category": "datastore", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "Datastore\/operation\/Redis\/connect", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "ENV[GUID_TEST_BASIC]", + "span.kind": "client", + "component": "Redis" + }, + {}, + { + "peer.hostname": "redisdb", + "peer.address": "redisdb:6379", + "db.instance": "0" + } + ], + [ + { + "category": "datastore", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "Datastore\/operation\/Redis\/exists", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "ENV[GUID_TEST_BASIC]", + "span.kind": "client", + "component": "Redis" + }, + {}, + { + "peer.hostname": "redisdb", + "peer.address": "redisdb:6379", + "db.instance": "0" + } + ], + [ + { + "category": "datastore", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "Datastore\/operation\/Redis\/ping", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "ENV[GUID_TEST_BASIC]", + "span.kind": "client", + "component": "Redis" + }, + {}, + { + "peer.hostname": "redisdb", + "peer.address": "redisdb:6379", + "db.instance": "0" + } + ], + [ + { + "category": "datastore", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "Datastore\/operation\/Redis\/set", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "ENV[GUID_TEST_BASIC]", + "span.kind": "client", + "component": "Redis" + }, + {}, + { + "peer.hostname": "redisdb", + "peer.address": "redisdb:6379", + "db.instance": "0" + } + ], + [ + { + "category": "datastore", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "Datastore\/operation\/Redis\/get", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "ENV[GUID_TEST_BASIC]", + "span.kind": "client", + "component": "Redis" + }, + {}, + { + "peer.hostname": "redisdb", + "peer.address": "redisdb:6379", + "db.instance": "0" + } + ], + [ + { + "category": "datastore", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "Datastore\/operation\/Redis\/getrange", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "ENV[GUID_TEST_BASIC]", + "span.kind": "client", + "component": "Redis" + }, + {}, + { + "peer.hostname": "redisdb", + "peer.address": "redisdb:6379", + "db.instance": "0" + } + ], + [ + { + "category": "datastore", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "Datastore\/operation\/Redis\/keys", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "ENV[GUID_TEST_BASIC]", + "span.kind": "client", + "component": "Redis" + }, + {}, + { + "peer.hostname": "redisdb", + "peer.address": "redisdb:6379", + "db.instance": "0" + } + ], + [ + { + "category": "datastore", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "Datastore\/operation\/Redis\/randomkey", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "ENV[GUID_TEST_BASIC]", + "span.kind": "client", + "component": "Redis" + }, + {}, + { + "peer.hostname": "redisdb", + "peer.address": "redisdb:6379", + "db.instance": "0" + } + ], + [ + { + "category": "datastore", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "Datastore\/operation\/Redis\/rawcommand", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "ENV[GUID_TEST_BASIC]", + "span.kind": "client", + "component": "Redis" + }, + {}, + { + "peer.hostname": "redisdb", + "peer.address": "redisdb:6379", + "db.instance": "0" + } + ], + [ + { + "category": "datastore", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "Datastore\/operation\/Redis\/setrange", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "ENV[GUID_TEST_BASIC]", + "span.kind": "client", + "component": "Redis" + }, + {}, + { + "peer.hostname": "redisdb", + "peer.address": "redisdb:6379", + "db.instance": "0" + } + ], + [ + { + "category": "datastore", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "Datastore\/operation\/Redis\/append", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "ENV[GUID_TEST_BASIC]", + "span.kind": "client", + "component": "Redis" + }, + {}, + { + "peer.hostname": "redisdb", + "peer.address": "redisdb:6379", + "db.instance": "0" + } + ], + [ + { + "category": "datastore", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "Datastore\/operation\/Redis\/get", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "ENV[GUID_TEST_BASIC]", + "span.kind": "client", + "component": "Redis" + }, + {}, + { + "peer.hostname": "redisdb", + "peer.address": "redisdb:6379", + "db.instance": "0" + } + ], + [ + { + "category": "datastore", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "Datastore\/operation\/Redis\/strlen", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "ENV[GUID_TEST_BASIC]", + "span.kind": "client", + "component": "Redis" + }, + {}, + { + "peer.hostname": "redisdb", + "peer.address": "redisdb:6379", + "db.instance": "0" + } + ], + [ + { + "category": "datastore", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "Datastore\/operation\/Redis\/type", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "ENV[GUID_TEST_BASIC]", + "span.kind": "client", + "component": "Redis" + }, + {}, + { + "peer.hostname": "redisdb", + "peer.address": "redisdb:6379", + "db.instance": "0" + } + ], + [ + { + "category": "datastore", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "Datastore\/operation\/Redis\/getset", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "ENV[GUID_TEST_BASIC]", + "span.kind": "client", + "component": "Redis" + }, + {}, + { + "peer.hostname": "redisdb", + "peer.address": "redisdb:6379", + "db.instance": "0" + } + ], + [ + { + "category": "datastore", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "Datastore\/operation\/Redis\/get", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "ENV[GUID_TEST_BASIC]", + "span.kind": "client", + "component": "Redis" + + }, + {}, + { + "peer.hostname": "redisdb", + "peer.address": "redisdb:6379", + "db.instance": "0" + } + ], + [ + { + "category": "datastore", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "Datastore\/operation\/Redis\/del", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "ENV[GUID_TEST_BASIC]", + "span.kind": "client", + "component": "Redis" + }, + {}, + { + "peer.hostname": "redisdb", + "peer.address": "redisdb:6379", + "db.instance": "0" + } + ], + [ + { + "category": "datastore", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "Datastore\/operation\/Redis\/del", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "ENV[GUID_TEST_BASIC]", + "span.kind": "client", + "component": "Redis" + }, + {}, + { + "peer.hostname": "redisdb", + "peer.address": "redisdb:6379", + "db.instance": "0" + } + ], + [ + { + "category": "datastore", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "Datastore\/operation\/Redis\/mset", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "ENV[GUID_TEST_BASIC]", + "span.kind": "client", + "component": "Redis" + }, + {}, + { + "peer.hostname": "redisdb", + "peer.address": "redisdb:6379", + "db.instance": "0" + } + ], + [ + { + "category": "datastore", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "Datastore\/operation\/Redis\/mget", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "ENV[GUID_TEST_BASIC]", + "span.kind": "client", + "component": "Redis" + }, + {}, + { + "peer.hostname": "redisdb", + "peer.address": "redisdb:6379", + "db.instance": "0" + } + ], + [ + { + "category": "datastore", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "Datastore\/operation\/Redis\/msetnx", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "ENV[GUID_TEST_BASIC]", + "span.kind": "client", + "component": "Redis" + }, + {}, + { + "peer.hostname": "redisdb", + "peer.address": "redisdb:6379", + "db.instance": "0" + } + ], + [ + { + "category": "datastore", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "Datastore\/operation\/Redis\/del", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "ENV[GUID_TEST_BASIC]", + "span.kind": "client", + "component": "Redis" + }, + {}, + { + "peer.hostname": "redisdb", + "peer.address": "redisdb:6379", + "db.instance": "0" + } + ], + [ + { + "category": "datastore", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "Datastore\/operation\/Redis\/msetnx", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "ENV[GUID_TEST_BASIC]", + "span.kind": "client", + "component": "Redis" + }, + {}, + { + "peer.hostname": "redisdb", + "peer.address": "redisdb:6379", + "db.instance": "0" + } + ], + [ + { + "category": "datastore", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "Datastore\/operation\/Redis\/unlink", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "ENV[GUID_TEST_BASIC]", + "span.kind": "client", + "component": "Redis" + }, + {}, + { + "peer.hostname": "redisdb", + "peer.address": "redisdb:6379", + "db.instance": "0" + } + ], + [ + { + "category": "datastore", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "Datastore\/operation\/Redis\/setnx", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "ENV[GUID_TEST_BASIC]", + "span.kind": "client", + "component": "Redis" + }, + {}, + { + "peer.hostname": "redisdb", + "peer.address": "redisdb:6379", + "db.instance": "0" + } + ], + [ + { + "category": "datastore", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "Datastore\/operation\/Redis\/setnx", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "ENV[GUID_TEST_BASIC]", + "span.kind": "client", + "component": "Redis" + }, + {}, + { + "peer.hostname": "redisdb", + "peer.address": "redisdb:6379", + "db.instance": "0" + } + ], + [ + { + "category": "datastore", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "Datastore\/operation\/Redis\/rename", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "ENV[GUID_TEST_BASIC]", + "span.kind": "client", + "component": "Redis" + }, + {}, + { + "peer.hostname": "redisdb", + "peer.address": "redisdb:6379", + "db.instance": "0" + } + ], + [ + { + "category": "datastore", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "Datastore\/operation\/Redis\/get", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "ENV[GUID_TEST_BASIC]", + "span.kind": "client", + "component": "Redis" + }, + {}, + { + "peer.hostname": "redisdb", + "peer.address": "redisdb:6379", + "db.instance": "0" + } + ], + [ + { + "category": "datastore", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "Datastore\/operation\/Redis\/renamenx", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "ENV[GUID_TEST_BASIC]", + "span.kind": "client", + "component": "Redis" + }, + {}, + { + "peer.hostname": "redisdb", + "peer.address": "redisdb:6379", + "db.instance": "0" + } + ], + [ + { + "category": "datastore", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "Datastore\/operation\/Redis\/set", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "ENV[GUID_TEST_BASIC]", + "span.kind": "client", + "component": "Redis" + }, + {}, + { + "peer.hostname": "redisdb", + "peer.address": "redisdb:6379", + "db.instance": "0" + } + ], + [ + { + "category": "datastore", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "Datastore\/operation\/Redis\/renamenx", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "ENV[GUID_TEST_BASIC]", + "span.kind": "client", + "component": "Redis" + }, + {}, + { + "peer.hostname": "redisdb", + "peer.address": "redisdb:6379", + "db.instance": "0" + } + ], + [ + { + "category": "datastore", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "Datastore\/operation\/Redis\/time", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "ENV[GUID_TEST_BASIC]", + "span.kind": "client", + "component": "Redis" + }, + {}, + { + "peer.hostname": "redisdb", + "peer.address": "redisdb:6379", + "db.instance": "0" + } + ], + [ + { + "category": "datastore", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "Datastore\/operation\/Redis\/eval", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "ENV[GUID_TEST_BASIC]", + "span.kind": "client", + "component": "Redis" + }, + {}, + { + "peer.hostname": "redisdb", + "peer.address": "redisdb:6379", + "db.instance": "0" + } + ], + [ + { + "category": "datastore", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "Datastore\/operation\/Redis\/evalsha", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "ENV[GUID_TEST_BASIC]", + "span.kind": "client", + "component": "Redis" + }, + {}, + { + "peer.hostname": "redisdb", + "peer.address": "redisdb:6379", + "db.instance": "0" + } + ], + [ + { + "category": "datastore", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "Datastore\/operation\/Redis\/set", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "ENV[GUID_TEST_BASIC]", + "span.kind": "client", + "component": "Redis" + }, + {}, + { + "peer.hostname": "redisdb", + "peer.address": "redisdb:6379", + "db.instance": "0" + } + ], + [ + { + "category": "datastore", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "Datastore\/operation\/Redis\/get", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "ENV[GUID_TEST_BASIC]", + "span.kind": "client", + "component": "Redis" + }, + {}, + { + "peer.hostname": "redisdb", + "peer.address": "redisdb:6379", + "db.instance": "0" + } + ], + [ + { + "category": "datastore", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "Datastore\/operation\/Redis\/exec", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "ENV[GUID_TEST_BASIC]", + "span.kind": "client", + "component": "Redis" + }, + {}, + { + "peer.hostname": "redisdb", + "peer.address": "redisdb:6379", + "db.instance": "0" + } + ], + [ + { + "category": "datastore", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "Datastore\/operation\/Redis\/del", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "ENV[GUID_TEST_BASIC]", + "span.kind": "client", + "component": "Redis" + }, + {}, + { + "peer.hostname": "redisdb", + "peer.address": "redisdb:6379", + "db.instance": "0" + } + ], + [ + { + "category": "datastore", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "Datastore\/operation\/Redis\/pfadd", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "ENV[GUID_TEST_BASIC]", + "span.kind": "client", + "component": "Redis" + }, + {}, + { + "peer.hostname": "redisdb", + "peer.address": "redisdb:6379", + "db.instance": "0" + } + ], + [ + { + "category": "datastore", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "Datastore\/operation\/Redis\/pfadd", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "ENV[GUID_TEST_BASIC]", + "span.kind": "client", + "component": "Redis" + }, + {}, + { + "peer.hostname": "redisdb", + "peer.address": "redisdb:6379", + "db.instance": "0" + } + ], + [ + { + "category": "datastore", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "Datastore\/operation\/Redis\/pfcount", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "ENV[GUID_TEST_BASIC]", + "span.kind": "client", + "component": "Redis" + }, + {}, + { + "peer.hostname": "redisdb", + "peer.address": "redisdb:6379", + "db.instance": "0" + } + ], + [ + { + "category": "datastore", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "Datastore\/operation\/Redis\/pfmerge", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "ENV[GUID_TEST_BASIC]", + "span.kind": "client", + "component": "Redis" + }, + {}, + { + "peer.hostname": "redisdb", + "peer.address": "redisdb:6379", + "db.instance": "0" + } + ], + [ + { + "category": "datastore", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "Datastore\/operation\/Redis\/pfcount", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "ENV[GUID_TEST_BASIC]", + "span.kind": "client", + "component": "Redis" + }, + {}, + { + "peer.hostname": "redisdb", + "peer.address": "redisdb:6379", + "db.instance": "0" + } + ], + [ + { + "category": "datastore", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "Datastore\/operation\/Redis\/publish", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "ENV[GUID_TEST_BASIC]", + "span.kind": "client", + "component": "Redis" + }, + {}, + { + "peer.hostname": "redisdb", + "peer.address": "redisdb:6379", + "db.instance": "0" + } + ], + [ + { + "category": "datastore", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "Datastore\/operation\/Redis\/dbsize", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "ENV[GUID_TEST_BASIC]", + "span.kind": "client", + "component": "Redis" + }, + {}, + { + "peer.hostname": "redisdb", + "peer.address": "redisdb:6379", + "db.instance": "0" + } + ], + [ + { + "category": "datastore", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "Datastore\/operation\/Redis\/del", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "ENV[GUID_TEST_BASIC]", + "span.kind": "client", + "component": "Redis" + }, + {}, + { + "peer.hostname": "redisdb", + "peer.address": "redisdb:6379", + "db.instance": "0" + } + ], + [ + { + "category": "datastore", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "Datastore\/operation\/Redis\/dbsize", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "ENV[GUID_TEST_BASIC]", + "span.kind": "client", + "component": "Redis" + }, + {}, + { + "peer.hostname": "redisdb", + "peer.address": "redisdb:6379", + "db.instance": "0" + } + ], + [ + { + "type": "Span", + "traceId": "??", + "transactionId": "??", + "sampled": true, + "priority": "??", + "name": "Custom\/a", + "guid": "??", + "timestamp": "??", + "duration": "??", + "category": "generic", + "parentId": "ENV[GUID_ROOT]" + }, + {}, + {} + ] + ] +] +*/ + + +require_once(realpath (dirname ( __FILE__ )) . '/../../include/helpers.php'); +require_once(realpath (dirname ( __FILE__ )) . '/../../include/tap.php'); +require_once(realpath (dirname ( __FILE__ )) . '/redis.inc'); + +env_var_for_expects("GUID_ROOT", newrelic_get_linking_metadata()['span.id'] ?? ''); + +function test_basic() { + global $REDIS_HOST, $REDIS_PORT; + + env_var_for_expects("GUID_TEST_BASIC", newrelic_get_linking_metadata()['span.id'] ?? ''); + + $redis = new Redis(); + $redis->connect($REDIS_HOST, $REDIS_PORT); + + /* generate a unique key to use for this test run */ + $key1 = randstr(16); + $key2 = "{$key1}_b"; + if ($redis->exists([$key1, $key2])) { + die("skip: key(s) already exist: $key1, $key2\n"); + exit(1); + } + + tap_equal("+PONG", $redis->ping("+PONG"), 'ping redis'); + + tap_assert($redis->set($key1, 'car'), 'set key'); + tap_equal('car', $redis->get($key1), 'get key'); + tap_equal('c', $redis->getrange($key1, 0, 0), 'get first character of key'); + + tap_equal([$key1], $redis->keys($key1), 'verify keys command returns known key'); + tap_assert(is_string($redis->randomkey()), 'ensure randomkey returns a key'); + tap_equal('car', $redis->rawcommand('get', $key1), 'GET via rawcommand returns key'); + + tap_equal(3, $redis->setrange($key1, 0, 'b'), 'set first character of key'); + tap_equal(strlen('barometric'), $redis->append($key1, 'ometric'), 'append to key'); + tap_equal('barometric', $redis->get($key1), 'get key'); + tap_equal(strlen('barometric'), $redis->strlen($key1), 'ask redis for key length'); + + tap_equal(Redis::REDIS_STRING, $redis->type($key1), 'ask redis for key type'); + + tap_equal('barometric', $redis->getset($key1, 'geometric'), 'execute getset command'); + tap_equal('geometric', $redis->get($key1), 'verify getset updated key'); + + tap_equal(1, $redis->del($key1), 'delete key'); + tap_equal(0, $redis->del($key1), 'delete missing key'); + + tap_assert($redis->mset([$key1 => 'bar']), 'mset key'); + tap_equal(['bar'], $redis->mget([$key1]), 'mget key'); + + tap_refute($redis->msetnx([$key1 => 'newbar']), 'msetnx existing key'); + tap_equal(1, $redis->del($key1), 'delete key'); + tap_assert($redis->msetnx([$key1 => 'newbar']), 'msetnx non existing key'); + tap_equal(1, $redis->unlink($key1), 'unlink key'); + + tap_assert($redis->setnx($key1, 'bar'), 'reuse deleted key'); + tap_refute($redis->setnx($key1, 'bar'), 'set duplicate key'); + + tap_assert($redis->rename($key1, $key2), 'rename a key'); + tap_equal('bar', $redis->get($key2), 'verify key was properly renamed'); + tap_assert($redis->renamenx($key2, $key1), 'renamenx to a nonexistent key'); + tap_assert($redis->set($key2, 'baz'), 'create a second key'); + tap_refute($redis->renamenx($key1, $key2), 'renamenx to an existing key'); + Fiber::suspend(); + $res = $redis->time(); + tap_assert(is_array($res) && count($res) == 2 && is_numeric($res[0]) && is_numeric($res[1]), + 'verify time command works'); + + Fiber::suspend(); + $lua = 'return 42'; + tap_equal(42, $redis->eval($lua), 'we can EVAL a lua script'); + tap_equal(42, $redis->evalsha(sha1($lua)), 'we can EVAL a lua script with an SHA hash'); + + $msg = 'So Long, and Thanks for All the Fish'; + tap_assert(is_object($redis->pipeline()), 'we can start a pipeline'); + tap_assert(is_object($redis->set($key1, $msg)->get($key1)), 'we can execute commands in a pipeline'); + tap_equal([true, $msg], $redis->exec(), 'exec returns the correct response'); + + tap_equal(2, $redis->del([$key1, $key2]), 'delete our temp keys'); + tap_equal(1, $redis->pfadd($key1, ['one', 'two', 'three']), 'pfadd reports new elements'); + tap_equal(0, $redis->pfadd($key1, ['three']), 'pfadd reports no new elements'); + tap_equal(3, $redis->pfcount($key1), 'pfcount reports correct cardinality'); + tap_assert($redis->pfmerge($key2, [$key1]), 'we can pfmerge into a destination key'); + tap_equal(3, $redis->pfcount($key2), 'our merged key has the correct count'); + + /* There's no trivial way to do deep verification of PUBLISH without a subscribed client + so simply issue the command and validate the response isn't an error */ + tap_equal(0, $redis->publish(uniqid(), 'message'), 'there are no listners to a random channel'); + + tap_assert(($db_size = $redis->dbsize()) > 0, 'verify a non-zero number of keys'); + + /* cleanup the key used by this test run */ + tap_equal(2, $redis->del([$key1, $key2]), 'delete keys'); + + tap_equal($db_size - 2, $redis->dbsize(), 'verify reduced redis db key count'); + + /* close connection */ + $redis->close(); +} + +function a() +{ + echo "Starting Func 'a'\n"; + env_var_for_expects("GUID_A", newrelic_get_linking_metadata()['span.id'] ?? ''); + time_nanosleep(0, 100000000); + Fiber::suspend(); + echo "Ending Func 'a'\n"; +}; + +$fiber_a = new Fiber('a'); +$fiber_basic = new Fiber('test_basic'); + + +$fiber_basic->start(); +$fiber_a->start(); +$fiber_basic->resume(); +$fiber_a->resume(); +$fiber_basic->resume(); diff --git a/tests/integration/span_events/fibers/test_fiber_custom_parameter.php b/tests/integration/span_events/fibers/test_fiber_custom_parameter.php new file mode 100644 index 000000000..136260815 --- /dev/null +++ b/tests/integration/span_events/fibers/test_fiber_custom_parameter.php @@ -0,0 +1,204 @@ +getMessage() . "\n"); + } + newrelic_add_custom_parameter("string", "b_str"); + newrelic_add_custom_parameter("int", 7); + Fiber::suspend(); + newrelic_add_custom_parameter("bool", false); + newrelic_add_custom_parameter("double", 1.5); +} + +function fraction($x) { + time_nanosleep(0, 100000000); + if (!$x) { + return 0; + } + return 1/$x; +} + +function a() +{ + time_nanosleep(0, 100000000); + echo "Hello\n"; + newrelic_add_custom_parameter("string", "a_str"); + newrelic_add_custom_parameter("int", 7); + newrelic_add_custom_parameter("bool", false); + newrelic_add_custom_parameter("double", 1.5); +}; + +$fiber = new Fiber('b'); +a(); +$fiber->start(); + +$fiber->resume(); diff --git a/tests/integration/span_events/fibers/test_fiber_heavy.php b/tests/integration/span_events/fibers/test_fiber_heavy.php new file mode 100644 index 000000000..f462a8eaa --- /dev/null +++ b/tests/integration/span_events/fibers/test_fiber_heavy.php @@ -0,0 +1,310 @@ +start(); + } + + $afterStartMemory = memory_get_usage(); + echo "After fiber start: " . number_format($afterStartMemory / 1024) . " KB" . PHP_EOL; + + foreach ($heavyFibers as $fiber) { + if (!$fiber->isTerminated()) { + $fiber->resume(); + } + } + + // Verify results + $activeCount = 0; + $terminatedCount = 0; + + foreach ($heavyFibers as $index => $fiber) { + if ($fiber->isTerminated()) { + $terminatedCount++; + } else { + $activeCount++; + } + } + + echo "Terminated fibers: $terminatedCount, Active: $activeCount" . PHP_EOL; + + $finalMemory = memory_get_usage(); + $finalPeakMemory = memory_get_peak_usage(); + + echo "Final memory: " . number_format($finalMemory / 1024) . " KB" . PHP_EOL; + echo "Peak memory: " . number_format($finalPeakMemory / 1024) . " KB" . PHP_EOL; + + // Cleanup test + unset($heavyFibers); + gc_collect_cycles(); + + $cleanupMemory = memory_get_usage(); + echo "After cleanup: " . number_format($cleanupMemory / 1024) . " KB" . PHP_EOL; + +} + +manyTest(200); diff --git a/tests/integration/span_events/fibers/test_fiber_many.php b/tests/integration/span_events/fibers/test_fiber_many.php new file mode 100644 index 000000000..997ec0109 --- /dev/null +++ b/tests/integration/span_events/fibers/test_fiber_many.php @@ -0,0 +1,299 @@ + $fiber) { + $results[$index] = $fiber->start(); + } + + $startExecutionTime = microtime(true); + echo "Fiber start time: " . number_format(($startExecutionTime - $creationTime) * 1000, 2) . "ms" . PHP_EOL; + + // Resume fibers multiple times + for ($iteration = 0; $iteration < $iterations; $iteration++) { + foreach ($fibers as $index => $fiber) { + if (!$fiber->isTerminated()) { + $results[$index] = $fiber->resume($iteration + 1); + } + } + } + + $endTime = microtime(true); + echo "Total execution time: " . number_format(($endTime - $startTime) * 1000, 2) . "ms" . PHP_EOL; + + // Verify results + $activeCount = 0; + $terminatedCount = 0; + $totalSum = 0; + + foreach ($fibers as $index => $fiber) { + if ($fiber->isTerminated()) { + $terminatedCount++; + $totalSum += $results[$index]; + } else { + $activeCount++; + } + } + + echo "Terminated fibers: $terminatedCount, Active: $activeCount, Total sum: $totalSum" . PHP_EOL; + return $endTime - $startTime; +} + +// Run tests with different fiber counts +$testSizes = [200]; +$iterations = 5; + +foreach ($testSizes as $size) { + echo "\n--- Many Fibers Test: $size fibers ---" . PHP_EOL; + $time = manyTest($size, $iterations); + echo "Average time per fiber: " . number_format(($time * 1000) / $size, 3) . "ms" . PHP_EOL; + echo "Throughput: " . number_format($size / $time, 0) . " fibers/second" . PHP_EOL; +} diff --git a/tests/integration/span_events/fibers/test_fiber_multi.php b/tests/integration/span_events/fibers/test_fiber_multi.php new file mode 100644 index 000000000..d16c7d6ff --- /dev/null +++ b/tests/integration/span_events/fibers/test_fiber_multi.php @@ -0,0 +1,323 @@ +operations[$name] = [ + 'fiber' => $fiber, + 'duration' => $duration, + 'current_step' => 0 + ]; + + return $fiber->start(); // Start immediately + } + + public function runOperations() { + $completed = 0; + $total = count($this->operations); + $tick = 0; + + env_var_for_expects("GUID_RUN_OP", newrelic_get_linking_metadata()['span.id'] ?? ''); + + while ($completed < $total) { + $tick++; + echo "\n--- Tick $tick ---" . PHP_EOL; + + foreach ($this->operations as $name => &$op) { + if ($op['fiber']->isTerminated()) { + continue; + } + + if ($op['current_step'] < $op['duration']) { + $result = $op['fiber']->resume("progress-" . ($op['current_step'] + 1)); + $op['current_step']++; + + if ($op['fiber']->isTerminated()) { + echo "COMPLETED: $result" . PHP_EOL; + $completed++; + } + } + } + unset($op); + + // Simulate processing delay + usleep(100000); // 0.1 seconds + } + + echo "\nAll operations completed!" . PHP_EOL; + } +} + +// Run +$multi = new MultiOperation(); +$multi->addOperation('database-query', 3); +$multi->addOperation('file-upload', 5); +$multi->addOperation('api-call', 2); +$multi->addOperation('image-processing', 4); + +$multi->runOperations(); diff --git a/tests/integration/span_events/fibers/test_fiber_multi2.php b/tests/integration/span_events/fibers/test_fiber_multi2.php new file mode 100644 index 000000000..7a907d34a --- /dev/null +++ b/tests/integration/span_events/fibers/test_fiber_multi2.php @@ -0,0 +1,353 @@ +producers[$name] = $fiber; + return $fiber->start(); + } + + public function addConsumer($name, $maxItems) { + env_var_for_expects("GUID_ADD_CONSUMER_" . $name, newrelic_get_linking_metadata()['span.id'] ?? ''); + time_nanosleep(0, 100000000); + + $fiber = new Fiber(function() use ($name, $maxItems) { + env_var_for_expects("GUID_FIBER_" . $name, newrelic_get_linking_metadata()['span.id'] ?? ''); + newrelic_add_custom_span_parameter("fiber_start_" . $name, $name); + try { + $consumed = 0; + while ($consumed < $maxItems - 1) { + $item = Fiber::suspend("waiting-for-item"); + if ($item !== null) { + echo "Consumer $name processed: $item" . PHP_EOL; + $consumed++; + } + } + } finally { + env_var_for_expects("GUID_FIBER_NAME_" . $name, $name); + newrelic_add_custom_span_parameter("fiber_end_" . $name, $name); + } + return "Consumer $name finished"; + }); + + $this->consumers[$name] = $fiber; + return $fiber->start(); + } + + public function processQueue($rounds) { + for ($round = 0; $round < $rounds; $round++) { + echo "\n--- Processing Round $round ---" . PHP_EOL; + + // Get items from producers + foreach ($this->producers as $name => $fiber) { + if (!$fiber->isTerminated()) { + $item = $fiber->resume(); + if (!$fiber->isTerminated()) { + $this->items[] = $item; + } else { + echo "PRODUCER $name COMPLETED" . PHP_EOL; + } + } + } + + // Give items to consumers + foreach ($this->consumers as $name => $fiber) { + if (!$fiber->isTerminated()) { + if (!empty($this->items)) { + $item = array_shift($this->items); + $result = $fiber->resume($item); + if ($fiber->isTerminated()) { + echo "CONSUMER $name COMPLETED" . PHP_EOL; + } + } else { + $fiber->resume(null); + } + } + } + + // Feed consumers with null if no items + foreach ($this->consumers as $name => $fiber) { + if (!$fiber->isTerminated()) { + $fiber->resume(null); + } + } + } + } +} + +// Run producer-consumer multi-fiber +echo "\n=== Producer-Consumer Pattern ===" . PHP_EOL; +$queue = new FiberQueue(); +$queue->addProducer('fast-producer', 3); +$queue->addProducer('slow-producer', 2); +$queue->addConsumer('consumer-a', 3); +$queue->addConsumer('consumer-b', 3); + +$queue->processQueue(8); + +echo "multi-fiber completed" . PHP_EOL; diff --git a/tests/integration/span_events/fibers/test_fiber_nested1.php b/tests/integration/span_events/fibers/test_fiber_nested1.php new file mode 100644 index 000000000..70678d0ae --- /dev/null +++ b/tests/integration/span_events/fibers/test_fiber_nested1.php @@ -0,0 +1,194 @@ +getMessage() . "\n"); + } + echo "Ending Func 'b'\n"; +} + +function fraction($x) { + echo "Starting Func 'fraction'\n"; + env_var_for_expects("GUID_FRACTION", newrelic_get_linking_metadata()['span.id'] ?? ''); + time_nanosleep(0, 100000000); + if (!$x) { + throw new RuntimeException('Division by zero'); + } + echo "Ending Func 'fraction'\n"; + return 1/$x; +} + +function a() +{ + echo "Starting Func 'a'\n"; + env_var_for_expects("GUID_A", newrelic_get_linking_metadata()['span.id'] ?? ''); + time_nanosleep(0, 100000000); + Fiber::suspend(); + echo "Ending Func 'a'\n"; +}; + +$fiber = new Fiber('a'); +$fiber->start(); + +b(); + +$fiber->resume(); diff --git a/tests/integration/span_events/fibers/test_fiber_nested2.php b/tests/integration/span_events/fibers/test_fiber_nested2.php new file mode 100644 index 000000000..7e94ba69e --- /dev/null +++ b/tests/integration/span_events/fibers/test_fiber_nested2.php @@ -0,0 +1,197 @@ +getMessage() . "\n"); + } + echo "Ending Func 'b'\n"; +} + +function fraction($x) { + echo "Starting Func 'fraction'\n"; + env_var_for_expects("GUID_FRACTION", newrelic_get_linking_metadata()['span.id'] ?? ''); + time_nanosleep(0, 100000000); + if (!$x) { + throw new RuntimeException('Division by zero'); + } + echo "Ending Func 'fraction'\n"; + return 1/$x; +} + +function a() +{ + echo "Starting Func 'a'\n"; + env_var_for_expects("GUID_A", newrelic_get_linking_metadata()['span.id'] ?? ''); + time_nanosleep(0, 100000000); + echo "Ending Func 'a'\n"; +}; + +$fiber = new Fiber('b'); +a(); +$fiber->start(); + +$fiber->resume(); diff --git a/tests/integration/span_events/fibers/test_fiber_nested3.php b/tests/integration/span_events/fibers/test_fiber_nested3.php new file mode 100644 index 000000000..1f9e69314 --- /dev/null +++ b/tests/integration/span_events/fibers/test_fiber_nested3.php @@ -0,0 +1,193 @@ +start(); + time_nanosleep(0, 100000000); + $fiber->resume(); + } catch (RuntimeException $e) { + echo("Caught exception: " . $e->getMessage() . "\n"); + } + + echo "Ending Func 'b'\n"; +} + +function fraction($x) { + echo "Starting Func 'fraction'\n"; + env_var_for_expects("GUID_FRACTION", newrelic_get_linking_metadata()['span.id'] ?? ''); + time_nanosleep(0, 100000000); + if (!$x) { + throw new RuntimeException('Division by zero'); + } + echo "Ending Func 'fraction'\n"; + return 1/$x; +} + +function a() +{ + echo "Starting Func 'a'\n"; + env_var_for_expects("GUID_A", newrelic_get_linking_metadata()['span.id'] ?? ''); + time_nanosleep(0, 100000000); + echo "Ending Func 'a'\n"; +}; + +a(); +b(); diff --git a/tests/integration/span_events/fibers/test_fiber_nested4.php b/tests/integration/span_events/fibers/test_fiber_nested4.php new file mode 100644 index 000000000..ecd66a8f8 --- /dev/null +++ b/tests/integration/span_events/fibers/test_fiber_nested4.php @@ -0,0 +1,200 @@ +start(); + time_nanosleep(0, 100000000); + $fiberc->resume(); + } catch (RuntimeException $e) { + echo("Caught exception: " . $e->getMessage() . "\n"); + } + echo "Ending Func 'b'\n"; +} + +function fraction($x) { + echo "Starting Func 'fraction'\n"; + env_var_for_expects("GUID_FRACTION", newrelic_get_linking_metadata()['span.id'] ?? ''); + time_nanosleep(0, 100000000); + if (!$x) { + throw new RuntimeException('Division by zero'); + } + echo "Ending Func 'fraction'\n"; + return 1/$x; +} + +function a() +{ + echo "Starting Func 'a'\n"; + env_var_for_expects("GUID_A", newrelic_get_linking_metadata()['span.id'] ?? ''); + Fiber::suspend(); + time_nanosleep(0, 100000000); + echo "Ending Func 'a'\n"; +}; + +$fibera = new Fiber('a'); +$fiberb = new Fiber('b'); + +$fibera->start(); +$fiberb->start(); +$fiberb->resume(); +$fibera->resume(); diff --git a/tests/integration/span_events/fibers/test_fiber_nested5.php b/tests/integration/span_events/fibers/test_fiber_nested5.php new file mode 100644 index 000000000..262dfca06 --- /dev/null +++ b/tests/integration/span_events/fibers/test_fiber_nested5.php @@ -0,0 +1,159 @@ +start(); + echo "Nested fiber level $level received: $nestedResult" . PHP_EOL; + } + + Fiber::suspend("suspended-level-$level"); + echo "Fiber level $level resuming" . PHP_EOL; + + if ($level < $maxLevel) { + echo "Fiber level $level will resume the suspended nested fiber" . PHP_EOL; + $nestedFiber->resume(); + } + return "completed-level-$level"; + }); +} + +// Create and run nested fibers +$mainFiber = createNestedFiber(1, 3); +$result = $mainFiber->start(); +echo "Main received from fiber: $result" . PHP_EOL; +$mainFiber->resume(); +$finalResult = $mainFiber->getReturn(); +echo "Final result: $finalResult" . PHP_EOL; diff --git a/tests/integration/span_events/fibers/test_fiber_nested6.php b/tests/integration/span_events/fibers/test_fiber_nested6.php new file mode 100644 index 000000000..b817aa9bf --- /dev/null +++ b/tests/integration/span_events/fibers/test_fiber_nested6.php @@ -0,0 +1,151 @@ +start(); + echo "Nested fiber level $level received: $nestedResult" . PHP_EOL; + } + + Fiber::suspend("suspended-level-$level"); + echo "Fiber level $level resuming" . PHP_EOL; + + return "completed-level-$level"; + }); +} + +// Create and run nested fibers +$mainFiber = createNestedFiber(1, 3); +$result = $mainFiber->start(); +echo "Main received from fiber: $result" . PHP_EOL; +$mainFiber->resume(); +$finalResult = $mainFiber->getReturn(); +echo "Final result: $finalResult" . PHP_EOL; diff --git a/tests/integration/span_events/fibers/test_fiber_nested_100_deep.php b/tests/integration/span_events/fibers/test_fiber_nested_100_deep.php new file mode 100644 index 000000000..34763ebe6 --- /dev/null +++ b/tests/integration/span_events/fibers/test_fiber_nested_100_deep.php @@ -0,0 +1,373 @@ += 100) { + echo "Reached maximum nesting depth: 100\n"; + Fiber::suspend(); + echo "Ending nested fiber: level $level\n"; + return $level; + } + + // Create the next nested fiber + $nested_fiber = new Fiber(function() use ($level) { + // Store GUIDs for key levels for span event verification + env_var_for_expects("GUID_FIBER_" . $level, newrelic_get_linking_metadata()['span.id'] ?? ''); + + return nested_fiber($level + 1); + }); + + // Suspend before starting the nested fiber + Fiber::suspend(); + + // Start and wait for the nested fiber + $result = $nested_fiber->start(); + + // Resume the nested fiber if it's suspended + if ($nested_fiber->isSuspended()) { + $result = $nested_fiber->resume(); + } + + echo "Ending nested fiber: level $level\n"; + return $result; +} + +// Start the nested fiber chain +$main_fiber = new Fiber(function() { + env_var_for_expects("GUID_FIBER_0", newrelic_get_linking_metadata()['span.id'] ?? ''); + return nested_fiber(1); +}); + +$main_fiber->start(); + +// Resume the main fiber to trigger all nested fiber execution +while ($main_fiber->isSuspended()) { + $main_fiber->resume(); +} + +echo "Nested fiber test completed successfully\n"; diff --git a/tests/integration/span_events/fibers/test_fiber_noresume.php b/tests/integration/span_events/fibers/test_fiber_noresume.php new file mode 100644 index 000000000..45ef4d219 --- /dev/null +++ b/tests/integration/span_events/fibers/test_fiber_noresume.php @@ -0,0 +1,212 @@ +resume doesn't happen, but no more execution will occur in the fiber. + +Here is what happens to the suspended fiber and its state: + +Pending finally blocks: Any finally blocks within the fiber's current execution stack will be executed, +allowing for cleanup (e.g., closing file handles, database connections). + +Destructors: The __destruct() methods of any objects held in the fiber’s scope will be called. + +No further execution: The code within the fiber will not complete, and it will not return to t +he Fiber::suspend() call to resume its normal flow. + +Memory release: The fiber's stack and associated memory are freed once the Fiber object has no +remaining references and is collected. + +Output should show that PHP functionality should continue to work as expected. +*/ + +/*SKIPIF +start(); + time_nanosleep(0, 100000000); + $fiberc->resume(); + } catch (RuntimeException $e) { + echo("Caught exception: " . $e->getMessage() . "\n"); + } + echo "Ending Func 'b'\n"; +} + +function fraction($x) { + echo "Starting Func 'fraction'\n"; + env_var_for_expects("GUID_FRACTION", newrelic_get_linking_metadata()['span.id'] ?? ''); + time_nanosleep(0, 100000000); + if (!$x) { + throw new RuntimeException('Division by zero'); + } + echo "Ending Func 'fraction'\n"; + return 1/$x; +} + +function a() +{ + echo "Starting Func 'a'\n"; + env_var_for_expects("GUID_A", newrelic_get_linking_metadata()['span.id'] ?? ''); + Fiber::suspend(); + time_nanosleep(0, 100000000); + echo "Ending Func 'a'\n"; +}; + +$fibera = new Fiber('a'); +$fiberb = new Fiber('b'); + +$fibera->start(); +$fiberb->start(); +$fiberb->resume(); diff --git a/tests/integration/span_events/fibers/test_fiber_noresume_finally.php b/tests/integration/span_events/fibers/test_fiber_noresume_finally.php new file mode 100644 index 000000000..7f0136ac9 --- /dev/null +++ b/tests/integration/span_events/fibers/test_fiber_noresume_finally.php @@ -0,0 +1,216 @@ +resume doesn't happen, but no more execution will occur in the fiber. + +Here is what happens to the suspended fiber and its state: + +Pending finally blocks: Any finally blocks within the fiber's current execution stack will be executed, +allowing for cleanup (e.g., closing file handles, database connections). + +Destructors: The __destruct() methods of any objects held in the fiber’s scope will be called. + +No further execution: The code within the fiber will not complete, and it will not return to t +he Fiber::suspend() call to resume its normal flow. + +Memory release: The fiber's stack and associated memory are freed once the Fiber object has no +remaining references and is collected. + +Output should show that PHP functionality should continue to work as expected. +*/ + +/*SKIPIF +start(); + time_nanosleep(0, 100000000); + $fiberc->resume(); + } catch (RuntimeException $e) { + echo("Caught exception: " . $e->getMessage() . "\n"); + } + echo "Ending Func 'b'\n"; +} + +function fraction($x) { + echo "Starting Func 'fraction'\n"; + env_var_for_expects("GUID_FRACTION", newrelic_get_linking_metadata()['span.id'] ?? ''); + time_nanosleep(0, 100000000); + if (!$x) { + throw new RuntimeException('Division by zero'); + } + echo "Ending Func 'fraction'\n"; + return 1/$x; +} + +function a() +{ + echo "Starting Func 'a'\n"; + env_var_for_expects("GUID_A", newrelic_get_linking_metadata()['span.id'] ?? ''); + try { + Fiber::suspend(); + } finally { + time_nanosleep(0, 100000000); + echo "Ending Func 'a'\n"; + } +}; + +$fibera = new Fiber('a'); +$fiberb = new Fiber('b'); + +$fibera->start(); +$fiberb->start(); +$fiberb->resume(); diff --git a/tests/integration/span_events/fibers/test_txn_stopstart_incaller_after_suspend.php b/tests/integration/span_events/fibers/test_txn_stopstart_incaller_after_suspend.php new file mode 100644 index 000000000..d4da1c535 --- /dev/null +++ b/tests/integration/span_events/fibers/test_txn_stopstart_incaller_after_suspend.php @@ -0,0 +1,221 @@ +", + "guid": "ENV[GUID_A]", + "timestamp": "??", + "duration": "??", + "category": "generic", + "parentId": "ENV[GUID_ROOT]" + }, + {}, + {} + ], + [ + { + "type": "Span", + "traceId": "??", + "transactionId": "??", + "sampled": true, + "priority": "??", + "name": "", + "guid": "ENV[GUID_B]", + "timestamp": "??", + "duration": "??", + "category": "generic", + "parentId": "ENV[GUID_ROOT]" + }, + {}, + {} + ], + [ + { + "traceId": "??", + "duration": "??", + "transactionId": "??", + "name": "OtherTransaction\/Custom\/txn_one", + "guid": "ENV[GUID_TXN_ONE]", + "type": "Span", + "category": "generic", + "priority": "??", + "sampled": true, + "nr.entryPoint": true, + "timestamp": "??", + "transaction.name": "OtherTransaction\/Custom\/txn_one" + }, + {}, + {} + ], + [ + { + "type": "Span", + "traceId": "??", + "transactionId": "??", + "sampled": true, + "priority": "??", + "name": "Custom\/c", + "guid": "ENV[GUID_C]", + "timestamp": "??", + "duration": "??", + "category": "generic", + "parentId": "ENV[GUID_TXN_ONE]" + }, + {}, + { + "error.message": "Uncaught exception 'RuntimeException' with message 'Division by zero' in __FILE__:??", + "error.class": "RuntimeException" + } + ], + [ + { + "type": "Span", + "traceId": "??", + "transactionId": "??", + "sampled": true, + "priority": "??", + "name": "Custom\/fraction", + "guid": "ENV[GUID_FRACTION]", + "timestamp": "??", + "duration": "??", + "category": "generic", + "parentId": "ENV[GUID_C]" + }, + {}, + { + "error.message": "Uncaught exception 'RuntimeException' with message 'Division by zero' in __FILE__:??", + "error.class": "RuntimeException" + } + ] + ] +] +*/ + +/*EXPECT +Starting Func 'a' +Starting Func 'b' +Starting Func 'c' +Starting Func 'fraction' +Caught exception: Division by zero +Ending Func 'b' +Ending Func 'a' +*/ + +require_once(realpath(dirname(__FILE__)) . '/../../../include/tap.php'); +require_once(realpath(dirname(__FILE__)) . '/../../../include/helpers.php'); + + +env_var_for_expects("GUID_ROOT", newrelic_get_linking_metadata()['span.id'] ?? ''); + +function c() +{ + echo "Starting Func 'c'\n"; + env_var_for_expects("GUID_C", newrelic_get_linking_metadata()['span.id'] ?? ''); + time_nanosleep(0, 100000000); + Fiber::suspend(); + echo fraction(0) . "\n"; + echo "Ending Func 'c'\n"; +} + +function b() +{ + echo "Starting Func 'b'\n"; + env_var_for_expects("GUID_B", newrelic_get_linking_metadata()['span.id'] ?? ''); + $fiberc = new Fiber('c'); + Fiber::suspend(); + try { + $fiberc->start(); + time_nanosleep(0, 100000000); + $fiberc->resume(); + } catch (RuntimeException $e) { + echo("Caught exception: " . $e->getMessage() . "\n"); + } + echo "Ending Func 'b'\n"; +} + +function fraction($x) { + echo "Starting Func 'fraction'\n"; + env_var_for_expects("GUID_FRACTION", newrelic_get_linking_metadata()['span.id'] ?? ''); + time_nanosleep(0, 100000000); + if (!$x) { + throw new RuntimeException('Division by zero'); + } + echo "Ending Func 'fraction'\n"; + return 1/$x; +} + +function a() +{ + echo "Starting Func 'a'\n"; + env_var_for_expects("GUID_A", newrelic_get_linking_metadata()['span.id'] ?? ''); + Fiber::suspend(); + time_nanosleep(0, 100000000); + echo "Ending Func 'a'\n"; +}; + +$fibera = new Fiber('a'); +$fiberb = new Fiber('b'); + +$fibera->start(); +$fiberb->start(); +newrelic_end_transaction(); +newrelic_start_transaction(ini_get("newrelic.appname")); +newrelic_name_transaction("txn_one"); +env_var_for_expects("GUID_TXN_ONE", newrelic_get_linking_metadata()['span.id'] ?? ''); +$fiberb->resume(); +$fibera->resume(); diff --git a/tests/integration/span_events/fibers/test_txn_stopstart_infiber_after_suspend.php b/tests/integration/span_events/fibers/test_txn_stopstart_infiber_after_suspend.php new file mode 100644 index 000000000..187e2adac --- /dev/null +++ b/tests/integration/span_events/fibers/test_txn_stopstart_infiber_after_suspend.php @@ -0,0 +1,221 @@ +", + "guid": "ENV[GUID_A]", + "timestamp": "??", + "duration": "??", + "category": "generic", + "parentId": "ENV[GUID_ROOT]" + }, + {}, + {} + ], + [ + { + "type": "Span", + "traceId": "??", + "transactionId": "??", + "sampled": true, + "priority": "??", + "name": "", + "guid": "ENV[GUID_B]", + "timestamp": "??", + "duration": "??", + "category": "generic", + "parentId": "ENV[GUID_ROOT]" + }, + {}, + {} + ], + [ + { + "traceId": "??", + "duration": "??", + "transactionId": "??", + "name": "OtherTransaction\/Custom\/txn_one", + "guid": "ENV[GUID_TXN_ONE]", + "type": "Span", + "category": "generic", + "priority": "??", + "sampled": true, + "nr.entryPoint": true, + "timestamp": "??", + "transaction.name": "OtherTransaction\/Custom\/txn_one" + }, + {}, + {} + ], + [ + { + "type": "Span", + "traceId": "??", + "transactionId": "??", + "sampled": true, + "priority": "??", + "name": "Custom\/c", + "guid": "ENV[GUID_C]", + "timestamp": "??", + "duration": "??", + "category": "generic", + "parentId": "ENV[GUID_TXN_ONE]" + }, + {}, + { + "error.message": "Uncaught exception 'RuntimeException' with message 'Division by zero' in __FILE__:??", + "error.class": "RuntimeException" + } + ], + [ + { + "type": "Span", + "traceId": "??", + "transactionId": "??", + "sampled": true, + "priority": "??", + "name": "Custom\/fraction", + "guid": "ENV[GUID_FRACTION]", + "timestamp": "??", + "duration": "??", + "category": "generic", + "parentId": "ENV[GUID_C]" + }, + {}, + { + "error.message": "Uncaught exception 'RuntimeException' with message 'Division by zero' in __FILE__:??", + "error.class": "RuntimeException" + } + ] + ] +] +*/ + +/*EXPECT +Starting Func 'a' +Starting Func 'b' +Starting Func 'c' +Starting Func 'fraction' +Caught exception: Division by zero +Ending Func 'b' +Ending Func 'a' +*/ + +require_once(realpath(dirname(__FILE__)) . '/../../../include/tap.php'); +require_once(realpath(dirname(__FILE__)) . '/../../../include/helpers.php'); + + +env_var_for_expects("GUID_ROOT", newrelic_get_linking_metadata()['span.id'] ?? ''); + +function c() +{ + echo "Starting Func 'c'\n"; + env_var_for_expects("GUID_C", newrelic_get_linking_metadata()['span.id'] ?? ''); + time_nanosleep(0, 100000000); + Fiber::suspend(); + echo fraction(0) . "\n"; + echo "Ending Func 'c'\n"; +} + +function b() +{ + echo "Starting Func 'b'\n"; + env_var_for_expects("GUID_B", newrelic_get_linking_metadata()['span.id'] ?? ''); + $fiberc = new Fiber('c'); + Fiber::suspend(); + newrelic_end_transaction(); + newrelic_start_transaction(ini_get("newrelic.appname")); + newrelic_name_transaction("txn_one"); + env_var_for_expects("GUID_TXN_ONE", newrelic_get_linking_metadata()['span.id'] ?? ''); + try { + $fiberc->start(); + time_nanosleep(0, 100000000); + $fiberc->resume(); + } catch (RuntimeException $e) { + echo("Caught exception: " . $e->getMessage() . "\n"); + } + echo "Ending Func 'b'\n"; +} + +function fraction($x) { + echo "Starting Func 'fraction'\n"; + env_var_for_expects("GUID_FRACTION", newrelic_get_linking_metadata()['span.id'] ?? ''); + time_nanosleep(0, 100000000); + if (!$x) { + throw new RuntimeException('Division by zero'); + } + echo "Ending Func 'fraction'\n"; + return 1/$x; +} + +function a() +{ + echo "Starting Func 'a'\n"; + env_var_for_expects("GUID_A", newrelic_get_linking_metadata()['span.id'] ?? ''); + Fiber::suspend(); + time_nanosleep(0, 100000000); + echo "Ending Func 'a'\n"; +}; + +$fibera = new Fiber('a'); +$fiberb = new Fiber('b'); + +$fibera->start(); +$fiberb->start(); +$fiberb->resume(); +$fibera->resume(); diff --git a/tests/integration/span_events/fibers/test_txn_stopstart_infiber_before_suspend.php b/tests/integration/span_events/fibers/test_txn_stopstart_infiber_before_suspend.php new file mode 100644 index 000000000..931d72923 --- /dev/null +++ b/tests/integration/span_events/fibers/test_txn_stopstart_infiber_before_suspend.php @@ -0,0 +1,221 @@ +", + "guid": "ENV[GUID_A]", + "timestamp": "??", + "duration": "??", + "category": "generic", + "parentId": "ENV[GUID_ROOT]" + }, + {}, + {} + ], + [ + { + "type": "Span", + "traceId": "??", + "transactionId": "??", + "sampled": true, + "priority": "??", + "name": "", + "guid": "ENV[GUID_B]", + "timestamp": "??", + "duration": "??", + "category": "generic", + "parentId": "ENV[GUID_ROOT]" + }, + {}, + {} + ], + [ + { + "traceId": "??", + "duration": "??", + "transactionId": "??", + "name": "OtherTransaction\/Custom\/txn_one", + "guid": "ENV[GUID_TXN_ONE]", + "type": "Span", + "category": "generic", + "priority": "??", + "sampled": true, + "nr.entryPoint": true, + "timestamp": "??", + "transaction.name": "OtherTransaction\/Custom\/txn_one" + }, + {}, + {} + ], + [ + { + "type": "Span", + "traceId": "??", + "transactionId": "??", + "sampled": true, + "priority": "??", + "name": "Custom\/c", + "guid": "ENV[GUID_C]", + "timestamp": "??", + "duration": "??", + "category": "generic", + "parentId": "ENV[GUID_TXN_ONE]" + }, + {}, + { + "error.message": "Uncaught exception 'RuntimeException' with message 'Division by zero' in __FILE__:??", + "error.class": "RuntimeException" + } + ], + [ + { + "type": "Span", + "traceId": "??", + "transactionId": "??", + "sampled": true, + "priority": "??", + "name": "Custom\/fraction", + "guid": "ENV[GUID_FRACTION]", + "timestamp": "??", + "duration": "??", + "category": "generic", + "parentId": "ENV[GUID_C]" + }, + {}, + { + "error.message": "Uncaught exception 'RuntimeException' with message 'Division by zero' in __FILE__:??", + "error.class": "RuntimeException" + } + ] + ] +] +*/ + +/*EXPECT +Starting Func 'a' +Starting Func 'b' +Starting Func 'c' +Starting Func 'fraction' +Caught exception: Division by zero +Ending Func 'b' +Ending Func 'a' +*/ + +require_once(realpath(dirname(__FILE__)) . '/../../../include/tap.php'); +require_once(realpath(dirname(__FILE__)) . '/../../../include/helpers.php'); + + +env_var_for_expects("GUID_ROOT", newrelic_get_linking_metadata()['span.id'] ?? ''); + +function c() +{ + echo "Starting Func 'c'\n"; + env_var_for_expects("GUID_C", newrelic_get_linking_metadata()['span.id'] ?? ''); + time_nanosleep(0, 100000000); + Fiber::suspend(); + echo fraction(0) . "\n"; + echo "Ending Func 'c'\n"; +} + +function b() +{ + echo "Starting Func 'b'\n"; + env_var_for_expects("GUID_B", newrelic_get_linking_metadata()['span.id'] ?? ''); + $fiberc = new Fiber('c'); + newrelic_end_transaction(); + newrelic_start_transaction(ini_get("newrelic.appname")); + newrelic_name_transaction("txn_one"); + env_var_for_expects("GUID_TXN_ONE", newrelic_get_linking_metadata()['span.id'] ?? ''); + Fiber::suspend(); + try { + $fiberc->start(); + time_nanosleep(0, 100000000); + $fiberc->resume(); + } catch (RuntimeException $e) { + echo("Caught exception: " . $e->getMessage() . "\n"); + } + echo "Ending Func 'b'\n"; +} + +function fraction($x) { + echo "Starting Func 'fraction'\n"; + env_var_for_expects("GUID_FRACTION", newrelic_get_linking_metadata()['span.id'] ?? ''); + time_nanosleep(0, 100000000); + if (!$x) { + throw new RuntimeException('Division by zero'); + } + echo "Ending Func 'fraction'\n"; + return 1/$x; +} + +function a() +{ + echo "Starting Func 'a'\n"; + env_var_for_expects("GUID_A", newrelic_get_linking_metadata()['span.id'] ?? ''); + Fiber::suspend(); + time_nanosleep(0, 100000000); + echo "Ending Func 'a'\n"; +}; + +$fibera = new Fiber('a'); +$fiberb = new Fiber('b'); + +$fibera->start(); +$fiberb->start(); +$fiberb->resume(); +$fibera->resume(); diff --git a/tests/integration/span_events/fibers/test_txn_stopstart_infiber_split_suspend.php b/tests/integration/span_events/fibers/test_txn_stopstart_infiber_split_suspend.php new file mode 100644 index 000000000..e63be7efb --- /dev/null +++ b/tests/integration/span_events/fibers/test_txn_stopstart_infiber_split_suspend.php @@ -0,0 +1,221 @@ +", + "guid": "ENV[GUID_A]", + "timestamp": "??", + "duration": "??", + "category": "generic", + "parentId": "ENV[GUID_ROOT]" + }, + {}, + {} + ], + [ + { + "type": "Span", + "traceId": "??", + "transactionId": "??", + "sampled": true, + "priority": "??", + "name": "", + "guid": "ENV[GUID_B]", + "timestamp": "??", + "duration": "??", + "category": "generic", + "parentId": "ENV[GUID_ROOT]" + }, + {}, + {} + ], + [ + { + "traceId": "??", + "duration": "??", + "transactionId": "??", + "name": "OtherTransaction\/Custom\/txn_one", + "guid": "ENV[GUID_TXN_ONE]", + "type": "Span", + "category": "generic", + "priority": "??", + "sampled": true, + "nr.entryPoint": true, + "timestamp": "??", + "transaction.name": "OtherTransaction\/Custom\/txn_one" + }, + {}, + {} + ], + [ + { + "type": "Span", + "traceId": "??", + "transactionId": "??", + "sampled": true, + "priority": "??", + "name": "Custom\/c", + "guid": "ENV[GUID_C]", + "timestamp": "??", + "duration": "??", + "category": "generic", + "parentId": "ENV[GUID_TXN_ONE]" + }, + {}, + { + "error.message": "Uncaught exception 'RuntimeException' with message 'Division by zero' in __FILE__:??", + "error.class": "RuntimeException" + } + ], + [ + { + "type": "Span", + "traceId": "??", + "transactionId": "??", + "sampled": true, + "priority": "??", + "name": "Custom\/fraction", + "guid": "ENV[GUID_FRACTION]", + "timestamp": "??", + "duration": "??", + "category": "generic", + "parentId": "ENV[GUID_C]" + }, + {}, + { + "error.message": "Uncaught exception 'RuntimeException' with message 'Division by zero' in __FILE__:??", + "error.class": "RuntimeException" + } + ] + ] +] +*/ + +/*EXPECT +Starting Func 'a' +Starting Func 'b' +Starting Func 'c' +Starting Func 'fraction' +Caught exception: Division by zero +Ending Func 'b' +Ending Func 'a' +*/ + +require_once(realpath(dirname(__FILE__)) . '/../../../include/tap.php'); +require_once(realpath(dirname(__FILE__)) . '/../../../include/helpers.php'); + + +env_var_for_expects("GUID_ROOT", newrelic_get_linking_metadata()['span.id'] ?? ''); + +function c() +{ + echo "Starting Func 'c'\n"; + env_var_for_expects("GUID_C", newrelic_get_linking_metadata()['span.id'] ?? ''); + time_nanosleep(0, 100000000); + Fiber::suspend(); + echo fraction(0) . "\n"; + echo "Ending Func 'c'\n"; +} + +function b() +{ + echo "Starting Func 'b'\n"; + env_var_for_expects("GUID_B", newrelic_get_linking_metadata()['span.id'] ?? ''); + $fiberc = new Fiber('c'); + newrelic_end_transaction(); + Fiber::suspend(); + newrelic_start_transaction(ini_get("newrelic.appname")); + newrelic_name_transaction("txn_one"); + env_var_for_expects("GUID_TXN_ONE", newrelic_get_linking_metadata()['span.id'] ?? ''); + try { + $fiberc->start(); + time_nanosleep(0, 100000000); + $fiberc->resume(); + } catch (RuntimeException $e) { + echo("Caught exception: " . $e->getMessage() . "\n"); + } + echo "Ending Func 'b'\n"; +} + +function fraction($x) { + echo "Starting Func 'fraction'\n"; + env_var_for_expects("GUID_FRACTION", newrelic_get_linking_metadata()['span.id'] ?? ''); + time_nanosleep(0, 100000000); + if (!$x) { + throw new RuntimeException('Division by zero'); + } + echo "Ending Func 'fraction'\n"; + return 1/$x; +} + +function a() +{ + echo "Starting Func 'a'\n"; + env_var_for_expects("GUID_A", newrelic_get_linking_metadata()['span.id'] ?? ''); + Fiber::suspend(); + time_nanosleep(0, 100000000); + echo "Ending Func 'a'\n"; +}; + +$fibera = new Fiber('a'); +$fiberb = new Fiber('b'); + +$fibera->start(); +$fiberb->start(); +$fiberb->resume(); +$fibera->resume();