phpthread.go 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179
  1. package frankenphp
  2. // #include "frankenphp.h"
  3. import "C"
  4. import (
  5. "fmt"
  6. "net/http"
  7. "runtime"
  8. "sync"
  9. "time"
  10. "unsafe"
  11. "go.uber.org/zap"
  12. )
  13. // representation of the actual underlying PHP thread
  14. // identified by the index in the phpThreads slice
  15. type phpThread struct {
  16. runtime.Pinner
  17. threadIndex int
  18. knownVariableKeys map[string]*C.zend_string
  19. requestChan chan *http.Request
  20. drainChan chan struct{}
  21. handlerMu *sync.Mutex
  22. handler threadHandler
  23. state *threadState
  24. }
  25. // interface that defines how the callbacks from the C thread should be handled
  26. type threadHandler interface {
  27. name() string
  28. beforeScriptExecution() string
  29. afterScriptExecution(exitStatus int)
  30. getActiveRequest() *http.Request
  31. }
  32. func newPHPThread(threadIndex int) *phpThread {
  33. return &phpThread{
  34. threadIndex: threadIndex,
  35. requestChan: make(chan *http.Request),
  36. handlerMu: &sync.Mutex{},
  37. state: newThreadState(),
  38. }
  39. }
  40. // boot the underlying PHP thread
  41. func (thread *phpThread) boot() {
  42. // thread must be in reserved state to boot
  43. if !thread.state.compareAndSwap(stateReserved, stateBooting) {
  44. logger.Error("thread is not in reserved state", zap.Int("threadIndex", thread.threadIndex), zap.Int("state", int(thread.state.get())))
  45. return
  46. }
  47. // boot threads as inactive
  48. thread.handlerMu.Lock()
  49. thread.handler = &inactiveThread{thread: thread}
  50. thread.drainChan = make(chan struct{})
  51. thread.handlerMu.Unlock()
  52. // start the actual posix thread - TODO: try this with go threads instead
  53. if !C.frankenphp_new_php_thread(C.uintptr_t(thread.threadIndex)) {
  54. logger.Panic("unable to create thread", zap.Int("threadIndex", thread.threadIndex))
  55. }
  56. thread.state.waitFor(stateInactive)
  57. }
  58. // shutdown the underlying PHP thread
  59. func (thread *phpThread) shutdown() {
  60. if !thread.state.requestSafeStateChange(stateShuttingDown) {
  61. // already shutting down or done
  62. return
  63. }
  64. close(thread.drainChan)
  65. thread.state.waitFor(stateDone)
  66. thread.drainChan = make(chan struct{})
  67. // threads go back to the reserved state from which they can be booted again
  68. if mainThread.state.is(stateReady) {
  69. thread.state.set(stateReserved)
  70. }
  71. }
  72. // change the thread handler safely
  73. // must be called from outside the PHP thread
  74. func (thread *phpThread) setHandler(handler threadHandler) {
  75. thread.handlerMu.Lock()
  76. defer thread.handlerMu.Unlock()
  77. if !thread.state.requestSafeStateChange(stateTransitionRequested) {
  78. // no state change allowed == shutdown or done
  79. return
  80. }
  81. close(thread.drainChan)
  82. thread.state.waitFor(stateTransitionInProgress)
  83. thread.handler = handler
  84. thread.drainChan = make(chan struct{})
  85. thread.state.set(stateTransitionComplete)
  86. }
  87. // transition to a new handler safely
  88. // is triggered by setHandler and executed on the PHP thread
  89. func (thread *phpThread) transitionToNewHandler() string {
  90. thread.state.set(stateTransitionInProgress)
  91. thread.state.waitFor(stateTransitionComplete)
  92. // execute beforeScriptExecution of the new handler
  93. return thread.handler.beforeScriptExecution()
  94. }
  95. func (thread *phpThread) getActiveRequest() *http.Request {
  96. return thread.handler.getActiveRequest()
  97. }
  98. // small status message for debugging
  99. func (thread *phpThread) debugStatus() string {
  100. reqState := ""
  101. if waitTime := thread.state.waitTime(); waitTime > 0 {
  102. reqState = fmt.Sprintf(", waiting for %dms", waitTime)
  103. } else if r := thread.getActiveRequest(); r != nil {
  104. fc := r.Context().Value(contextKey).(*FrankenPHPContext)
  105. sinceMs := time.Since(fc.startedAt).Milliseconds()
  106. path := r.URL.Path
  107. if fc.originalRequest != nil {
  108. path = fc.originalRequest.URL.Path
  109. }
  110. reqState = fmt.Sprintf(", handling %s for %dms ", path, sinceMs)
  111. }
  112. return fmt.Sprintf("Thread %d (%s%s) %s", thread.threadIndex, thread.state.name(), reqState, thread.handler.name())
  113. }
  114. // Pin a string that is not null-terminated
  115. // PHP's zend_string may contain null-bytes
  116. func (thread *phpThread) pinString(s string) *C.char {
  117. sData := unsafe.StringData(s)
  118. if sData == nil {
  119. return nil
  120. }
  121. thread.Pin(sData)
  122. return (*C.char)(unsafe.Pointer(sData))
  123. }
  124. // C strings must be null-terminated
  125. func (thread *phpThread) pinCString(s string) *C.char {
  126. return thread.pinString(s + "\x00")
  127. }
  128. //export go_frankenphp_before_script_execution
  129. func go_frankenphp_before_script_execution(threadIndex C.uintptr_t) *C.char {
  130. thread := phpThreads[threadIndex]
  131. scriptName := thread.handler.beforeScriptExecution()
  132. // if no scriptName is passed, shut down
  133. if scriptName == "" {
  134. return nil
  135. }
  136. // overwrite php.ini config (if necessary)
  137. mainThread.overridePHPIni()
  138. // return the name of the PHP script that should be executed
  139. return thread.pinCString(scriptName)
  140. }
  141. //export go_frankenphp_after_script_execution
  142. func go_frankenphp_after_script_execution(threadIndex C.uintptr_t, exitStatus C.int) {
  143. thread := phpThreads[threadIndex]
  144. if exitStatus < 0 {
  145. panic(ScriptExecutionError)
  146. }
  147. thread.handler.afterScriptExecution(int(exitStatus))
  148. // unpin all memory used during script execution
  149. thread.Unpin()
  150. }
  151. //export go_frankenphp_on_thread_shutdown
  152. func go_frankenphp_on_thread_shutdown(threadIndex C.uintptr_t) {
  153. phpThreads[threadIndex].Unpin()
  154. phpThreads[threadIndex].state.set(stateDone)
  155. }