phpthread.go 4.6 KB

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