phpthread.go 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192
  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. requestMu sync.Mutex
  23. handler threadHandler
  24. state *threadState
  25. }
  26. // interface that defines how the callbacks from the C thread should be handled
  27. type threadHandler interface {
  28. name() string
  29. beforeScriptExecution() string
  30. afterScriptExecution(exitStatus int)
  31. getActiveRequest() *http.Request
  32. }
  33. func newPHPThread(threadIndex int) *phpThread {
  34. return &phpThread{
  35. threadIndex: threadIndex,
  36. requestChan: make(chan *http.Request),
  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. // get the active request from outside the PHP thread
  99. func (thread *phpThread) getActiveRequestSafely() *http.Request {
  100. thread.handlerMu.Lock()
  101. thread.requestMu.Lock()
  102. r := thread.getActiveRequest()
  103. thread.requestMu.Unlock()
  104. thread.handlerMu.Unlock()
  105. return r
  106. }
  107. // small status message for debugging
  108. func (thread *phpThread) debugStatus() string {
  109. reqState := ""
  110. if waitTime := thread.state.waitTime(); waitTime > 0 {
  111. reqState = fmt.Sprintf(", waiting for %dms", waitTime)
  112. } else if r := thread.getActiveRequestSafely(); r != nil {
  113. fc := r.Context().Value(contextKey).(*FrankenPHPContext)
  114. path := r.URL.Path
  115. if fc.originalRequest != nil {
  116. path = fc.originalRequest.URL.Path
  117. }
  118. if fc.responseWriter == nil {
  119. reqState = fmt.Sprintf(", executing worker script: %s ", path)
  120. } else {
  121. sinceMs := time.Since(fc.startedAt).Milliseconds()
  122. reqState = fmt.Sprintf(", handling %s for %dms ", path, sinceMs)
  123. }
  124. }
  125. return fmt.Sprintf("Thread %d (%s%s) %s", thread.threadIndex, thread.state.name(), reqState, thread.handler.name())
  126. }
  127. // Pin a string that is not null-terminated
  128. // PHP's zend_string may contain null-bytes
  129. func (thread *phpThread) pinString(s string) *C.char {
  130. sData := unsafe.StringData(s)
  131. if sData == nil {
  132. return nil
  133. }
  134. thread.Pin(sData)
  135. return (*C.char)(unsafe.Pointer(sData))
  136. }
  137. // C strings must be null-terminated
  138. func (thread *phpThread) pinCString(s string) *C.char {
  139. return thread.pinString(s + "\x00")
  140. }
  141. //export go_frankenphp_before_script_execution
  142. func go_frankenphp_before_script_execution(threadIndex C.uintptr_t) *C.char {
  143. thread := phpThreads[threadIndex]
  144. scriptName := thread.handler.beforeScriptExecution()
  145. // if no scriptName is passed, shut down
  146. if scriptName == "" {
  147. return nil
  148. }
  149. // return the name of the PHP script that should be executed
  150. return thread.pinCString(scriptName)
  151. }
  152. //export go_frankenphp_after_script_execution
  153. func go_frankenphp_after_script_execution(threadIndex C.uintptr_t, exitStatus C.int) {
  154. thread := phpThreads[threadIndex]
  155. if exitStatus < 0 {
  156. panic(ScriptExecutionError)
  157. }
  158. thread.handler.afterScriptExecution(int(exitStatus))
  159. // unpin all memory used during script execution
  160. thread.Unpin()
  161. }
  162. //export go_frankenphp_on_thread_shutdown
  163. func go_frankenphp_on_thread_shutdown(threadIndex C.uintptr_t) {
  164. phpThreads[threadIndex].Unpin()
  165. phpThreads[threadIndex].state.set(stateDone)
  166. }