feat: add support for PHP timeouts on Linux (#128)

This commit is contained in:
Kévin Dunglas
2023-03-05 15:47:20 +01:00
committed by GitHub
parent 91190dc135
commit 75cd310292
13 changed files with 416 additions and 122 deletions

View File

@@ -1,9 +1,88 @@
FROM php:8.2-zts-bullseye AS builder
FROM php:8.2-zts-bullseye AS php-base
# Note that this image is based on the official PHP image, once https://github.com/php/php-src/pull/10141 is merged, this stage can be removed
RUN rm -Rf /usr/local/include/php/ /usr/local/lib/libphp.* /usr/local/lib/php/ /usr/local/php/
ENV PHPIZE_DEPS \
autoconf \
dpkg-dev \
file \
g++ \
gcc \
libc-dev \
make \
pkg-config \
re2c
RUN apt-get update && \
apt-get -y --no-install-recommends install \
$PHPIZE_DEPS \
libargon2-dev \
libcurl4-openssl-dev \
libonig-dev \
libreadline-dev \
libsodium-dev \
libsqlite3-dev \
libssl-dev \
libxml2-dev \
zlib1g-dev \
bison \
git \
&& \
apt-get clean
RUN git clone --depth=1 --single-branch --branch=PHP-8.2 https://github.com/php/php-src.git && \
cd php-src && \
# --enable-embed is only necessary to generate libphp.so, we don't use this SAPI directly
./buildconf && \
./configure \
--enable-embed \
--enable-zts \
--disable-zend-signals \
--enable-zend-max-execution-timers \
# --enable-mysqlnd is included here because it's harder to compile after the fact than extensions are (since it's a plugin for several extensions, not an extension in itself)
--enable-mysqlnd \
# make sure invalid --configure-flags are fatal errors instead of just warnings
--enable-option-checking=fatal \
# https://github.com/docker-library/php/issues/439
--with-mhash \
# https://github.com/docker-library/php/issues/822
--with-pic \
# --enable-ftp is included here because ftp_ssl_connect() needs ftp to be compiled statically (see https://github.com/docker-library/php/issues/236)
--enable-ftp \
# --enable-mbstring is included here because otherwise there's no way to get pecl to use it properly (see https://github.com/docker-library/php/issues/195)
--enable-mbstring \
# https://wiki.php.net/rfc/argon2_password_hash
--with-password-argon2 \
# https://wiki.php.net/rfc/libsodium
--with-sodium=shared \
# always build against system sqlite3 (https://github.com/php/php-src/commit/6083a387a81dbbd66d6316a3a12a63f06d5f7109)
--with-pdo-sqlite=/usr \
--with-sqlite3=/usr \
--with-curl \
--with-iconv \
--with-openssl \
--with-readline \
--with-zlib \
# https://github.com/bwoebi/phpdbg-docs/issues/1#issuecomment-163872806 ("phpdbg is primarily a CLI debugger, and is not suitable for debugging an fpm stack.")
--disable-phpdbg \
--with-config-file-path="$PHP_INI_DIR" \
--with-config-file-scan-dir="$PHP_INI_DIR/conf.d" && \
make -j$(nproc) && \
make install && \
rm -Rf php-src/ && \
echo "Creating src archive for building extensions\n" && \
tar -c -f /usr/src/php.tar.xz -J /php-src/ && \
ldconfig && \
php --version
FROM php-base AS builder
COPY --from=golang:1.19-bullseye /usr/local/go/bin/go /usr/local/bin/go
COPY --from=golang:1.19-bullseye /usr/local/go /usr/local/go
# This is required to link the frankenPHP binary to the PHP binary
# This is required to link the FrankenPHP binary to the PHP binary
RUN apt-get update && \
apt-get -y --no-install-recommends install \
libargon2-dev \
@@ -35,7 +114,7 @@ COPY internal internal
COPY testdata testdata
# todo: automate this?
# see https://github.com/docker-library/php/blob/master/8.2-rc/bullseye/zts/Dockerfile#L57-L59 for php values
# see https://github.com/docker-library/php/blob/master/8.2/bullseye/zts/Dockerfile#L57-L59 for PHP values
ENV CGO_LDFLAGS="-lssl -lcrypto -lreadline -largon2 -lcurl -lonig -lz $PHP_LDFLAGS" CGO_CFLAGS=$PHP_CFLAGS CGO_CPPFLAGS=$PHP_CPPFLAGS
RUN cd caddy/frankenphp && \
@@ -57,6 +136,13 @@ RUN echo '<?php phpinfo();' > /app/public/index.php
COPY --from=builder /usr/local/bin/frankenphp /usr/local/bin/frankenphp
COPY --from=builder /etc/Caddyfile /etc/Caddyfile
COPY --from=php-base /usr/local/include/php/ /usr/local/include/php
COPY --from=php-base /usr/local/lib/libphp.* /usr/local/lib
COPY --from=php-base /usr/local/lib/php/ /usr/local/lib/php
COPY --from=php-base /usr/local/php/ /usr/local/php
COPY --from=php-base /usr/local/bin/ /usr/local/bin
COPY --from=php-base /usr/src /usr/src
RUN sed -i 's/php/frankenphp run/g' /usr/local/bin/docker-php-entrypoint
CMD [ "--config", "/etc/Caddyfile" ]

View File

@@ -1,4 +1,84 @@
FROM php:8.2-zts-alpine3.17 AS builder
FROM php:8.2-zts-alpine3.17 AS php-base
# Note that this image is based on the official PHP image, once https://github.com/php/php-src/pull/10141 is merged, this stage can be removed
RUN rm -Rf /usr/local/include/php/ /usr/local/lib/libphp.* /usr/local/lib/php/ /usr/local/php/
ENV PHPIZE_DEPS \
autoconf \
dpkg-dev dpkg \
file \
g++ \
gcc \
libc-dev \
make \
pkgconf \
re2c
RUN apk add --no-cache --virtual .build-deps \
$PHPIZE_DEPS \
argon2-dev \
coreutils \
curl-dev \
readline-dev \
libsodium-dev \
sqlite-dev \
openssl-dev \
libxml2-dev \
gnu-libiconv-dev \
linux-headers \
oniguruma-dev \
bison \
git
RUN git clone --depth=1 --single-branch --branch=PHP-8.2 https://github.com/php/php-src.git
WORKDIR /php-src/
# --enable-embed is only necessary to generate libphp.so, we don't use this SAPI directly
RUN ./buildconf
RUN ./configure \
--enable-embed \
--enable-zts \
--disable-zend-signals \
--enable-zend-max-execution-timers \
# --enable-mysqlnd is included here because it's harder to compile after the fact than extensions are (since it's a plugin for several extensions, not an extension in itself)
--enable-mysqlnd \
# make sure invalid --configure-flags are fatal errors instead of just warnings
--enable-option-checking=fatal \
# https://github.com/docker-library/php/issues/439
--with-mhash \
# https://github.com/docker-library/php/issues/822
--with-pic \
# --enable-ftp is included here because ftp_ssl_connect() needs ftp to be compiled statically (see https://github.com/docker-library/php/issues/236)
--enable-ftp \
# --enable-mbstring is included here because otherwise there's no way to get pecl to use it properly (see https://github.com/docker-library/php/issues/195)
--enable-mbstring \
# https://wiki.php.net/rfc/argon2_password_hash
--with-password-argon2 \
# https://wiki.php.net/rfc/libsodium
--with-sodium=shared \
# always build against system sqlite3 (https://github.com/php/php-src/commit/6083a387a81dbbd66d6316a3a12a63f06d5f7109)
--with-pdo-sqlite=/usr \
--with-sqlite3=/usr \
--with-curl \
--with-iconv \
--with-openssl \
--with-readline \
--with-zlib \
# https://github.com/bwoebi/phpdbg-docs/issues/1#issuecomment-163872806 ("phpdbg is primarily a CLI debugger, and is not suitable for debugging an fpm stack.")
--disable-phpdbg \
--with-config-file-path="$PHP_INI_DIR" \
--with-config-file-scan-dir="$PHP_INI_DIR/conf.d"
RUN make -j$(nproc)
RUN make install
RUN rm -Rf /php-src/
RUN echo "Creating src archive for building extensions\n"
RUN tar -c -f /usr/src/php.tar.xz -J /php-src/
#RUN ldconfig
RUN php --version
FROM php-base AS builder
COPY --from=golang:1.19-alpine3.17 /usr/local/go/bin/go /usr/local/bin/go
COPY --from=golang:1.19-alpine3.17 /usr/local/go /usr/local/go
@@ -56,6 +136,13 @@ RUN echo '<?php phpinfo();' > /app/public/index.php
COPY --from=builder /usr/local/bin/frankenphp /usr/local/bin/frankenphp
COPY --from=builder /etc/Caddyfile /etc/Caddyfile
COPY --from=php-base /usr/local/include/php/ /usr/local/include/php
COPY --from=php-base /usr/local/lib/libphp.* /usr/local/lib
COPY --from=php-base /usr/local/lib/php/ /usr/local/lib/php
COPY --from=php-base /usr/local/php/ /usr/local/php
COPY --from=php-base /usr/local/bin/ /usr/local/bin
COPY --from=php-base /usr/src /usr/src
RUN sed -i 's/php/frankenphp run/g' /usr/local/bin/docker-php-entrypoint
CMD [ "--config", "/etc/Caddyfile" ]

View File

@@ -30,7 +30,8 @@ RUN apt-get update && \
gdb \
valgrind \
neovim \
zsh && \
zsh \
libtool-bin && \
echo 'set auto-load safe-path /' > /root/.gdbinit && \
echo '* soft core unlimited' >> /etc/security/limits.conf \
&& \
@@ -44,6 +45,7 @@ RUN git clone --branch=PHP-8.2 https://github.com/php/php-src.git && \
--enable-embed \
--enable-zts \
--disable-zend-signals \
--enable-zend-max-execution-timers \
--enable-debug && \
make -j$(nproc) && \
make install && \

View File

@@ -20,7 +20,8 @@ Then, configure PHP for your platform:
./configure \
--enable-embed \
--enable-zts \
--disable-zend-signals
--disable-zend-signals \
--enable-zend-max-execution-timers
```
### Mac

View File

@@ -2,16 +2,18 @@
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <php_config.h>
#include <php.h>
#include <SAPI.h>
#include <ext/standard/head.h>
#include <php_main.h>
#include <php_variables.h>
#include <php_output.h>
#include <SAPI.h>
#include <Zend/zend_alloc.h>
#include <Zend/zend_types.h>
#include <Zend/zend_exceptions.h>
#include <Zend/zend_interfaces.h>
#include <ext/standard/head.h>
#include <ext/spl/spl_exceptions.h>
#include "C-Thread-Pool/thpool.h"
#include "C-Thread-Pool/thpool.c"
@@ -23,10 +25,12 @@
ZEND_TSRMLS_CACHE_DEFINE()
#endif
/* Timeouts are currently fundamentally broken with ZTS: https://bugs.php.net/bug.php?id=79464 */
/* Timeouts are currently fundamentally broken with ZTS except on Linux: https://bugs.php.net/bug.php?id=79464 */
#ifndef ZEND_MAX_EXECUTION_TIMERS
static const char HARDCODED_INI[] =
"max_execution_time=0\n"
"max_input_time=-1\n\0";
#endif
static const char *MODULES_TO_RELOAD[] = {
"filter",
@@ -34,8 +38,8 @@ static const char *MODULES_TO_RELOAD[] = {
NULL
};
frankenphp_php_version frankenphp_version() {
return (frankenphp_php_version){
frankenphp_version frankenphp_get_version() {
return (frankenphp_version){
PHP_MAJOR_VERSION,
PHP_MINOR_VERSION,
PHP_RELEASE_VERSION,
@@ -45,26 +49,31 @@ frankenphp_php_version frankenphp_version() {
};
}
int frankenphp_check_version() {
#ifndef ZTS
return -1;
frankenphp_config frankenphp_get_config() {
return (frankenphp_config){
frankenphp_get_version(),
#ifdef ZTS
true,
#else
false,
#endif
if (PHP_VERSION_ID < 80200) {
return -2;
}
#ifdef ZEND_SIGNALS
return -3;
true,
#else
false,
#endif
return SUCCESS;
#ifdef ZEND_MAX_EXECUTION_TIMERS
true,
#else
false,
#endif
};
}
typedef struct frankenphp_server_context {
bool worker;
uintptr_t current_request;
uintptr_t main_request; /* Only available during worker initialization */
uintptr_t main_request;
bool worker_ready;
char *cookie_data;
bool finished;
} frankenphp_server_context;
@@ -82,17 +91,12 @@ static void frankenphp_request_reset() {
}
/* Adapted from php_request_shutdown */
static void frankenphp_worker_request_shutdown(uintptr_t current_request) {
static void frankenphp_worker_request_shutdown() {
/* Flush all output buffers */
zend_try {
php_output_end_all();
} zend_end_try();
/* Reset max_execution_time (no longer executing php code after response sent) */
/*zend_try {
zend_unset_timeout();
} zend_end_try();*/
// TODO: store the list of modules to reload in a global module variable
const char **module_name;
zend_module_entry *module;
@@ -116,10 +120,7 @@ static void frankenphp_worker_request_shutdown(uintptr_t current_request) {
sapi_deactivate();
} zend_end_try();
if (current_request != 0) go_frankenphp_worker_handle_request_end(current_request, true);
zend_set_memory_limit(PG(memory_limit));
}
/* Adapted from php_request_startup() */
@@ -189,23 +190,25 @@ static int frankenphp_worker_request_startup() {
PHP_FUNCTION(frankenphp_finish_request) { /* {{{ */
if (zend_parse_parameters_none() == FAILURE) {
RETURN_THROWS();
RETURN_THROWS();
}
frankenphp_server_context *ctx = SG(server_context);
if(!ctx->finished) {
php_output_end_all();
php_header();
if (ctx->finished) {
RETURN_FALSE;
}
go_frankenphp_worker_handle_request_end(ctx->current_request, false);
ctx->finished = true;
php_output_end_all();
php_header();
RETURN_TRUE;
}
if (ctx->current_request != 0) {
go_frankenphp_finish_request(ctx->main_request, ctx->current_request, false);
}
RETURN_FALSE;
ctx->finished = true;
RETURN_TRUE;
} /* }}} */
PHP_FUNCTION(frankenphp_handle_request) {
@@ -218,24 +221,40 @@ PHP_FUNCTION(frankenphp_handle_request) {
frankenphp_server_context *ctx = SG(server_context);
uintptr_t previous_request = ctx->current_request;
if (ctx->main_request) {
/* Clean the first dummy request created to initialize the worker */
frankenphp_worker_request_shutdown(0);
if (ctx->main_request == 0) {
// not a worker, throw an error
zend_throw_exception(spl_ce_RuntimeException, "frankenphp_handle_request() called while not in worker mode", 0);
RETURN_THROWS();
}
previous_request = ctx->main_request;
if (!ctx->worker_ready) {
/* Clean the first dummy request created to initialize the worker */
frankenphp_worker_request_shutdown();
ctx->worker_ready = true;
/* Mark the worker as ready to handle requests */
go_frankenphp_worker_ready();
}
uintptr_t next_request = go_frankenphp_worker_handle_request_start(previous_request);
#ifdef ZEND_MAX_EXECUTION_TIMERS
// Disable timeouts while waiting for a request to handle
zend_unset_timeout();
#endif
uintptr_t request = go_frankenphp_worker_handle_request_start(ctx->main_request);
if (
frankenphp_worker_request_startup() == FAILURE
/* Shutting down */
|| !next_request
|| !request
) RETURN_FALSE;
#ifdef ZEND_MAX_EXECUTION_TIMERS
// Reset default timeout
// TODO: add support for max_input_time
zend_set_timeout(INI_INT("max_execution_time"), 0);
#endif
/* Call the PHP func */
zval retval = {0};
fci.size = sizeof fci;
@@ -245,10 +264,12 @@ PHP_FUNCTION(frankenphp_handle_request) {
}
/* If an exception occured, print the message to the client before closing the connection */
if (EG(exception))
if (EG(exception)) {
zend_exception_error(EG(exception), E_ERROR);
}
frankenphp_worker_request_shutdown(next_request);
frankenphp_worker_request_shutdown();
go_frankenphp_finish_request(ctx->main_request, request, true);
RETURN_TRUE;
}
@@ -319,7 +340,7 @@ uintptr_t frankenphp_request_shutdown()
{
frankenphp_server_context *ctx = SG(server_context);
if (ctx->worker && ctx->current_request) {
if (ctx->main_request && ctx->current_request) {
frankenphp_request_reset();
}
@@ -358,32 +379,28 @@ int frankenphp_update_server_context(
if (create) {
#ifdef ZTS
/* initial resource fetch */
(void)ts_resource(0);
/* initial resource fetch */
(void)ts_resource(0);
# ifdef PHP_WIN32
ZEND_TSRMLS_CACHE_UPDATE();
ZEND_TSRMLS_CACHE_UPDATE();
# endif
#endif
/* todo: use a pool */
ctx = (frankenphp_server_context *) calloc(1, sizeof(frankenphp_server_context));
if (ctx == NULL) return FAILURE;
/* todo: use a pool */
ctx = (frankenphp_server_context *) calloc(1, sizeof(frankenphp_server_context));
if (ctx == NULL) return FAILURE;
ctx->worker = false;
ctx->current_request = 0;
ctx->main_request = 0;
ctx->cookie_data = NULL;
ctx->finished = false;
ctx->cookie_data = NULL;
ctx->finished = false;
SG(server_context) = ctx;
} else
SG(server_context) = ctx;
} else {
ctx = (frankenphp_server_context *) SG(server_context);
}
ctx->main_request = main_request;
ctx->current_request = current_request;
if (ctx->main_request) ctx->worker = true;
SG(request_info).auth_password = auth_password;
SG(request_info).auth_user = auth_user;
SG(request_info).request_method = request_method;
@@ -541,6 +558,7 @@ sapi_module_struct frankenphp_sapi_module = {
static void *manager_thread(void *arg) {
#ifdef ZTS
// TODO: use tsrm_startup() directly as we now the number of expected threads
php_tsrm_startup();
/*tsrm_error_set(TSRM_ERROR_LEVEL_INFO, NULL);*/
# ifdef PHP_WIN32
@@ -550,8 +568,10 @@ static void *manager_thread(void *arg) {
sapi_startup(&frankenphp_sapi_module);
#ifndef ZEND_MAX_EXECUTION_TIMERS
frankenphp_sapi_module.ini_entries = malloc(sizeof(HARDCODED_INI));
memcpy(frankenphp_sapi_module.ini_entries, HARDCODED_INI, sizeof(HARDCODED_INI));
#endif
frankenphp_sapi_module.startup(&frankenphp_sapi_module);

View File

@@ -130,8 +130,9 @@ type FrankenPHPContext struct {
// Whether the request is already closed by us
closed sync.Once
responseWriter http.ResponseWriter
done chan interface{}
responseWriter http.ResponseWriter
done chan interface{}
currentWorkerRequest cgo.Handle
}
func clientHasClosed(r *http.Request) bool {
@@ -174,9 +175,16 @@ type PHPVersion struct {
VersionID int
}
type PHPConfig struct {
Version PHPVersion
ZTS bool
ZendSignals bool
ZendMaxExecutionTimers bool
}
// Version returns infos about the PHP version.
func Version() PHPVersion {
cVersion := C.frankenphp_version()
cVersion := C.frankenphp_get_version()
return PHPVersion{
int(cVersion.major_version),
@@ -188,6 +196,17 @@ func Version() PHPVersion {
}
}
func Config() PHPConfig {
cConfig := C.frankenphp_get_config()
return PHPConfig{
Version: Version(),
ZTS: bool(cConfig.zts),
ZendSignals: bool(cConfig.zend_signals),
ZendMaxExecutionTimers: bool(cConfig.zend_max_execution_timers),
}
}
// Init starts the PHP runtime and the configured workers.
func Init(options ...Option) error {
if requestChan != nil {
@@ -238,18 +257,19 @@ func Init(options ...Option) error {
return NotEnoughThreads
}
switch C.frankenphp_check_version() {
case -1:
if opt.numThreads != 1 {
opt.numThreads = 1
logger.Warn(`ZTS is not enabled, only 1 thread will be available, recompile PHP using the "--enable-zts" configuration option or performance will be degraded`)
}
config := Config()
case -2:
if config.Version.MajorVersion < 8 || config.Version.MinorVersion < 2 {
return InvalidPHPVersionError
}
case -3:
return ZendSignalsError
if config.ZTS {
if !config.ZendMaxExecutionTimers && runtime.GOOS == "linux" {
logger.Warn(`Zend Timer is not enabled, "--enable-zend-timer" configuration option or timeouts (e.g. "max_execution_time") will not work as expected`)
}
} else {
opt.numThreads = 1
logger.Warn(`ZTS is not enabled, only 1 thread will be available, recompile PHP using the "--enable-zts" configuration option or performance will be degraded`)
}
shutdownWG.Add(1)
@@ -293,7 +313,7 @@ func getLogger() *zap.Logger {
return logger
}
func updateServerContext(request *http.Request, create bool) error {
func updateServerContext(request *http.Request, create bool, mrh C.uintptr_t) error {
fc, ok := FromContext(request.Context())
if !ok {
return InvalidRequestError
@@ -333,9 +353,9 @@ func updateServerContext(request *http.Request, create bool) error {
cRequestUri := C.CString(request.URL.RequestURI())
var rh, mwrh cgo.Handle
var rh cgo.Handle
if fc.responseWriter == nil {
mwrh = cgo.NewHandle(request)
mrh = C.uintptr_t(cgo.NewHandle(request))
} else {
rh = cgo.NewHandle(request)
}
@@ -343,7 +363,7 @@ func updateServerContext(request *http.Request, create bool) error {
ret := C.frankenphp_update_server_context(
C.bool(create),
C.uintptr_t(rh),
C.uintptr_t(mwrh),
mrh,
cMethod,
cQueryString,
@@ -425,7 +445,7 @@ func go_execute_script(rh unsafe.Pointer) {
}
defer maybeCloseContext(fc)
if err := updateServerContext(request, true); err != nil {
if err := updateServerContext(request, true, 0); err != nil {
panic(err)
}
@@ -519,6 +539,7 @@ func go_write_header(rh C.uintptr_t, status C.int) {
return
}
// FIXME: http: superfluous response.WriteHeader call from github.com/dunglas/frankenphp.go_write_header
fc.responseWriter.WriteHeader(int(status))
if status >= 100 && status < 200 {

View File

@@ -2,19 +2,27 @@
#define _FRANKENPPHP_H
#include <stdint.h>
#include <stdbool.h>
#include <Zend/zend_types.h>
typedef struct frankenphp_php_version {
int major_version;
int minor_version;
int release_version;
typedef struct frankenphp_version {
unsigned char major_version;
unsigned char minor_version;
unsigned char release_version;
const char *extra_version;
const char *version;
int version_id;
} frankenphp_php_version;
unsigned long version_id;
} frankenphp_version;
frankenphp_version frankenphp_get_version();
typedef struct frankenphp_config {
frankenphp_version version;
bool zts;
bool zend_signals;
bool zend_max_execution_timers;
} frankenphp_config;
frankenphp_config frankenphp_get_config();
frankenphp_php_version frankenphp_version();
int frankenphp_check_version();
int frankenphp_init(int num_threads);
int frankenphp_update_server_context(

View File

@@ -109,7 +109,9 @@ func BenchmarkHelloWorld(b *testing.B) {
}
func TestHelloWorld_module(t *testing.T) { testHelloWorld(t, nil) }
func TestHelloWorld_worker(t *testing.T) { testHelloWorld(t, &testOptions{workerScript: "index.php"}) }
func TestHelloWorld_worker(t *testing.T) {
testHelloWorld(t, &testOptions{workerScript: "index.php"})
}
func testHelloWorld(t *testing.T, opts *testOptions) {
runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
req := httptest.NewRequest("GET", fmt.Sprintf("http://example.com/index.php?i=%d", i), nil)
@@ -591,6 +593,28 @@ func testFlush(t *testing.T, opts *testOptions) {
}, opts)
}
func TestTimeout_module(t *testing.T) { testTimeout(t, &testOptions{}) }
func TestTimeout_worker(t *testing.T) {
testTimeout(t, &testOptions{workerScript: "timeout.php"})
}
func testTimeout(t *testing.T, opts *testOptions) {
config := frankenphp.Config()
if !config.ZendMaxExecutionTimers {
t.Skip("Zend Timer is not enabled")
}
runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
req := httptest.NewRequest("GET", fmt.Sprintf("http://example.com/timeout.php?i=%d", i), nil)
w := httptest.NewRecorder()
handler(w, req)
resp := w.Result()
body, _ := io.ReadAll(resp.Body)
assert.Contains(t, string(body), fmt.Sprintf("request: %d\n<br />\n<b>Fatal error</b>: Maximum execution time of 1 second exceeded in", i))
}, opts)
}
func TestVersion(t *testing.T) {
v := frankenphp.Version()

2
testdata/Caddyfile vendored
View File

@@ -1,7 +1,7 @@
{
debug
frankenphp {
worker ./phpinfo.php
worker ./timeout.php
}
}

3
testdata/non-worker.php vendored Normal file
View File

@@ -0,0 +1,3 @@
<?php
frankenphp_handle_request(function () {});

14
testdata/timeout.php vendored Normal file
View File

@@ -0,0 +1,14 @@
<?php
require_once __DIR__.'/_executor.php';
return function () {
printf("request: %d\n", $_GET['i'] ?? 'unknown');
set_time_limit(1);
$x = true;
$y = 0;
while ($x) {
$y++;
}
};

View File

@@ -43,11 +43,19 @@ func startWorkers(fileName string, nbWorkers int) error {
for i := 0; i < nbWorkers; i++ {
go func() {
defer shutdownWG.Done()
for {
// Create main dummy request
r, err := http.NewRequest("GET", "", nil)
fc := &FrankenPHPContext{
Env: map[string]string{"SCRIPT_FILENAME": absFileName},
}
r, err := http.NewRequestWithContext(context.WithValue(
context.Background(),
contextKey,
fc,
), "GET", "", nil)
if err != nil {
// TODO: this should never happen, maybe can we remove this block?
m.Lock()
defer m.Unlock()
errors = append(errors, fmt.Errorf("workers %q: unable to create main worker request: %w", absFileName, err))
@@ -55,16 +63,8 @@ func startWorkers(fileName string, nbWorkers int) error {
return
}
ctx := context.WithValue(
r.Context(),
contextKey,
&FrankenPHPContext{
Env: map[string]string{"SCRIPT_FILENAME": absFileName},
},
)
l.Debug("starting", zap.String("worker", absFileName))
if err := ServeHTTP(nil, r.WithContext(ctx)); err != nil {
if err := ServeHTTP(nil, r); err != nil {
m.Lock()
defer m.Unlock()
errors = append(errors, fmt.Errorf("workers %q: unable to start: %w", absFileName, err))
@@ -72,6 +72,13 @@ func startWorkers(fileName string, nbWorkers int) error {
return
}
if fc.currentWorkerRequest != 0 {
// Terminate the pending HTTP request handled by the worker
maybeCloseContext(fc.currentWorkerRequest.Value().(*http.Request).Context().Value(contextKey).(*FrankenPHPContext))
fc.currentWorkerRequest.Delete()
fc.currentWorkerRequest = 0
}
// TODO: make the max restart configurable
if _, ok := workersRequestChans.Load(absFileName); ok {
workersReadyWG.Add(1)
@@ -113,11 +120,11 @@ func go_frankenphp_worker_ready() {
}
//export go_frankenphp_worker_handle_request_start
func go_frankenphp_worker_handle_request_start(rh C.uintptr_t) C.uintptr_t {
previousRequest := cgo.Handle(rh).Value().(*http.Request)
previousFc := previousRequest.Context().Value(contextKey).(*FrankenPHPContext)
func go_frankenphp_worker_handle_request_start(mrh C.uintptr_t) C.uintptr_t {
mainRequest := cgo.Handle(mrh).Value().(*http.Request)
fc := mainRequest.Context().Value(contextKey).(*FrankenPHPContext)
v, ok := workersRequestChans.Load(previousFc.Env["SCRIPT_FILENAME"])
v, ok := workersRequestChans.Load(fc.Env["SCRIPT_FILENAME"])
if !ok {
// Probably shutting down
return 0
@@ -127,40 +134,48 @@ func go_frankenphp_worker_handle_request_start(rh C.uintptr_t) C.uintptr_t {
l := getLogger()
l.Debug("waiting for request", zap.String("worker", previousFc.Env["SCRIPT_FILENAME"]))
l.Debug("waiting for request", zap.String("worker", fc.Env["SCRIPT_FILENAME"]))
r, ok := <-rc
if !ok {
// channel closed, server is shutting down
l.Debug("shutting down", zap.String("worker", previousFc.Env["SCRIPT_FILENAME"]))
l.Debug("shutting down", zap.String("worker", fc.Env["SCRIPT_FILENAME"]))
return 0
}
l.Debug("request handling started", zap.String("worker", previousFc.Env["SCRIPT_FILENAME"]), zap.String("url", r.RequestURI))
if err := updateServerContext(r, false); err != nil {
fc.currentWorkerRequest = cgo.NewHandle(r)
l.Debug("request handling started", zap.String("worker", fc.Env["SCRIPT_FILENAME"]), zap.String("url", r.RequestURI))
if err := updateServerContext(r, false, mrh); err != nil {
// Unexpected error
l.Debug("unexpected error", zap.String("worker", previousFc.Env["SCRIPT_FILENAME"]), zap.String("url", r.RequestURI), zap.Error(err))
l.Debug("unexpected error", zap.String("worker", fc.Env["SCRIPT_FILENAME"]), zap.String("url", r.RequestURI), zap.Error(err))
return 0
}
return C.uintptr_t(cgo.NewHandle(r))
return C.uintptr_t(fc.currentWorkerRequest)
}
//export go_frankenphp_worker_handle_request_end
func go_frankenphp_worker_handle_request_end(rh C.uintptr_t, deleteHandle bool) {
if rh == 0 {
return
}
//export go_frankenphp_finish_request
func go_frankenphp_finish_request(mrh, rh C.uintptr_t, deleteHandle bool) {
rHandle := cgo.Handle(rh)
r := rHandle.Value().(*http.Request)
fc := r.Context().Value(contextKey).(*FrankenPHPContext)
if deleteHandle {
cgo.Handle(rh).Delete()
rHandle.Delete()
cgo.Handle(mrh).Value().(*http.Request).Context().Value(contextKey).(*FrankenPHPContext).currentWorkerRequest = 0
}
maybeCloseContext(fc)
fc.Logger.Debug("request handling finished", zap.String("worker", fc.Env["SCRIPT_FILENAME"]), zap.String("url", r.RequestURI))
var fields []zap.Field
if mrh == 0 {
fields = append(fields, zap.String("worker", fc.Env["SCRIPT_FILENAME"]), zap.String("url", r.RequestURI))
} else {
fields = append(fields, zap.String("url", r.RequestURI))
}
fc.Logger.Debug("request handling finished", fields...)
}

View File

@@ -62,6 +62,19 @@ func TestNonWorkerModeAlwaysWorks(t *testing.T) {
}, &testOptions{workerScript: "phpinfo.php"})
}
func TestCannotCallHandleRequestInNonWorkerMode(t *testing.T) {
runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
req := httptest.NewRequest("GET", "http://example.com/non-worker.php", nil)
w := httptest.NewRecorder()
handler(w, req)
resp := w.Result()
body, _ := io.ReadAll(resp.Body)
assert.Contains(t, string(body), "<b>Fatal error</b>: Uncaught RuntimeException: frankenphp_handle_request() called while not in worker mode")
}, nil)
}
func ExampleServeHTTP_workers() {
if err := frankenphp.Init(
frankenphp.WithWorkers("worker1.php", 4),