package frankenphp // #include "frankenphp.h" import "C" import ( "fmt" "sync" "github.com/dunglas/frankenphp/internal/memory" "go.uber.org/zap" ) // represents the main PHP thread // the thread needs to keep running as long as all other threads are running type phpMainThread struct { state *threadState done chan struct{} numThreads int maxThreads int phpIni map[string]string } var ( phpThreads []*phpThread mainThread *phpMainThread ) // start the main PHP thread // start a fixed number of inactive PHP threads // reserve a fixed number of possible PHP threads func initPHPThreads(numThreads int, numMaxThreads int, phpIni map[string]string) (*phpMainThread, error) { mainThread = &phpMainThread{ state: newThreadState(), done: make(chan struct{}), numThreads: numThreads, maxThreads: numMaxThreads, phpIni: phpIni, } // initialize the first thread // this needs to happen before starting the main thread // since some extensions access environment variables on startup // the threadIndex on the main thread defaults to 0 -> phpThreads[0].Pin(...) initialThread := newPHPThread(0) phpThreads = []*phpThread{initialThread} if err := mainThread.start(); err != nil { return nil, err } // initialize all other threads phpThreads = make([]*phpThread, mainThread.maxThreads) phpThreads[0] = initialThread for i := 1; i < mainThread.maxThreads; i++ { phpThreads[i] = newPHPThread(i) } // start the underlying C threads ready := sync.WaitGroup{} ready.Add(numThreads) for i := 0; i < numThreads; i++ { thread := phpThreads[i] go func() { thread.boot() ready.Done() }() } ready.Wait() return mainThread, nil } // ThreadDebugStatus prints the state of all PHP threads - debugging purposes only func ThreadDebugStatus() string { statusMessage := "" reservedThreadCount := 0 for _, thread := range phpThreads { if thread.state.is(stateReserved) { reservedThreadCount++ continue } statusMessage += thread.debugStatus() + "\n" } statusMessage += fmt.Sprintf("%d additional threads can be started at runtime\n", reservedThreadCount) return statusMessage } func drainPHPThreads() { doneWG := sync.WaitGroup{} doneWG.Add(len(phpThreads)) mainThread.state.set(stateShuttingDown) close(mainThread.done) for _, thread := range phpThreads { // shut down all reserved threads if thread.state.compareAndSwap(stateReserved, stateDone) { doneWG.Done() continue } // shut down all active threads go func(thread *phpThread) { thread.shutdown() doneWG.Done() }(thread) } doneWG.Wait() mainThread.state.set(stateDone) mainThread.state.waitFor(stateReserved) phpThreads = nil } func (mainThread *phpMainThread) start() error { if C.frankenphp_new_main_thread(C.int(mainThread.numThreads)) != 0 { return MainThreadCreationError } mainThread.state.waitFor(stateReady) return nil } func getInactivePHPThread() *phpThread { thread := getPHPThreadAtState(stateInactive) if thread != nil { return thread } thread = getPHPThreadAtState(stateReserved) if thread == nil { return nil } thread.boot() return thread } func getPHPThreadAtState(state stateID) *phpThread { for _, thread := range phpThreads { if thread.state.is(state) { return thread } } return nil } //export go_frankenphp_main_thread_is_ready func go_frankenphp_main_thread_is_ready() { mainThread.setAutomaticMaxThreads() if mainThread.maxThreads < mainThread.numThreads { mainThread.maxThreads = mainThread.numThreads } mainThread.state.set(stateReady) mainThread.state.waitFor(stateDone) } // max_threads = auto // Estimate the amount of threads based on php.ini and system memory_limit // If unable to get the system's memory limit, simply double num_threads func (mainThread *phpMainThread) setAutomaticMaxThreads() { if mainThread.maxThreads >= 0 { return } perThreadMemoryLimit := int64(C.frankenphp_get_current_memory_limit()) totalSysMemory := memory.TotalSysMemory() if perThreadMemoryLimit <= 0 || totalSysMemory == 0 { mainThread.maxThreads = mainThread.numThreads * 2 return } maxAllowedThreads := totalSysMemory / uint64(perThreadMemoryLimit) mainThread.maxThreads = int(maxAllowedThreads) logger.Debug("Automatic thread limit", zap.Int("perThreadMemoryLimitMB", int(perThreadMemoryLimit/1024/1024)), zap.Int("maxThreads", mainThread.maxThreads)) } //export go_frankenphp_shutdown_main_thread func go_frankenphp_shutdown_main_thread() { mainThread.state.set(stateReserved) } //export go_get_custom_php_ini func go_get_custom_php_ini() *C.char { if mainThread.phpIni == nil { return nil } // pass the php.ini overrides to PHP before startup // TODO: if needed this would also be possible on a per-thread basis overrides := "" for k, v := range mainThread.phpIni { overrides += fmt.Sprintf("%s=%s\n", k, v) } return C.CString(overrides) }