Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
d852f1c
test123
AlliBalliBaba Sep 9, 2025
abdb279
Initial testing.
AlliBalliBaba Sep 14, 2025
8ad2351
FOrmatting.
AlliBalliBaba Sep 14, 2025
65e1137
test
AlliBalliBaba Sep 14, 2025
a699920
test
AlliBalliBaba Sep 14, 2025
a102da8
Adds tests and optimizations.
AlliBalliBaba Sep 16, 2025
6e79380
formatting
AlliBalliBaba Sep 16, 2025
f43c8bb
Removes log.
AlliBalliBaba Sep 16, 2025
7f52e2d
Allows watching with threads.
AlliBalliBaba Sep 17, 2025
9c4cf7e
Throws on task handling failure.
AlliBalliBaba Sep 17, 2025
7438edd
Adds direct dispatching test.
AlliBalliBaba Sep 17, 2025
2387a9d
Allows prepared env.
AlliBalliBaba Sep 17, 2025
6c3e1d6
Fixes race.
AlliBalliBaba Sep 17, 2025
7982b3a
Adds max queue len and more tests.
AlliBalliBaba Sep 18, 2025
2297616
Adjusts queue len.
AlliBalliBaba Sep 18, 2025
7a2bb89
Waits briefly to ensure logs are flushed
AlliBalliBaba Sep 18, 2025
f5e6a04
Fixes small issues.
AlliBalliBaba Sep 18, 2025
eb2b575
Fixes thread attaching.
AlliBalliBaba Sep 18, 2025
0d43eff
Adjusts name.
AlliBalliBaba Sep 18, 2025
c16665a
Allows direct execution on tasks and correctly frees in types_test.
AlliBalliBaba Sep 18, 2025
83c7a88
Cleanup.
AlliBalliBaba Sep 20, 2025
9c36ed4
Merge branch 'main' into feat/task-threads
AlliBalliBaba Sep 20, 2025
639817e
Merge branch 'main' into feat/task-threads
AlliBalliBaba Sep 22, 2025
b8addd7
Merge branch 'main' into feat/task-threads
AlliBalliBaba Oct 1, 2025
99bb21f
Allows setting args with task-workers.
AlliBalliBaba Oct 5, 2025
0c0a0cb
Adds more tests.
AlliBalliBaba Oct 7, 2025
0dff2a2
Adjusts naming.
AlliBalliBaba Oct 7, 2025
df7e77d
Adjusts naming.
AlliBalliBaba Oct 7, 2025
77fec2b
Adds docs.
AlliBalliBaba Oct 7, 2025
268d294
Fixes pinning.
AlliBalliBaba Oct 7, 2025
b23f3f8
Foxes pinning.
AlliBalliBaba Oct 7, 2025
58d1761
Simplifies by removing args.
AlliBalliBaba Oct 11, 2025
05bf065
Uses goValue and phpValue for task dispatching.
AlliBalliBaba Oct 11, 2025
7565628
Allows setting queue len.
AlliBalliBaba Oct 11, 2025
117b415
Merge branch 'main' into feat/task-threads
AlliBalliBaba Oct 11, 2025
02a3b3f
Fixes build error.
AlliBalliBaba Oct 11, 2025
6b9c236
Removes docs (still experimental)
AlliBalliBaba Oct 11, 2025
8a5d489
Returns error messages directly to PHP.
AlliBalliBaba Oct 11, 2025
8144a06
clang-format.
AlliBalliBaba Oct 11, 2025
03d886d
Removes more code.
AlliBalliBaba Oct 11, 2025
a5a9351
Adds sleep back in.
AlliBalliBaba Oct 11, 2025
12b6aae
Prevents test race condition.
AlliBalliBaba Oct 11, 2025
e801a49
Prevents test race condition.
AlliBalliBaba Oct 11, 2025
3f63a4d
Combines frankenphp_handle_task() and frankenphp_handle_request().
AlliBalliBaba Oct 26, 2025
b275cd5
Merge branch 'main' into feat/task-threads
AlliBalliBaba Oct 26, 2025
694b618
Formatting.
AlliBalliBaba Oct 26, 2025
d54f736
simplifications
AlliBalliBaba Oct 26, 2025
02c27fc
Fixes library tests.
AlliBalliBaba Oct 26, 2025
acf423f
adds comments.
AlliBalliBaba Oct 26, 2025
fd40e62
cleanup.
AlliBalliBaba Oct 26, 2025
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
18 changes: 18 additions & 0 deletions caddy/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ type FrankenPHPApp struct {
MaxThreads int `json:"max_threads,omitempty"`
// Workers configures the worker scripts to start.
Workers []workerConfig `json:"workers,omitempty"`
// TaskWorkers configures the task worker scripts to start.
TaskWorkers []workerConfig `json:"task_workers,omitempty"`
// Overwrites the default php ini configuration
PhpIni map[string]string `json:"php_ini,omitempty"`
// The maximum amount of time a request may be stalled waiting for a thread
Expand Down Expand Up @@ -127,6 +129,15 @@ func (f *FrankenPHPApp) Start() error {

opts = append(opts, frankenphp.WithWorkers(w.Name, repl.ReplaceKnown(w.FileName, ""), w.Num, workerOpts...))
}
for _, tw := range f.TaskWorkers {
workerOpts := []frankenphp.WorkerOption{
frankenphp.WithWorkerEnv(tw.Env),
frankenphp.WithWorkerWatchMode(tw.Watch),
frankenphp.AsTaskWorker(true, 0), // TODO: maxQueueLen configurable here?
}

opts = append(opts, frankenphp.WithWorkers(tw.Name, repl.ReplaceKnown(tw.FileName, ""), tw.Num, workerOpts...))
}

frankenphp.Shutdown()
if err := frankenphp.Init(opts...); err != nil {
Expand Down Expand Up @@ -234,6 +245,13 @@ func (f *FrankenPHPApp) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
}
}

case "task_worker":
twc, err := parseWorkerConfig(d)
if err != nil {
return err
}
f.TaskWorkers = append(f.TaskWorkers, twc)

case "worker":
wc, err := parseWorkerConfig(d)
if err != nil {
Expand Down
28 changes: 28 additions & 0 deletions caddy/caddy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1423,3 +1423,31 @@ func TestWorkerMatchDirectiveWithoutFileServer(t *testing.T) {
// the request should completely fall through the php_server module
tester.AssertGetResponse("http://localhost:"+testPort+"/static.txt", http.StatusNotFound, "Request falls through")
}

func TestServerWithTaskWorker(t *testing.T) {
tester := caddytest.NewTester(t)
taskWorker, err := fastabs.FastAbs("../testdata/tasks/task-worker.php")
require.NoError(t, err)
tester.InitServer(`
{
skip_install_trust
admin localhost:2999

frankenphp {
num_threads 2
task_worker `+taskWorker+` {
num 1
}
}
}
`, "caddyfile")

debugState := getDebugState(t, tester)
require.Len(t, debugState.ThreadDebugStates, 2, "there should be 3 threads")
require.Equal(
t,
debugState.ThreadDebugStates[1].Name,
"Task Worker PHP Thread - "+taskWorker,
"the second spawned thread should be the task worker",
)
}
6 changes: 2 additions & 4 deletions cgi.go
Original file line number Diff line number Diff line change
Expand Up @@ -277,13 +277,13 @@ func splitPos(path string, splitPath []string) int {
// See: https://github.com/php/php-src/blob/345e04b619c3bc11ea17ee02cdecad6ae8ce5891/main/SAPI.h#L72
//
//export go_update_request_info
func go_update_request_info(threadIndex C.uintptr_t, info *C.sapi_request_info) C.bool {
func go_update_request_info(threadIndex C.uintptr_t, info *C.sapi_request_info) {
thread := phpThreads[threadIndex]
fc := thread.getRequestContext()
request := fc.request

if request == nil {
return C.bool(fc.worker != nil)
return
}

authUser, authPassword, ok := request.BasicAuth()
Expand Down Expand Up @@ -311,8 +311,6 @@ func go_update_request_info(threadIndex C.uintptr_t, info *C.sapi_request_info)
info.request_uri = thread.pinCString(request.URL.RequestURI())

info.proto_num = C.int(request.ProtoMajor*1000 + request.ProtoMinor)

return C.bool(fc.worker != nil)
}

// SanitizedPathJoin performs filepath.Join(root, reqPath) that
Expand Down
77 changes: 76 additions & 1 deletion frankenphp.c
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ frankenphp_config frankenphp_get_config() {
bool should_filter_var = 0;
__thread uintptr_t thread_index;
__thread bool is_worker_thread = false;
__thread bool is_task_worker_thread = false;
__thread zval *os_environment = NULL;

static void frankenphp_update_request_context() {
Expand All @@ -82,7 +83,12 @@ static void frankenphp_update_request_context() {
/* status It is not reset by zend engine, set it to 200. */
SG(sapi_headers).http_response_code = 200;

is_worker_thread = go_update_request_info(thread_index, &SG(request_info));
go_update_request_info(thread_index, &SG(request_info));
}

void frankenphp_update_thread_context(bool is_worker, bool is_task_worker) {
is_worker_thread = is_worker;
is_task_worker_thread = is_task_worker;
}

static void frankenphp_free_request_context() {
Expand Down Expand Up @@ -411,6 +417,49 @@ PHP_FUNCTION(frankenphp_response_headers) /* {{{ */
}
/* }}} */

/* Handle a message in task worker mode */
static bool frankenphp_handle_message(zend_fcall_info fci,
zend_fcall_info_cache fcc) {
zval *arg = go_frankenphp_worker_handle_task(thread_index);
if (arg == NULL) {
return false;
}

/* Call the PHP func passed to frankenphp_handle_request() */
zval retval = {0};
fci.size = sizeof fci;
fci.retval = &retval;
fci.params = arg;
fci.param_count = 1;
zend_bool status = zend_call_function(&fci, &fcc) == SUCCESS;

if (!status || Z_TYPE(retval) == IS_UNDEF) {
go_frankenphp_finish_task(thread_index, NULL);
zval_ptr_dtor(arg);
} else {
go_frankenphp_finish_task(thread_index, &retval);
}

zval_ptr_dtor(&retval);

/*
* If an exception occurred, print the message to the client before
* exiting
*/
if (EG(exception) && !zend_is_unwind_exit(EG(exception)) &&
!zend_is_graceful_exit(EG(exception))) {
zend_exception_error(EG(exception), E_ERROR);
zend_bailout();
}

zend_try { php_output_end_all(); }
zend_end_try();

zval_ptr_dtor(arg);

return true;
}

PHP_FUNCTION(frankenphp_handle_request) {
zend_fcall_info fci;
zend_fcall_info_cache fcc;
Expand All @@ -420,6 +469,13 @@ PHP_FUNCTION(frankenphp_handle_request) {
ZEND_PARSE_PARAMETERS_END();

if (!is_worker_thread) {

/* thread is a task worker
* handle the message and do not reset globals */
if (is_task_worker_thread) {
bool keep_running = frankenphp_handle_message(fci, fcc);
RETURN_BOOL(keep_running);
}
/* not a worker, throw an error */
zend_throw_exception(
spl_ce_RuntimeException,
Expand Down Expand Up @@ -484,6 +540,25 @@ PHP_FUNCTION(frankenphp_handle_request) {
RETURN_TRUE;
}

PHP_FUNCTION(frankenphp_send_request) {
zval *zv;
char *worker_name = NULL;
size_t worker_name_len = 0;

ZEND_PARSE_PARAMETERS_START(1, 2);
Z_PARAM_ZVAL(zv);
Z_PARAM_OPTIONAL
Z_PARAM_STRING(worker_name, worker_name_len);
ZEND_PARSE_PARAMETERS_END();

char *error = go_frankenphp_send_request(thread_index, zv, worker_name,
worker_name_len);
if (error) {
zend_throw_exception(spl_ce_RuntimeException, error, 0);
RETURN_THROWS();
}
}

PHP_FUNCTION(headers_send) {
zend_long response_code = 200;

Expand Down
36 changes: 34 additions & 2 deletions frankenphp.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ import (
"unsafe"
// debug on Linux
//_ "github.com/ianlancetaylor/cgosymbolizer"

"github.com/dunglas/frankenphp/internal/watcher"
)

type contextKeyStruct struct{}
Expand All @@ -52,7 +54,8 @@ var (
ErrScriptExecution = errors.New("error during PHP script execution")
ErrNotRunning = errors.New("FrankenPHP is not running. For proper configuration visit: https://frankenphp.dev/docs/config/#caddyfile-config")

isRunning bool
isRunning bool
watcherIsEnabled bool

loggerMu sync.RWMutex
logger *slog.Logger
Expand Down Expand Up @@ -151,6 +154,10 @@ func calculateMaxThreads(opt *opt) (int, int, int, error) {
numWorkers += opt.workers[i].num
}

for _, tw := range opt.taskWorkers {
numWorkers += tw.num
}

numThreadsIsSet := opt.numThreads > 0
maxThreadsIsSet := opt.maxThreads != 0
maxThreadsIsAuto := opt.maxThreads < 0 // maxthreads < 0 signifies auto mode (see phpmaintread.go)
Expand Down Expand Up @@ -279,10 +286,23 @@ func Init(options ...Option) error {
convertToRegularThread(getInactivePHPThread())
}

directoriesToWatch := getDirectoriesToWatch(append(opt.workers, opt.taskWorkers...))
watcherIsEnabled = len(directoriesToWatch) > 0 // watcherIsEnabled needs to be set before initWorkers()

if err := initWorkers(opt.workers); err != nil {
return err
}

if err := initTaskWorkers(opt.taskWorkers); err != nil {
return err
}

if watcherIsEnabled {
if err := watcher.InitWatcher(directoriesToWatch, RestartWorkers, logger); err != nil {
return err
}
}

initAutoScaling(mainThread)

ctx := context.Background()
Expand All @@ -300,8 +320,11 @@ func Shutdown() {
return
}

drainWatcher()
if watcherIsEnabled {
watcher.DrainWatcher()
}
drainAutoScaling()
drainTaskWorkers()
drainPHPThreads()

metrics.Shutdown()
Expand Down Expand Up @@ -629,3 +652,12 @@ func timeoutChan(timeout time.Duration) <-chan time.Time {

return time.After(timeout)
}

func getDirectoriesToWatch(workerOpts []workerOpt) []string {
directoriesToWatch := []string{}
for _, w := range workerOpts {
directoriesToWatch = append(directoriesToWatch, w.watch...)
}

return directoriesToWatch
}
2 changes: 2 additions & 0 deletions frankenphp.h
Original file line number Diff line number Diff line change
Expand Up @@ -86,4 +86,6 @@ void frankenphp_register_bulk(

void register_extensions(zend_module_entry *m, int len);

void frankenphp_update_thread_context(bool is_worker, bool is_task_worker);

#endif
2 changes: 2 additions & 0 deletions frankenphp.stub.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

function frankenphp_handle_request(callable $callback): bool {}

function frankenphp_send_request(mixed $task, string $workerName = ''): void {}

function headers_send(int $status = 200): int {}

function frankenphp_finish_request(): bool {}
Expand Down
8 changes: 8 additions & 0 deletions frankenphp_arginfo.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_frankenphp_handle_request, 0, 1,
ZEND_ARG_TYPE_INFO(0, callback, IS_CALLABLE, 0)
ZEND_END_ARG_INFO()

ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_frankenphp_send_request, 0, 1,
IS_VOID, 0)
ZEND_ARG_TYPE_INFO(0, task, IS_MIXED, 0)
ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, worker_name, IS_STRING, 0, "\"\"")
ZEND_END_ARG_INFO()

ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_headers_send, 0, 0, IS_LONG, 0)
ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, status, IS_LONG, 0, "200")
ZEND_END_ARG_INFO()
Expand All @@ -31,6 +37,7 @@ ZEND_END_ARG_INFO()
#define arginfo_apache_response_headers arginfo_frankenphp_response_headers

ZEND_FUNCTION(frankenphp_handle_request);
ZEND_FUNCTION(frankenphp_send_request);
ZEND_FUNCTION(headers_send);
ZEND_FUNCTION(frankenphp_finish_request);
ZEND_FUNCTION(frankenphp_request_headers);
Expand All @@ -39,6 +46,7 @@ ZEND_FUNCTION(frankenphp_response_headers);
// clang-format off
static const zend_function_entry ext_functions[] = {
ZEND_FE(frankenphp_handle_request, arginfo_frankenphp_handle_request)
ZEND_FE(frankenphp_send_request, arginfo_frankenphp_send_request)
ZEND_FE(headers_send, arginfo_headers_send)
ZEND_FE(frankenphp_finish_request, arginfo_frankenphp_finish_request)
ZEND_FALIAS(fastcgi_finish_request, frankenphp_finish_request, arginfo_fastcgi_finish_request)
Expand Down
20 changes: 19 additions & 1 deletion options.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ type opt struct {
numThreads int
maxThreads int
workers []workerOpt
taskWorkers []workerOpt
logger *slog.Logger
metrics Metrics
phpIni map[string]string
Expand All @@ -35,6 +36,8 @@ type workerOpt struct {
env PreparedEnv
watch []string
maxConsecutiveFailures int
isTaskWorker bool
maxQueueLen int
}

// WithNumThreads configures the number of PHP threads to start.
Expand Down Expand Up @@ -80,7 +83,11 @@ func WithWorkers(name string, fileName string, num int, options ...WorkerOption)
}
}

o.workers = append(o.workers, worker)
if worker.isTaskWorker {
o.taskWorkers = append(o.taskWorkers, worker)
} else {
o.workers = append(o.workers, worker)
}

return nil
}
Expand Down Expand Up @@ -141,3 +148,14 @@ func WithMaxWaitTime(maxWaitTime time.Duration) Option {
return nil
}
}

// EXPERIMENTAL: AsTaskWorker configures the worker as a task worker.
// no http requests will be handled.
// no globals resetting will be performed between tasks.
func AsTaskWorker(isTaskWorker bool, maxQueueLen int) WorkerOption {
return func(w *workerOpt) error {
w.isTaskWorker = isTaskWorker
w.maxQueueLen = maxQueueLen
return nil
}
}
4 changes: 4 additions & 0 deletions phpthread.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,10 @@ func (thread *phpThread) pinCString(s string) *C.char {
return thread.pinString(s + "\x00")
}

func (thread *phpThread) updateContext(isWorker bool, isTaskWorker bool) {
C.frankenphp_update_thread_context(C.bool(isWorker), C.bool(isTaskWorker))
}

//export go_frankenphp_before_script_execution
func go_frankenphp_before_script_execution(threadIndex C.uintptr_t) *C.char {
thread := phpThreads[threadIndex]
Expand Down
Loading
Loading