@@ -25,6 +25,10 @@ ddog_Endpoint *dogstatsd_endpoint; // always set when ddtrace_endpoint is set
2525struct ddog_InstanceId * ddtrace_sidecar_instance_id ;
2626static uint8_t dd_sidecar_formatted_session_id [36 ];
2727
28+ // Connection mode tracking
29+ dd_sidecar_active_mode_t ddtrace_sidecar_active_mode = DD_SIDECAR_CONNECTION_NONE ;
30+ int32_t ddtrace_sidecar_master_pid = 0 ;
31+
2832static inline void dd_set_endpoint_test_token (ddog_Endpoint * endpoint ) {
2933 if (zai_config_is_initialized ()) {
3034 if (ZSTR_LEN (get_DD_TRACE_AGENT_TEST_SESSION_TOKEN ())) {
@@ -158,6 +162,148 @@ static void dd_sidecar_on_reconnect(ddog_SidecarTransport *transport) {
158162
159163}
160164
165+ // Subprocess connection mode - current default behavior
166+ ddog_SidecarTransport * ddtrace_sidecar_connect_subprocess (void ) {
167+ if (!ddtrace_endpoint ) {
168+ return NULL ;
169+ }
170+ ZEND_ASSERT (dogstatsd_endpoint != NULL );
171+
172+ dd_set_endpoint_test_token (dogstatsd_endpoint );
173+
174+ #ifdef _WIN32
175+ DDOG_PHP_FUNCTION = (const uint8_t * )zend_hash_func ;
176+ #endif
177+
178+ char logpath [MAXPATHLEN ];
179+ int error_fd = atomic_load (& ddtrace_error_log_fd );
180+ if (error_fd == -1 || ddtrace_get_fd_path (error_fd , logpath ) < 0 ) {
181+ * logpath = 0 ;
182+ }
183+
184+ ddog_SidecarTransport * sidecar_transport ;
185+ if (!ddtrace_ffi_try ("Failed connecting to sidecar (subprocess mode)" ,
186+ ddog_sidecar_connect_php (& sidecar_transport , logpath ,
187+ dd_zend_string_to_CharSlice (get_global_DD_TRACE_LOG_LEVEL ()),
188+ get_global_DD_INSTRUMENTATION_TELEMETRY_ENABLED (),
189+ dd_sidecar_on_reconnect ,
190+ ddtrace_endpoint ))) {
191+ return NULL ;
192+ }
193+
194+ dd_sidecar_post_connect (& sidecar_transport , false, logpath );
195+
196+ // Set active mode
197+ ddtrace_sidecar_active_mode = DD_SIDECAR_CONNECTION_SUBPROCESS ;
198+
199+ return sidecar_transport ;
200+ }
201+
202+ // Thread connection mode - fallback when subprocess fails
203+ ddog_SidecarTransport * ddtrace_sidecar_connect_thread (void ) {
204+ if (!ddtrace_endpoint ) {
205+ return NULL ;
206+ }
207+ ZEND_ASSERT (dogstatsd_endpoint != NULL );
208+
209+ #ifndef _WIN32
210+ int32_t current_pid = (int32_t )getpid ();
211+ bool is_master = (ddtrace_sidecar_master_pid == 0 || current_pid == ddtrace_sidecar_master_pid );
212+
213+ if (is_master ) {
214+ // Set master PID
215+ if (ddtrace_sidecar_master_pid == 0 ) {
216+ ddtrace_sidecar_master_pid = current_pid ;
217+ }
218+
219+ // Start master listener thread (only if not already running)
220+ if (!ddog_sidecar_is_master_listener_active (ddtrace_sidecar_master_pid )) {
221+ if (!ddtrace_ffi_try ("Failed starting master listener thread" ,
222+ ddog_sidecar_connect_master (ddtrace_sidecar_master_pid ))) {
223+ LOG (WARN , "Failed to start master listener thread" );
224+ return NULL ;
225+ }
226+
227+ LOG (INFO , "Started master listener thread (PID=%d)" , ddtrace_sidecar_master_pid );
228+ }
229+ }
230+
231+ // Connect as worker to master listener
232+ ddog_SidecarTransport * sidecar_transport ;
233+ if (!ddtrace_ffi_try ("Failed connecting to master listener (thread mode)" ,
234+ ddog_sidecar_connect_worker (ddtrace_sidecar_master_pid , & sidecar_transport ))) {
235+ LOG (WARN , "Failed to connect to master listener" );
236+ return NULL ;
237+ }
238+
239+ char logpath [MAXPATHLEN ];
240+ int error_fd = atomic_load (& ddtrace_error_log_fd );
241+ if (error_fd == -1 || ddtrace_get_fd_path (error_fd , logpath ) < 0 ) {
242+ * logpath = 0 ;
243+ }
244+
245+ dd_sidecar_post_connect (& sidecar_transport , false, logpath );
246+
247+ // Set active mode
248+ ddtrace_sidecar_active_mode = DD_SIDECAR_CONNECTION_THREAD ;
249+
250+ return sidecar_transport ;
251+ #else
252+ // Thread mode not supported on Windows
253+ LOG (ERROR , "Thread-based sidecar connection is not supported on Windows" );
254+ return NULL ;
255+ #endif
256+ }
257+
258+ // Auto-fallback connection logic
259+ ddog_SidecarTransport * ddtrace_sidecar_connect_with_fallback (void ) {
260+ zend_long mode = get_global_DD_TRACE_SIDECAR_CONNECTION_MODE ();
261+ ddog_SidecarTransport * transport = NULL ;
262+
263+ switch (mode ) {
264+ case DD_TRACE_SIDECAR_CONNECTION_MODE_SUBPROCESS :
265+ // Force subprocess only
266+ LOG (INFO , "Sidecar connection mode: subprocess (forced)" );
267+ transport = ddtrace_sidecar_connect_subprocess ();
268+ if (!transport ) {
269+ LOG (ERROR , "Subprocess connection failed (mode=subprocess, no fallback)" );
270+ }
271+ break ;
272+
273+ case DD_TRACE_SIDECAR_CONNECTION_MODE_THREAD :
274+ // Force thread only
275+ LOG (INFO , "Sidecar connection mode: thread (forced)" );
276+ transport = ddtrace_sidecar_connect_thread ();
277+ if (!transport ) {
278+ LOG (ERROR , "Thread connection failed (mode=thread, no fallback)" );
279+ }
280+ break ;
281+
282+ case DD_TRACE_SIDECAR_CONNECTION_MODE_AUTO :
283+ default :
284+ // Try subprocess first
285+ LOG (INFO , "Sidecar connection mode: auto (trying subprocess first)" );
286+ transport = ddtrace_sidecar_connect_subprocess ();
287+
288+ if (transport ) {
289+ LOG (INFO , "Connected to sidecar via subprocess" );
290+ } else {
291+ // Fallback to thread mode
292+ LOG (WARN , "Subprocess connection failed, falling back to thread mode" );
293+ transport = ddtrace_sidecar_connect_thread ();
294+
295+ if (transport ) {
296+ LOG (INFO , "Connected to sidecar via thread (fallback)" );
297+ } else {
298+ LOG (ERROR , "Both subprocess and thread connections failed, sidecar unavailable" );
299+ }
300+ }
301+ break ;
302+ }
303+
304+ return transport ;
305+ }
306+
161307static ddog_SidecarTransport * dd_sidecar_connection_factory_ex (bool is_fork ) {
162308 // Should not happen, unless the agent url is malformed
163309 if (!ddtrace_endpoint ) {
@@ -189,7 +335,20 @@ static ddog_SidecarTransport *dd_sidecar_connection_factory_ex(bool is_fork) {
189335}
190336
191337ddog_SidecarTransport * dd_sidecar_connection_factory (void ) {
192- return dd_sidecar_connection_factory_ex (false);
338+ // Reconnect using the same mode that succeeded initially
339+ switch (ddtrace_sidecar_active_mode ) {
340+ case DD_SIDECAR_CONNECTION_SUBPROCESS :
341+ return ddtrace_sidecar_connect_subprocess ();
342+
343+ case DD_SIDECAR_CONNECTION_THREAD :
344+ return ddtrace_sidecar_connect_thread ();
345+
346+ case DD_SIDECAR_CONNECTION_NONE :
347+ default :
348+ // Shouldn't happen, but fall back to auto mode
349+ LOG (WARN , "Reconnection attempted with no active mode, using fallback logic" );
350+ return ddtrace_sidecar_connect_with_fallback ();
351+ }
193352}
194353
195354bool ddtrace_sidecar_maybe_enable_appsec (bool * appsec_activation , bool * appsec_config ) {
@@ -222,7 +381,8 @@ void ddtrace_sidecar_setup(bool appsec_activation, bool appsec_config) {
222381
223382 ddog_init_remote_config (get_global_DD_INSTRUMENTATION_TELEMETRY_ENABLED (), appsec_activation , appsec_config );
224383
225- ddtrace_sidecar = dd_sidecar_connection_factory ();
384+ // Use fallback connection logic
385+ ddtrace_sidecar = ddtrace_sidecar_connect_with_fallback ();
226386 if (!ddtrace_sidecar ) { // Something went wrong
227387 if (ddtrace_endpoint ) {
228388 dd_free_endpoints ();
@@ -234,6 +394,15 @@ void ddtrace_sidecar_setup(bool appsec_activation, bool appsec_config) {
234394 }
235395}
236396
397+ // Initialize sidecar globals at module init
398+ void ddtrace_sidecar_minit (void ) {
399+ #ifndef _WIN32
400+ if (ddtrace_sidecar_master_pid == 0 ) {
401+ ddtrace_sidecar_master_pid = (int32_t )getpid ();
402+ }
403+ #endif
404+ }
405+
237406void ddtrace_sidecar_ensure_active (void ) {
238407 if (ddtrace_sidecar ) {
239408 ddtrace_sidecar_reconnect (& ddtrace_sidecar , dd_sidecar_connection_factory );
@@ -261,8 +430,29 @@ void ddtrace_sidecar_finalize(bool clear_id) {
261430}
262431
263432void ddtrace_sidecar_shutdown (void ) {
433+ #ifndef _WIN32
434+ // Shutdown master listener if this is the master process and thread mode is active
435+ int32_t current_pid = (int32_t )getpid ();
436+ if (ddtrace_sidecar_active_mode == DD_SIDECAR_CONNECTION_THREAD &&
437+ ddtrace_sidecar_master_pid != 0 &&
438+ current_pid == ddtrace_sidecar_master_pid ) {
439+
440+ // Close worker connection first to avoid deadlock
441+ if (ddtrace_sidecar ) {
442+ ddog_sidecar_transport_drop (ddtrace_sidecar );
443+ ddtrace_sidecar = NULL ;
444+ }
445+
446+ // Then shutdown listener thread
447+ ddtrace_ffi_try ("Failed shutting down master listener" ,
448+ ddog_sidecar_shutdown_master_listener ());
449+ }
450+ #endif
451+
452+ // Standard cleanup
264453 if (ddtrace_sidecar_instance_id ) {
265454 ddog_sidecar_instanceId_drop (ddtrace_sidecar_instance_id );
455+ ddtrace_sidecar_instance_id = NULL ;
266456 }
267457
268458 if (ddtrace_endpoint ) {
@@ -271,7 +461,11 @@ void ddtrace_sidecar_shutdown(void) {
271461
272462 if (ddtrace_sidecar ) {
273463 ddog_sidecar_transport_drop (ddtrace_sidecar );
464+ ddtrace_sidecar = NULL ;
274465 }
466+
467+ // Reset mode
468+ ddtrace_sidecar_active_mode = DD_SIDECAR_CONNECTION_NONE ;
275469}
276470
277471void ddtrace_force_new_instance_id (void ) {
@@ -286,6 +480,19 @@ void ddtrace_reset_sidecar(void) {
286480
287481 if (ddtrace_sidecar ) {
288482 ddog_sidecar_transport_drop (ddtrace_sidecar );
483+ ddtrace_sidecar = NULL ;
484+
485+ // Don't reconnect in thread mode after fork (Option A: documented incompatibility)
486+ if (ddtrace_sidecar_active_mode == DD_SIDECAR_CONNECTION_THREAD ) {
487+ // Sidecar unavailable in child process after fork
488+ LOG (WARN , "Thread mode sidecar cannot be reset after fork, sidecar unavailable" );
489+ if (ddtrace_endpoint ) {
490+ dd_free_endpoints ();
491+ }
492+ return ;
493+ }
494+
495+ // For subprocess mode, reconnect with is_fork=true
289496 ddtrace_sidecar = dd_sidecar_connection_factory_ex (true);
290497 if (!ddtrace_sidecar ) { // Something went wrong
291498 if (ddtrace_endpoint ) {
@@ -596,6 +803,11 @@ void ddtrace_sidecar_rinit(void) {
596803
597804void ddtrace_sidecar_rshutdown (void ) {
598805 ddog_Vec_Tag_drop (DDTRACE_G (active_global_tags ));
806+
807+ // For CLI SAPI, shut down sidecar here (before ASAN checks)
808+ if (strcmp (sapi_module .name , "cli" ) == 0 ) {
809+ ddtrace_sidecar_shutdown ();
810+ }
599811}
600812
601813bool ddtrace_alter_test_session_token (zval * old_value , zval * new_value , zend_string * new_str ) {
0 commit comments