Skip to content

feat: macOS support for Zend Max Execution timers #13468

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 24 additions & 8 deletions Zend/Zend.m4
Original file line number Diff line number Diff line change
Expand Up @@ -297,19 +297,35 @@ fi
AC_MSG_CHECKING(whether to enable zend signal handling)
AC_MSG_RESULT($ZEND_SIGNALS)


dnl Enable Zend Max Execution Timers by default on macOS
AS_CASE(["$host_alias"], [*darwin*], [ZEND_MAX_EXECUTION_TIMERS="yes"], [ZEND_MAX_EXECUTION_TIMERS=$ZEND_ZTS])

dnl Don't enable Zend Max Execution Timers by default until PHP 8.3 to not break the ABI
AC_ARG_ENABLE([zend-max-execution-timers],
[AS_HELP_STRING([--enable-zend-max-execution-timers],
[whether to enable zend max execution timers])],
[ZEND_MAX_EXECUTION_TIMERS=$enableval],
[ZEND_MAX_EXECUTION_TIMERS=$ZEND_ZTS])

AS_CASE(["$host_alias"], [*linux*|*freebsd*], [], [ZEND_MAX_EXECUTION_TIMERS='no'])

PHP_CHECK_FUNC(timer_create, rt)
if test "$ac_cv_func_timer_create" != "yes"; then
ZEND_MAX_EXECUTION_TIMERS='no'
fi
[])

AS_CASE(
["$host_alias"],
[*linux*|*freebsd*], [
PHP_CHECK_FUNC(timer_create, rt)
if test "$ac_cv_func_timer_create" != "yes"; then
ZEND_MAX_EXECUTION_TIMERS='no'
fi
],
[*darwin*], [],
[ZEND_MAX_EXECUTION_TIMERS='no']
)

AS_CASE(["$host_alias"], [*darwin*], [], [
PHP_CHECK_FUNC(timer_create, rt)
if test "$ac_cv_func_timer_create" != "yes"; then
ZEND_MAX_EXECUTION_TIMERS='no'
fi
])

if test "$ZEND_MAX_EXECUTION_TIMERS" = "yes"; then
AC_DEFINE(ZEND_MAX_EXECUTION_TIMERS, 1, [Use zend max execution timers])
Expand Down
4 changes: 4 additions & 0 deletions Zend/zend_execute.h
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,10 @@ ZEND_API zend_class_entry *zend_fetch_class(zend_string *class_name, uint32_t fe
ZEND_API zend_class_entry *zend_fetch_class_with_scope(zend_string *class_name, uint32_t fetch_type, zend_class_entry *scope);
ZEND_API zend_class_entry *zend_fetch_class_by_name(zend_string *class_name, zend_string *lcname, uint32_t fetch_type);

#if defined(__APPLE__) && !defined(ZTS)
void zend_timeout_handler(void);
#endif

ZEND_API zend_function * ZEND_FASTCALL zend_fetch_function(zend_string *name);
ZEND_API zend_function * ZEND_FASTCALL zend_fetch_function_str(const char *name, size_t len);
ZEND_API void ZEND_FASTCALL zend_init_func_run_time_cache(zend_op_array *op_array);
Expand Down
21 changes: 15 additions & 6 deletions Zend/zend_execute_API.c
Original file line number Diff line number Diff line change
Expand Up @@ -1401,20 +1401,25 @@ ZEND_API ZEND_NORETURN void ZEND_FASTCALL zend_timeout(void) /* {{{ */

#ifndef ZEND_WIN32
# ifdef ZEND_MAX_EXECUTION_TIMERS
# if defined(__APPLE__) && !defined(ZTS)
void zend_timeout_handler(void) /* {{{ */
# else
static void zend_timeout_handler(int dummy, siginfo_t *si, void *uc) /* {{{ */
# endif
{
#ifdef ZTS
# ifdef ZTS
if (!tsrm_is_managed_thread()) {
fprintf(stderr, "zend_timeout_handler() called in a thread not managed by PHP. The expected signal handler will not be called. This is probably a bug.\n");

return;
}
#endif
# endif

# ifndef __APPLE__
if (si->si_value.sival_ptr != &EG(max_execution_timer_timer)) {
#ifdef MAX_EXECUTION_TIMERS_DEBUG
# ifdef MAX_EXECUTION_TIMERS_DEBUG
fprintf(stderr, "Executing previous handler (if set) for unexpected signal SIGRTMIN received on thread %d\n", (pid_t) syscall(SYS_gettid));
#endif
# endif

if (EG(oldact).sa_sigaction) {
EG(oldact).sa_sigaction(dummy, si, uc);
Expand All @@ -1425,6 +1430,7 @@ static void zend_timeout_handler(int dummy, siginfo_t *si, void *uc) /* {{{ */

return;
}
# endif /* ifndef __APPLE__ */
# else
static void zend_timeout_handler(int dummy) /* {{{ */
{
Expand Down Expand Up @@ -1537,18 +1543,21 @@ static void zend_set_timeout_ex(zend_long seconds, bool reset_signals) /* {{{ */
#elif defined(ZEND_MAX_EXECUTION_TIMERS)
zend_max_execution_timer_settime(seconds);

# if !defined(__APPLE__) || defined(ZTS)
if (reset_signals) {
sigset_t sigset;
struct sigaction act;

act.sa_sigaction = zend_timeout_handler;
sigemptyset(&act.sa_mask);
act.sa_flags = SA_ONSTACK | SA_SIGINFO;
sigaction(SIGRTMIN, &act, NULL);
sigaction(ZEND_MAX_EXECUTION_TIMERS_SIGNAL, &act, NULL);

sigemptyset(&sigset);
sigaddset(&sigset, SIGRTMIN);
sigaddset(&sigset, ZEND_MAX_EXECUTION_TIMERS_SIGNAL);
sigprocmask(SIG_UNBLOCK, &sigset, NULL);
}
# endif
#elif defined(HAVE_SETITIMER)
{
struct itimerval t_r; /* timeout requested */
Expand Down
8 changes: 8 additions & 0 deletions Zend/zend_globals.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@
#include <setjmp.h>
#include <stdint.h>
#include <sys/types.h>
#ifdef __APPLE__
#include <dispatch/dispatch.h>
#endif

#include "zend_globals_macros.h"

Expand Down Expand Up @@ -299,7 +302,12 @@ struct _zend_executor_globals {
#endif

#ifdef ZEND_MAX_EXECUTION_TIMERS
# ifdef __APPLE__
dispatch_source_t max_execution_timer_source;
bool max_execution_timer_suspended;
# else
timer_t max_execution_timer_timer;
# endif
pid_t pid;
struct sigaction oldact;
#endif
Expand Down
140 changes: 119 additions & 21 deletions Zend/zend_max_execution_timer.c
Original file line number Diff line number Diff line change
Expand Up @@ -16,32 +16,129 @@

#ifdef ZEND_MAX_EXECUTION_TIMERS

# ifdef __APPLE__

#include <dispatch/dispatch.h>
# ifdef ZTS
#include <pthread.h>
# else
#include "zend_execute.h"
# endif

#include "zend.h"
#include "zend_globals.h"

// macOS doesn't support timer_create(), fallback to Grand Central Dispatch

static inline void zend_max_execution_timer_handler(void *arg)
{
#ifdef ZTS
pthread_t *tid = (pthread_t *) arg;
pthread_kill(*tid, ZEND_MAX_EXECUTION_TIMERS_SIGNAL);
#else
zend_timeout_handler();
#endif
}

static inline void zend_max_execution_timer_cancel(void *arg)
{
free(arg);
}

ZEND_API void zend_max_execution_timer_init(void) /* {{{ */
{
pid_t pid = getpid();

if (EG(pid) == pid) {
return;
}

dispatch_queue_global_t queue = dispatch_get_global_queue(QOS_CLASS_UTILITY, 0);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

quick question: did you try with using your own queue or at least, does it make any meaningful difference in your opinion e.g.

dispatch_queue_attr_t attr = dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_UTILITY, 0);
dispatch_queue_t queue = dispatch_queue_create("net.php.zend_max_execution_timer", attr);

Copy link
Member Author

@dunglas dunglas Feb 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to the docs, this queue looks adapted to our use case, but I'm not a specialist in Mac specifics.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ACK. Was wondering what is best for, at least, ZTS context.

EG(max_execution_timer_source) = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
if (EG(max_execution_timer_source) == NULL) {
zend_strerror_noreturn(E_ERROR, errno, "Could not create dispatch source");
}

EG(pid) = pid;
EG(max_execution_timer_suspended) = 1;

# ifdef ZTS
pthread_t tid = pthread_self();
pthread_t *ptid = malloc(sizeof(pthread_t));
memcpy(ptid, &tid, sizeof(pthread_t));
dispatch_set_context(EG(max_execution_timer_source), ptid);
#else
//dispatch_set_context(EG(max_execution_timer_source), NULL);
# endif

dispatch_source_set_event_handler_f(EG(max_execution_timer_source), zend_max_execution_timer_handler);
dispatch_source_set_cancel_handler_f(EG(max_execution_timer_source), zend_max_execution_timer_cancel);
} /* }}} */

void zend_max_execution_timer_settime(zend_long seconds) /* {{{ */
{
if (seconds == 0) {
if (!EG(max_execution_timer_suspended)) {
dispatch_suspend(EG(max_execution_timer_source));
EG(max_execution_timer_suspended) = 1;
}

return;
}

dispatch_source_set_timer(
EG(max_execution_timer_source),
dispatch_time(DISPATCH_TIME_NOW, seconds * NSEC_PER_SEC),
seconds * NSEC_PER_SEC,
0
Comment on lines +91 to +93
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we disable recurrence of the timer?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

);
if (EG(max_execution_timer_suspended)) {
dispatch_resume(EG(max_execution_timer_source));
EG(max_execution_timer_suspended) = 0;
}
} /* }}} */

void zend_max_execution_timer_shutdown(void) /* {{{ */
{
/* Don't try to delete a timer created before a call to fork() */
if (EG(pid) != getpid()) {
return;
}

EG(pid) = 0;

dispatch_source_cancel(EG(max_execution_timer_source));
//dispatch_release(EG(max_execution_timer_source));
} /* }}} */

# else

#include <stdio.h>
#include <signal.h>
#include <time.h>
#include <unistd.h>
#include <errno.h>
#include <sys/syscall.h>
#include <sys/types.h>
# ifdef __FreeBSD__
# include <pthread_np.h>
# endif
# ifdef __FreeBSD__
# include <pthread_np.h>
# endif

#include "zend.h"
#include "zend_globals.h"

// Musl Libc defines this macro, glibc does not
// According to "man 2 timer_create" this field should always be available, but it's not: https://sourceware.org/bugzilla/show_bug.cgi?id=27417
# ifndef sigev_notify_thread_id
# define sigev_notify_thread_id _sigev_un._tid
# endif
# ifndef sigev_notify_thread_id
# define sigev_notify_thread_id _sigev_un._tid
# endif

// FreeBSD doesn't support CLOCK_BOOTTIME
# ifdef __FreeBSD__
# define ZEND_MAX_EXECUTION_TIMERS_CLOCK CLOCK_MONOTONIC
# else
# define ZEND_MAX_EXECUTION_TIMERS_CLOCK CLOCK_BOOTTIME
# endif
# ifdef __FreeBSD__
# define ZEND_MAX_EXECUTION_TIMERS_CLOCK CLOCK_MONOTONIC
# else
# define ZEND_MAX_EXECUTION_TIMERS_CLOCK CLOCK_BOOTTIME
# endif

ZEND_API void zend_max_execution_timer_init(void) /* {{{ */
{
Expand All @@ -54,12 +151,12 @@ ZEND_API void zend_max_execution_timer_init(void) /* {{{ */
struct sigevent sev;
sev.sigev_notify = SIGEV_THREAD_ID;
sev.sigev_value.sival_ptr = &EG(max_execution_timer_timer);
sev.sigev_signo = SIGRTMIN;
# ifdef __FreeBSD__
sev.sigev_signo = ZEND_MAX_EXECUTION_TIMERS_SIGNAL;
# ifdef __FreeBSD__
sev.sigev_notify_thread_id = pthread_getthreadid_np();
# else
# else
sev.sigev_notify_thread_id = (pid_t) syscall(SYS_gettid);
# endif
# endif

// Measure wall time instead of CPU time as originally planned now that it is possible https://github.com/php/php-src/pull/6504#issuecomment-1370303727
if (timer_create(ZEND_MAX_EXECUTION_TIMERS_CLOCK, &sev, &EG(max_execution_timer_timer)) != 0) {
Expand All @@ -68,9 +165,9 @@ ZEND_API void zend_max_execution_timer_init(void) /* {{{ */

EG(pid) = pid;

# ifdef MAX_EXECUTION_TIMERS_DEBUG
# ifdef MAX_EXECUTION_TIMERS_DEBUG
fprintf(stderr, "Timer %#jx created on thread %d\n", (uintmax_t) EG(max_execution_timer_timer), sev.sigev_notify_thread_id);
# endif
# endif

sigaction(sev.sigev_signo, NULL, &EG(oldact));
}
Expand All @@ -89,9 +186,9 @@ void zend_max_execution_timer_settime(zend_long seconds) /* {{{ }*/
its.it_value.tv_sec = seconds;
its.it_value.tv_nsec = its.it_interval.tv_sec = its.it_interval.tv_nsec = 0;

# ifdef MAX_EXECUTION_TIMERS_DEBUG
# ifdef MAX_EXECUTION_TIMERS_DEBUG
fprintf(stderr, "Setting timer %#jx on thread %d (%ld seconds)...\n", (uintmax_t) timer, (pid_t) syscall(SYS_gettid), seconds);
# endif
# endif

if (timer_settime(timer, 0, &its, NULL) != 0) {
zend_strerror_noreturn(E_ERROR, errno, "Could not set timer");
Expand All @@ -110,9 +207,9 @@ void zend_max_execution_timer_shutdown(void) /* {{{ */

timer_t timer = EG(max_execution_timer_timer);

# ifdef MAX_EXECUTION_TIMERS_DEBUG
# ifdef MAX_EXECUTION_TIMERS_DEBUG
fprintf(stderr, "Deleting timer %#jx on thread %d...\n", (uintmax_t) timer, (pid_t) syscall(SYS_gettid));
# endif
# endif

int err = timer_delete(timer);
if (err != 0) {
Expand All @@ -121,4 +218,5 @@ void zend_max_execution_timer_shutdown(void) /* {{{ */
}
/* }}}} */

# endif
#endif
6 changes: 6 additions & 0 deletions Zend/zend_max_execution_timer.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@

#include "zend_long.h"

# ifdef __APPLE__
#define ZEND_MAX_EXECUTION_TIMERS_SIGNAL SIGIO
# else
#define ZEND_MAX_EXECUTION_TIMERS_SIGNAL SIGRTMIN
# endif

/* Must be called after calls to fork() */
ZEND_API void zend_max_execution_timer_init(void);
void zend_max_execution_timer_settime(zend_long seconds);
Expand Down
1 change: 1 addition & 0 deletions sapi/fpm/tests/status-listen.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ require_once "tester.inc";

$cfg = <<<EOT
[global]
log_level = debug
error_log = {{FILE:LOG}}
[unconfined]
listen = {{ADDR}}
Expand Down