2024-12-17 11:28:51 +01:00
package frankenphp
// #include "frankenphp.h"
import "C"
import (
2025-04-29 01:08:15 +02:00
"context"
2025-12-02 23:10:12 +01:00
"fmt"
2025-04-26 17:04:46 +08:00
"log/slog"
2024-12-17 11:28:51 +01:00
"path/filepath"
"time"
2025-09-18 09:21:49 +02:00
"unsafe"
2025-12-02 23:10:12 +01:00
"github.com/dunglas/frankenphp/internal/state"
2024-12-17 11:28:51 +01:00
)
// representation of a thread assigned to a worker script
// executes the PHP worker script in a loop
// implements the threadHandler interface
type workerThread struct {
2025-12-02 23:10:12 +01:00
state * state . ThreadState
2025-11-17 16:32:23 +01:00
thread * phpThread
worker * worker
dummyFrankenPHPContext * frankenPHPContext
dummyContext context . Context
workerFrankenPHPContext * frankenPHPContext
workerContext context . Context
isBootingScript bool // true if the worker has not reached frankenphp_handle_request yet
2025-12-02 23:10:12 +01:00
failureCount int // number of consecutive startup failures
2024-12-17 11:28:51 +01:00
}
func convertToWorkerThread ( thread * phpThread , worker * worker ) {
thread . setHandler ( & workerThread {
state : thread . state ,
thread : thread ,
worker : worker ,
} )
worker . attachThread ( thread )
}
// beforeScriptExecution returns the name of the script or an empty string on shutdown
func ( handler * workerThread ) beforeScriptExecution ( ) string {
2025-12-02 23:10:12 +01:00
switch handler . state . Get ( ) {
case state . TransitionRequested :
2025-10-28 20:37:20 +01:00
if handler . worker . onThreadShutdown != nil {
handler . worker . onThreadShutdown ( handler . thread . threadIndex )
2025-09-18 09:21:49 +02:00
}
2024-12-17 11:28:51 +01:00
handler . worker . detachThread ( handler . thread )
return handler . thread . transitionToNewHandler ( )
2025-12-02 23:10:12 +01:00
case state . Restarting :
2025-10-28 20:37:20 +01:00
if handler . worker . onThreadShutdown != nil {
handler . worker . onThreadShutdown ( handler . thread . threadIndex )
2025-09-18 09:21:49 +02:00
}
2025-12-02 23:10:12 +01:00
handler . state . Set ( state . Yielding )
handler . state . WaitFor ( state . Ready , state . ShuttingDown )
2024-12-17 11:28:51 +01:00
return handler . beforeScriptExecution ( )
2025-12-02 23:10:12 +01:00
case state . Ready , state . TransitionComplete :
handler . thread . updateContext ( true )
2025-10-28 20:37:20 +01:00
if handler . worker . onThreadReady != nil {
handler . worker . onThreadReady ( handler . thread . threadIndex )
2025-09-18 09:21:49 +02:00
}
2025-11-17 16:32:23 +01:00
2024-12-17 11:28:51 +01:00
setupWorkerScript ( handler , handler . worker )
2025-11-17 16:32:23 +01:00
2024-12-17 11:28:51 +01:00
return handler . worker . fileName
2025-12-02 23:10:12 +01:00
case state . ShuttingDown :
2025-10-28 20:37:20 +01:00
if handler . worker . onThreadShutdown != nil {
handler . worker . onThreadShutdown ( handler . thread . threadIndex )
2025-09-18 09:21:49 +02:00
}
2025-02-19 20:39:33 +01:00
handler . worker . detachThread ( handler . thread )
2025-11-17 16:32:23 +01:00
2024-12-17 11:28:51 +01:00
// signal to stop
return ""
}
2025-11-17 16:32:23 +01:00
2025-12-02 23:10:12 +01:00
panic ( "unexpected state: " + handler . state . Name ( ) )
2024-12-17 11:28:51 +01:00
}
func ( handler * workerThread ) afterScriptExecution ( exitStatus int ) {
tearDownWorkerScript ( handler , exitStatus )
}
2025-11-17 16:32:23 +01:00
func ( handler * workerThread ) frankenPHPContext ( ) * frankenPHPContext {
if handler . workerFrankenPHPContext != nil {
return handler . workerFrankenPHPContext
}
return handler . dummyFrankenPHPContext
}
func ( handler * workerThread ) context ( ) context . Context {
2025-03-10 08:44:03 +01:00
if handler . workerContext != nil {
return handler . workerContext
2024-12-17 11:28:51 +01:00
}
2025-03-10 08:44:03 +01:00
return handler . dummyContext
2024-12-17 11:28:51 +01:00
}
2025-02-19 20:39:33 +01:00
func ( handler * workerThread ) name ( ) string {
return "Worker PHP Thread - " + handler . worker . fileName
}
2024-12-17 11:28:51 +01:00
func setupWorkerScript ( handler * workerThread , worker * worker ) {
2025-03-22 19:32:59 +08:00
metrics . StartWorker ( worker . name )
2024-12-17 11:28:51 +01:00
// Create a dummy request to set up the worker
2025-03-10 08:44:03 +01:00
fc , err := newDummyContext (
filepath . Base ( worker . fileName ) ,
2025-11-23 23:14:23 +01:00
worker . requestOptions ... ,
2024-12-17 11:28:51 +01:00
)
if err != nil {
panic ( err )
}
2025-11-17 16:32:23 +01:00
ctx := context . WithValue ( globalCtx , contextKey , fc )
2025-08-25 16:18:20 +02:00
fc . worker = worker
2025-11-17 16:32:23 +01:00
handler . dummyFrankenPHPContext = fc
handler . dummyContext = ctx
2025-03-10 08:44:03 +01:00
handler . isBootingScript = true
2025-11-17 16:32:23 +01:00
if globalLogger . Enabled ( ctx , slog . LevelDebug ) {
globalLogger . LogAttrs ( ctx , slog . LevelDebug , "starting" , slog . String ( "worker" , worker . name ) , slog . Int ( "thread" , handler . thread . threadIndex ) )
}
2024-12-17 11:28:51 +01:00
}
func tearDownWorkerScript ( handler * workerThread , exitStatus int ) {
2025-03-10 08:44:03 +01:00
worker := handler . worker
2025-11-17 16:32:23 +01:00
handler . dummyFrankenPHPContext = nil
2025-03-10 08:44:03 +01:00
handler . dummyContext = nil
2024-12-17 11:28:51 +01:00
// if the worker request is not nil, the script might have crashed
// make sure to close the worker request context
2025-11-17 16:32:23 +01:00
if handler . workerFrankenPHPContext != nil {
handler . workerFrankenPHPContext . closeContext ( )
handler . workerFrankenPHPContext = nil
2025-03-10 08:44:03 +01:00
handler . workerContext = nil
2024-12-17 11:28:51 +01:00
}
// on exit status 0 we just run the worker script again
2025-03-10 22:49:58 +01:00
if exitStatus == 0 && ! handler . isBootingScript {
2025-03-22 19:32:59 +08:00
metrics . StopWorker ( worker . name , StopReasonRestart )
2025-11-17 16:32:23 +01:00
if globalLogger . Enabled ( globalCtx , slog . LevelDebug ) {
globalLogger . LogAttrs ( globalCtx , slog . LevelDebug , "restarting" , slog . String ( "worker" , worker . name ) , slog . Int ( "thread" , handler . thread . threadIndex ) , slog . Int ( "exit_status" , exitStatus ) )
}
2025-05-19 17:13:30 +02:00
2024-12-17 11:28:51 +01:00
return
}
2025-03-10 22:49:58 +01:00
// worker has thrown a fatal error or has not reached frankenphp_handle_request
2026-02-21 17:34:35 +01:00
if handler . isBootingScript {
metrics . StopWorker ( worker . name , StopReasonBootFailure )
} else {
metrics . StopWorker ( worker . name , StopReasonCrash )
}
2025-03-10 22:49:58 +01:00
if ! handler . isBootingScript {
2025-05-19 17:13:30 +02:00
// fatal error (could be due to exit(1), timeouts, etc.)
2025-11-17 16:32:23 +01:00
if globalLogger . Enabled ( globalCtx , slog . LevelDebug ) {
globalLogger . LogAttrs ( globalCtx , slog . LevelDebug , "restarting" , slog . String ( "worker" , worker . name ) , slog . Int ( "thread" , handler . thread . threadIndex ) , slog . Int ( "exit_status" , exitStatus ) )
}
2025-05-19 17:13:30 +02:00
2025-03-10 22:49:58 +01:00
return
}
2025-12-02 23:10:12 +01:00
if worker . maxConsecutiveFailures >= 0 && startupFailChan != nil && ! watcherIsEnabled && handler . failureCount >= worker . maxConsecutiveFailures {
startupFailChan <- fmt . Errorf ( "too many consecutive failures: worker %s has not reached frankenphp_handle_request()" , worker . fileName )
handler . thread . state . Set ( state . ShuttingDown )
return
2025-11-17 16:32:23 +01:00
}
2025-03-10 22:49:58 +01:00
2025-12-02 23:10:12 +01:00
if watcherIsEnabled {
// worker script has probably failed due to script changes while watcher is enabled
if globalLogger . Enabled ( globalCtx , slog . LevelError ) {
globalLogger . LogAttrs ( globalCtx , slog . LevelWarn , "(watcher enabled) worker script has not reached frankenphp_handle_request()" , slog . String ( "worker" , worker . name ) , slog . Int ( "thread" , handler . thread . threadIndex ) )
2024-12-17 11:28:51 +01:00
}
2025-12-02 23:10:12 +01:00
} else {
// rare case where worker script has failed on a restart during normal operation
// this can happen if startup success depends on external resources
2025-11-17 16:32:23 +01:00
if globalLogger . Enabled ( globalCtx , slog . LevelWarn ) {
2025-12-02 23:10:12 +01:00
globalLogger . LogAttrs ( globalCtx , slog . LevelWarn , "worker script has failed on restart" , slog . String ( "worker" , worker . name ) , slog . Int ( "thread" , handler . thread . threadIndex ) , slog . Int ( "failures" , handler . failureCount ) )
2025-11-17 16:32:23 +01:00
}
2024-12-17 11:28:51 +01:00
}
2025-12-02 23:10:12 +01:00
// wait a bit and try again (exponential backoff)
backoffDuration := time . Duration ( handler . failureCount * handler . failureCount * 100 ) * time . Millisecond
if backoffDuration > time . Second {
backoffDuration = time . Second
}
handler . failureCount ++
time . Sleep ( backoffDuration )
2024-12-17 11:28:51 +01:00
}
2025-01-18 19:30:25 +01:00
// waitForWorkerRequest is called during frankenphp_handle_request in the php worker script.
2025-09-18 09:21:49 +02:00
func ( handler * workerThread ) waitForWorkerRequest ( ) ( bool , any ) {
2024-12-17 11:28:51 +01:00
// unpin any memory left over from previous requests
handler . thread . Unpin ( )
2025-11-17 16:32:23 +01:00
if globalLogger . Enabled ( globalCtx , slog . LevelDebug ) {
globalLogger . LogAttrs ( globalCtx , slog . LevelDebug , "waiting for request" , slog . String ( "worker" , handler . worker . name ) , slog . Int ( "thread" , handler . thread . threadIndex ) )
}
2024-12-17 11:28:51 +01:00
2025-03-10 08:44:03 +01:00
// Clear the first dummy request created to initialize the worker
if handler . isBootingScript {
handler . isBootingScript = false
2025-12-02 23:10:12 +01:00
handler . failureCount = 0
2025-03-10 08:44:03 +01:00
if ! C . frankenphp_shutdown_dummy_request ( ) {
panic ( "Not in CGI context" )
}
2026-02-21 17:34:35 +01:00
// worker is truly ready only after reaching frankenphp_handle_request()
metrics . ReadyWorker ( handler . worker . name )
2025-03-10 08:44:03 +01:00
}
2025-12-02 23:10:12 +01:00
if handler . state . Is ( state . TransitionComplete ) {
handler . state . Set ( state . Ready )
2024-12-17 11:28:51 +01:00
}
2025-12-02 23:10:12 +01:00
handler . state . MarkAsWaiting ( true )
2025-02-19 20:39:33 +01:00
2025-11-17 16:32:23 +01:00
var requestCH contextHolder
2024-12-17 11:28:51 +01:00
select {
case <- handler . thread . drainChan :
2025-11-17 16:32:23 +01:00
if globalLogger . Enabled ( globalCtx , slog . LevelDebug ) {
globalLogger . LogAttrs ( globalCtx , slog . LevelDebug , "shutting down" , slog . String ( "worker" , handler . worker . name ) , slog . Int ( "thread" , handler . thread . threadIndex ) )
}
2024-12-17 11:28:51 +01:00
2025-02-19 20:39:33 +01:00
// flush the opcache when restarting due to watcher or admin api
// note: this is done right before frankenphp_handle_request() returns 'false'
2025-12-02 23:10:12 +01:00
if handler . state . Is ( state . Restarting ) {
2024-12-17 11:28:51 +01:00
C . frankenphp_reset_opcache ( )
}
2025-09-18 09:21:49 +02:00
return false , nil
2025-11-17 16:32:23 +01:00
case requestCH = <- handler . thread . requestChan :
case requestCH = <- handler . worker . requestChan :
2024-12-17 11:28:51 +01:00
}
2025-11-17 16:32:23 +01:00
handler . workerContext = requestCH . ctx
handler . workerFrankenPHPContext = requestCH . frankenPHPContext
2025-12-02 23:10:12 +01:00
handler . state . MarkAsWaiting ( false )
2024-12-17 11:28:51 +01:00
2025-11-17 16:32:23 +01:00
if globalLogger . Enabled ( requestCH . ctx , slog . LevelDebug ) {
if handler . workerFrankenPHPContext . request == nil {
globalLogger . LogAttrs ( requestCH . ctx , slog . LevelDebug , "request handling started" , slog . String ( "worker" , handler . worker . name ) , slog . Int ( "thread" , handler . thread . threadIndex ) )
} else {
globalLogger . LogAttrs ( requestCH . ctx , slog . LevelDebug , "request handling started" , slog . String ( "worker" , handler . worker . name ) , slog . Int ( "thread" , handler . thread . threadIndex ) , slog . String ( "url" , handler . workerFrankenPHPContext . request . RequestURI ) )
}
2025-10-09 14:10:09 +02:00
}
2024-12-17 11:28:51 +01:00
2025-11-17 16:32:23 +01:00
return true , handler . workerFrankenPHPContext . handlerParameters
2024-12-17 11:28:51 +01:00
}
2025-01-18 19:30:25 +01:00
// go_frankenphp_worker_handle_request_start is called at the start of every php request served.
//
2024-12-17 11:28:51 +01:00
//export go_frankenphp_worker_handle_request_start
2025-09-18 09:21:49 +02:00
func go_frankenphp_worker_handle_request_start ( threadIndex C . uintptr_t ) ( C . bool , unsafe . Pointer ) {
2024-12-17 11:28:51 +01:00
handler := phpThreads [ threadIndex ] . handler . ( * workerThread )
2025-09-18 09:21:49 +02:00
hasRequest , parameters := handler . waitForWorkerRequest ( )
if parameters != nil {
2025-10-09 14:10:09 +02:00
var ptr unsafe . Pointer
switch p := parameters . ( type ) {
case unsafe . Pointer :
ptr = p
2025-09-18 09:21:49 +02:00
2025-10-09 14:10:09 +02:00
default :
2025-10-09 14:46:15 +02:00
ptr = PHPValue ( p )
2025-10-09 14:10:09 +02:00
}
handler . thread . Pin ( ptr )
return C . bool ( hasRequest ) , ptr
2025-09-18 09:21:49 +02:00
}
return C . bool ( hasRequest ) , nil
2024-12-17 11:28:51 +01:00
}
2025-01-18 19:30:25 +01:00
// go_frankenphp_finish_worker_request is called at the end of every php request served.
//
2024-12-17 11:28:51 +01:00
//export go_frankenphp_finish_worker_request
2025-09-18 09:21:49 +02:00
func go_frankenphp_finish_worker_request ( threadIndex C . uintptr_t , retval * C . zval ) {
2024-12-17 11:28:51 +01:00
thread := phpThreads [ threadIndex ]
2025-11-17 16:32:23 +01:00
ctx := thread . context ( )
fc := ctx . Value ( contextKey ) . ( * frankenPHPContext )
2025-09-18 09:21:49 +02:00
if retval != nil {
2025-10-21 11:20:54 +02:00
r , err := GoValue [ any ] ( unsafe . Pointer ( retval ) )
2025-11-17 16:32:23 +01:00
if err != nil && globalLogger . Enabled ( ctx , slog . LevelError ) {
globalLogger . LogAttrs ( ctx , slog . LevelError , "cannot convert return value" , slog . Any ( "error" , err ) , slog . Int ( "thread" , thread . threadIndex ) )
2025-10-21 11:20:54 +02:00
}
fc . handlerReturn = r
2025-09-18 09:21:49 +02:00
}
2024-12-17 11:28:51 +01:00
2025-03-10 08:44:03 +01:00
fc . closeContext ( )
2025-11-17 16:32:23 +01:00
thread . handler . ( * workerThread ) . workerFrankenPHPContext = nil
2025-03-10 08:44:03 +01:00
thread . handler . ( * workerThread ) . workerContext = nil
2024-12-17 11:28:51 +01:00
2025-11-17 16:32:23 +01:00
if globalLogger . Enabled ( ctx , slog . LevelDebug ) {
if fc . request == nil {
fc . logger . LogAttrs ( ctx , slog . LevelDebug , "request handling finished" , slog . String ( "worker" , fc . worker . name ) , slog . Int ( "thread" , thread . threadIndex ) )
} else {
fc . logger . LogAttrs ( ctx , slog . LevelDebug , "request handling finished" , slog . String ( "worker" , fc . worker . name ) , slog . Int ( "thread" , thread . threadIndex ) , slog . String ( "url" , fc . request . RequestURI ) )
}
2025-10-09 14:10:09 +02:00
}
2024-12-17 11:28:51 +01:00
}
// when frankenphp_finish_request() is directly called from PHP
//
//export go_frankenphp_finish_php_request
func go_frankenphp_finish_php_request ( threadIndex C . uintptr_t ) {
2025-05-20 10:10:46 +02:00
thread := phpThreads [ threadIndex ]
2025-11-17 16:32:23 +01:00
fc := thread . frankenPHPContext ( )
2025-05-20 10:10:46 +02:00
2025-03-10 08:44:03 +01:00
fc . closeContext ( )
2024-12-17 11:28:51 +01:00
2025-11-17 16:32:23 +01:00
ctx := thread . context ( )
if fc . logger . Enabled ( ctx , slog . LevelDebug ) {
fc . logger . LogAttrs ( ctx , slog . LevelDebug , "request handling finished" , slog . Int ( "thread" , thread . threadIndex ) , slog . String ( "url" , fc . request . RequestURI ) )
}
2024-12-17 11:28:51 +01:00
}