phpthread.go 5.3 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. requestChan chan *http.Request
  19. drainChan chan struct{}
  20. handlerMu sync.Mutex
  21. requestMu 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. state: newThreadState(),
  37. }
  38. }
  39. // boot the underlying PHP thread
  40. func (thread *phpThread) boot() {
  41. // thread must be in reserved state to boot
  42. if !thread.state.compareAndSwap(stateReserved, stateBooting) {
  43. logger.Error("thread is not in reserved state", zap.Int("threadIndex", thread.threadIndex), zap.Int("state", int(thread.state.get())))
  44. return
  45. }
  46. // boot threads as inactive
  47. thread.handlerMu.Lock()
  48. thread.handler = &inactiveThread{thread: thread}
  49. thread.drainChan = make(chan struct{})
  50. thread.handlerMu.Unlock()
  51. // start the actual posix thread - TODO: try this with go threads instead
  52. if !C.frankenphp_new_php_thread(C.uintptr_t(thread.threadIndex)) {
  53. logger.Panic("unable to create thread", zap.Int("threadIndex", thread.threadIndex))
  54. }
  55. thread.state.waitFor(stateInactive)
  56. }
  57. // shutdown the underlying PHP thread
  58. func (thread *phpThread) shutdown() {
  59. if !thread.state.requestSafeStateChange(stateShuttingDown) {
  60. // already shutting down or done
  61. return
  62. }
  63. close(thread.drainChan)
  64. thread.state.waitFor(stateDone)
  65. thread.drainChan = make(chan struct{})
  66. // threads go back to the reserved state from which they can be booted again
  67. if mainThread.state.is(stateReady) {
  68. thread.state.set(stateReserved)
  69. }
  70. }
  71. // change the thread handler safely
  72. // must be called from outside the PHP thread
  73. func (thread *phpThread) setHandler(handler threadHandler) {
  74. thread.handlerMu.Lock()
  75. defer thread.handlerMu.Unlock()
  76. if !thread.state.requestSafeStateChange(stateTransitionRequested) {
  77. // no state change allowed == shutdown or done
  78. return
  79. }
  80. close(thread.drainChan)
  81. thread.state.waitFor(stateTransitionInProgress)
  82. thread.handler = handler
  83. thread.drainChan = make(chan struct{})
  84. thread.state.set(stateTransitionComplete)
  85. }
  86. // transition to a new handler safely
  87. // is triggered by setHandler and executed on the PHP thread
  88. func (thread *phpThread) transitionToNewHandler() string {
  89. thread.state.set(stateTransitionInProgress)
  90. thread.state.waitFor(stateTransitionComplete)
  91. // execute beforeScriptExecution of the new handler
  92. return thread.handler.beforeScriptExecution()
  93. }
  94. func (thread *phpThread) getActiveRequest() *http.Request {
  95. return thread.handler.getActiveRequest()
  96. }
  97. // get the active request from outside the PHP thread
  98. func (thread *phpThread) getActiveRequestSafely() *http.Request {
  99. thread.handlerMu.Lock()
  100. thread.requestMu.Lock()
  101. r := thread.getActiveRequest()
  102. thread.requestMu.Unlock()
  103. thread.handlerMu.Unlock()
  104. return r
  105. }
  106. // small status message for debugging
  107. func (thread *phpThread) debugStatus() string {
  108. reqState := ""
  109. if waitTime := thread.state.waitTime(); waitTime > 0 {
  110. reqState = fmt.Sprintf(", waiting for %dms", waitTime)
  111. } else if r := thread.getActiveRequestSafely(); r != nil {
  112. fc := r.Context().Value(contextKey).(*FrankenPHPContext)
  113. path := r.URL.Path
  114. if fc.originalRequest != nil {
  115. path = fc.originalRequest.URL.Path
  116. }
  117. if fc.responseWriter == nil {
  118. reqState = fmt.Sprintf(", executing worker script: %s ", path)
  119. } else {
  120. sinceMs := time.Since(fc.startedAt).Milliseconds()
  121. reqState = fmt.Sprintf(", handling %s for %dms ", path, sinceMs)
  122. }
  123. }
  124. return fmt.Sprintf("Thread %d (%s%s) %s", thread.threadIndex, thread.state.name(), reqState, thread.handler.name())
  125. }
  126. // Pin a string that is not null-terminated
  127. // PHP's zend_string may contain null-bytes
  128. func (thread *phpThread) pinString(s string) *C.char {
  129. sData := unsafe.StringData(s)
  130. if sData == nil {
  131. return nil
  132. }
  133. thread.Pin(sData)
  134. return (*C.char)(unsafe.Pointer(sData))
  135. }
  136. // C strings must be null-terminated
  137. func (thread *phpThread) pinCString(s string) *C.char {
  138. return thread.pinString(s + "\x00")
  139. }
  140. //export go_frankenphp_before_script_execution
  141. func go_frankenphp_before_script_execution(threadIndex C.uintptr_t) *C.char {
  142. thread := phpThreads[threadIndex]
  143. scriptName := thread.handler.beforeScriptExecution()
  144. // if no scriptName is passed, shut down
  145. if scriptName == "" {
  146. return nil
  147. }
  148. // return the name of the PHP script that should be executed
  149. return thread.pinCString(scriptName)
  150. }
  151. //export go_frankenphp_after_script_execution
  152. func go_frankenphp_after_script_execution(threadIndex C.uintptr_t, exitStatus C.int) {
  153. thread := phpThreads[threadIndex]
  154. if exitStatus < 0 {
  155. panic(ScriptExecutionError)
  156. }
  157. thread.handler.afterScriptExecution(int(exitStatus))
  158. // unpin all memory used during script execution
  159. thread.Unpin()
  160. }
  161. //export go_frankenphp_on_thread_shutdown
  162. func go_frankenphp_on_thread_shutdown(threadIndex C.uintptr_t) {
  163. thread := phpThreads[threadIndex]
  164. thread.Unpin()
  165. thread.state.set(stateDone)
  166. }