thread-worker.go 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247
  1. package frankenphp
  2. // #include "frankenphp.h"
  3. import "C"
  4. import (
  5. "net/http"
  6. "path/filepath"
  7. "time"
  8. "go.uber.org/zap"
  9. "go.uber.org/zap/zapcore"
  10. )
  11. // representation of a thread assigned to a worker script
  12. // executes the PHP worker script in a loop
  13. // implements the threadHandler interface
  14. type workerThread struct {
  15. state *threadState
  16. thread *phpThread
  17. worker *worker
  18. fakeRequest *http.Request
  19. workerRequest *http.Request
  20. backoff *exponentialBackoff
  21. inRequest bool // true if the worker is currently handling a request
  22. }
  23. func convertToWorkerThread(thread *phpThread, worker *worker) {
  24. thread.setHandler(&workerThread{
  25. state: thread.state,
  26. thread: thread,
  27. worker: worker,
  28. backoff: &exponentialBackoff{
  29. maxBackoff: 1 * time.Second,
  30. minBackoff: 100 * time.Millisecond,
  31. maxConsecutiveFailures: 6,
  32. },
  33. })
  34. worker.attachThread(thread)
  35. }
  36. // beforeScriptExecution returns the name of the script or an empty string on shutdown
  37. func (handler *workerThread) beforeScriptExecution() string {
  38. switch handler.state.get() {
  39. case stateTransitionRequested:
  40. handler.worker.detachThread(handler.thread)
  41. return handler.thread.transitionToNewHandler()
  42. case stateRestarting:
  43. handler.state.set(stateYielding)
  44. handler.state.waitFor(stateReady, stateShuttingDown)
  45. return handler.beforeScriptExecution()
  46. case stateReady, stateTransitionComplete:
  47. setupWorkerScript(handler, handler.worker)
  48. return handler.worker.fileName
  49. case stateShuttingDown:
  50. handler.worker.detachThread(handler.thread)
  51. // signal to stop
  52. return ""
  53. }
  54. panic("unexpected state: " + handler.state.name())
  55. }
  56. func (handler *workerThread) afterScriptExecution(exitStatus int) {
  57. tearDownWorkerScript(handler, exitStatus)
  58. }
  59. func (handler *workerThread) getActiveRequest() *http.Request {
  60. if handler.workerRequest != nil {
  61. return handler.workerRequest
  62. }
  63. return handler.fakeRequest
  64. }
  65. func (handler *workerThread) name() string {
  66. return "Worker PHP Thread - " + handler.worker.fileName
  67. }
  68. func setupWorkerScript(handler *workerThread, worker *worker) {
  69. handler.backoff.wait()
  70. metrics.StartWorker(worker.fileName)
  71. // Create a dummy request to set up the worker
  72. r, err := http.NewRequest(http.MethodGet, filepath.Base(worker.fileName), nil)
  73. if err != nil {
  74. panic(err)
  75. }
  76. r, err = NewRequestWithContext(
  77. r,
  78. WithRequestDocumentRoot(filepath.Dir(worker.fileName), false),
  79. WithRequestPreparedEnv(worker.env),
  80. )
  81. if err != nil {
  82. panic(err)
  83. }
  84. if err := updateServerContext(handler.thread, r, true, false); err != nil {
  85. panic(err)
  86. }
  87. handler.setFakeRequest(r)
  88. if c := logger.Check(zapcore.DebugLevel, "starting"); c != nil {
  89. c.Write(zap.String("worker", worker.fileName), zap.Int("thread", handler.thread.threadIndex))
  90. }
  91. }
  92. func tearDownWorkerScript(handler *workerThread, exitStatus int) {
  93. // if the worker request is not nil, the script might have crashed
  94. // make sure to close the worker request context
  95. if handler.workerRequest != nil {
  96. fc := handler.workerRequest.Context().Value(contextKey).(*FrankenPHPContext)
  97. maybeCloseContext(fc)
  98. handler.setWorkerRequest(nil)
  99. }
  100. fc := handler.fakeRequest.Context().Value(contextKey).(*FrankenPHPContext)
  101. fc.exitStatus = exitStatus
  102. handler.setFakeRequest(nil)
  103. // on exit status 0 we just run the worker script again
  104. worker := handler.worker
  105. if fc.exitStatus == 0 {
  106. // TODO: make the max restart configurable
  107. metrics.StopWorker(worker.fileName, StopReasonRestart)
  108. handler.backoff.recordSuccess()
  109. if c := logger.Check(zapcore.DebugLevel, "restarting"); c != nil {
  110. c.Write(zap.String("worker", worker.fileName))
  111. }
  112. return
  113. }
  114. // TODO: error status
  115. // on exit status 1 we apply an exponential backoff when restarting
  116. metrics.StopWorker(worker.fileName, StopReasonCrash)
  117. if !handler.inRequest && handler.backoff.recordFailure() {
  118. if !watcherIsEnabled {
  119. logger.Panic("too many consecutive worker failures", zap.String("worker", worker.fileName), zap.Int("failures", handler.backoff.failureCount))
  120. }
  121. logger.Warn("many consecutive worker failures", zap.String("worker", worker.fileName), zap.Int("failures", handler.backoff.failureCount))
  122. }
  123. }
  124. // waitForWorkerRequest is called during frankenphp_handle_request in the php worker script.
  125. func (handler *workerThread) waitForWorkerRequest() bool {
  126. // unpin any memory left over from previous requests
  127. handler.thread.Unpin()
  128. handler.state.markAsWaiting(true)
  129. if c := logger.Check(zapcore.DebugLevel, "waiting for request"); c != nil {
  130. c.Write(zap.String("worker", handler.worker.fileName))
  131. }
  132. if handler.state.compareAndSwap(stateTransitionComplete, stateReady) {
  133. metrics.ReadyWorker(handler.worker.fileName)
  134. }
  135. var r *http.Request
  136. select {
  137. case <-handler.thread.drainChan:
  138. if c := logger.Check(zapcore.DebugLevel, "shutting down"); c != nil {
  139. c.Write(zap.String("worker", handler.worker.fileName))
  140. }
  141. // flush the opcache when restarting due to watcher or admin api
  142. // note: this is done right before frankenphp_handle_request() returns 'false'
  143. if handler.state.is(stateRestarting) {
  144. C.frankenphp_reset_opcache()
  145. }
  146. return false
  147. case r = <-handler.thread.requestChan:
  148. case r = <-handler.worker.requestChan:
  149. }
  150. handler.setWorkerRequest(r)
  151. handler.state.markAsWaiting(false)
  152. if c := logger.Check(zapcore.DebugLevel, "request handling started"); c != nil {
  153. c.Write(zap.String("worker", handler.worker.fileName), zap.String("url", r.RequestURI))
  154. }
  155. handler.inRequest = true
  156. if err := updateServerContext(handler.thread, r, false, true); err != nil {
  157. // Unexpected error or invalid request
  158. if c := logger.Check(zapcore.DebugLevel, "unexpected error"); c != nil {
  159. c.Write(zap.String("worker", handler.worker.fileName), zap.String("url", r.RequestURI), zap.Error(err))
  160. }
  161. fc := r.Context().Value(contextKey).(*FrankenPHPContext)
  162. rejectRequest(fc.responseWriter, err.Error())
  163. maybeCloseContext(fc)
  164. handler.workerRequest = nil
  165. return handler.waitForWorkerRequest()
  166. }
  167. return true
  168. }
  169. func (handler *workerThread) setWorkerRequest(r *http.Request) {
  170. handler.thread.requestMu.Lock()
  171. handler.workerRequest = r
  172. handler.thread.requestMu.Unlock()
  173. }
  174. func (handler *workerThread) setFakeRequest(r *http.Request) {
  175. handler.thread.requestMu.Lock()
  176. handler.fakeRequest = r
  177. handler.thread.requestMu.Unlock()
  178. }
  179. // go_frankenphp_worker_handle_request_start is called at the start of every php request served.
  180. //
  181. //export go_frankenphp_worker_handle_request_start
  182. func go_frankenphp_worker_handle_request_start(threadIndex C.uintptr_t) C.bool {
  183. handler := phpThreads[threadIndex].handler.(*workerThread)
  184. return C.bool(handler.waitForWorkerRequest())
  185. }
  186. // go_frankenphp_finish_worker_request is called at the end of every php request served.
  187. //
  188. //export go_frankenphp_finish_worker_request
  189. func go_frankenphp_finish_worker_request(threadIndex C.uintptr_t) {
  190. thread := phpThreads[threadIndex]
  191. r := thread.getActiveRequest()
  192. fc := r.Context().Value(contextKey).(*FrankenPHPContext)
  193. maybeCloseContext(fc)
  194. thread.handler.(*workerThread).workerRequest = nil
  195. if c := fc.logger.Check(zapcore.DebugLevel, "request handling finished"); c != nil {
  196. c.Write(zap.String("worker", fc.scriptFilename), zap.String("url", r.RequestURI))
  197. }
  198. }
  199. // when frankenphp_finish_request() is directly called from PHP
  200. //
  201. //export go_frankenphp_finish_php_request
  202. func go_frankenphp_finish_php_request(threadIndex C.uintptr_t) {
  203. r := phpThreads[threadIndex].getActiveRequest()
  204. fc := r.Context().Value(contextKey).(*FrankenPHPContext)
  205. maybeCloseContext(fc)
  206. if c := fc.logger.Check(zapcore.DebugLevel, "request handling finished"); c != nil {
  207. c.Write(zap.String("url", r.RequestURI))
  208. }
  209. }