php_threads_test.go 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175
  1. package frankenphp
  2. import (
  3. "net/http"
  4. "path/filepath"
  5. "sync"
  6. "sync/atomic"
  7. "testing"
  8. "github.com/stretchr/testify/assert"
  9. "go.uber.org/zap"
  10. )
  11. func TestStartAndStopTheMainThreadWithOneInactiveThread(t *testing.T) {
  12. logger = zap.NewNop() // the logger needs to not be nil
  13. assert.NoError(t, initPHPThreads(1)) // reserve 1 thread
  14. assert.Len(t, phpThreads, 1)
  15. assert.Equal(t, 0, phpThreads[0].threadIndex)
  16. assert.True(t, phpThreads[0].state.is(stateInactive))
  17. assert.Nil(t, phpThreads[0].worker)
  18. drainPHPThreads()
  19. assert.Nil(t, phpThreads)
  20. }
  21. // We'll start 100 threads and check that their hooks work correctly
  22. func TestStartAndStop100PHPThreadsThatDoNothing(t *testing.T) {
  23. logger = zap.NewNop() // the logger needs to not be nil
  24. numThreads := 100
  25. readyThreads := atomic.Uint64{}
  26. finishedThreads := atomic.Uint64{}
  27. workingThreads := atomic.Uint64{}
  28. workWG := sync.WaitGroup{}
  29. workWG.Add(numThreads)
  30. assert.NoError(t, initPHPThreads(numThreads))
  31. for i := 0; i < numThreads; i++ {
  32. newThread := getInactivePHPThread()
  33. newThread.setActive(
  34. // onStartup => before the thread is ready
  35. func(thread *phpThread) {
  36. if thread.threadIndex == newThread.threadIndex {
  37. readyThreads.Add(1)
  38. }
  39. },
  40. // beforeScriptExecution => we stop here immediately
  41. func(thread *phpThread) {
  42. if thread.threadIndex == newThread.threadIndex {
  43. workingThreads.Add(1)
  44. }
  45. workWG.Done()
  46. newThread.setInactive()
  47. },
  48. // afterScriptExecution => no script is executed, we shouldn't reach here
  49. func(thread *phpThread, exitStatus int) {
  50. panic("hook afterScriptExecution should not be called here")
  51. },
  52. // onShutdown => after the thread is done
  53. func(thread *phpThread) {
  54. if thread.threadIndex == newThread.threadIndex {
  55. finishedThreads.Add(1)
  56. }
  57. },
  58. )
  59. }
  60. workWG.Wait()
  61. drainPHPThreads()
  62. assert.Equal(t, numThreads, int(readyThreads.Load()))
  63. assert.Equal(t, numThreads, int(workingThreads.Load()))
  64. assert.Equal(t, numThreads, int(finishedThreads.Load()))
  65. }
  66. // This test calls sleep() 10.000 times for 1ms in 100 PHP threads.
  67. func TestSleep10000TimesIn100Threads(t *testing.T) {
  68. logger, _ = zap.NewDevelopment() // the logger needs to not be nil
  69. numThreads := 100
  70. maxExecutions := 10000
  71. executionMutex := sync.Mutex{}
  72. executionCount := 0
  73. scriptPath, _ := filepath.Abs("./testdata/sleep.php")
  74. workWG := sync.WaitGroup{}
  75. workWG.Add(maxExecutions)
  76. assert.NoError(t, initPHPThreads(numThreads))
  77. for i := 0; i < numThreads; i++ {
  78. getInactivePHPThread().setActive(
  79. // onStartup => fake a request on startup (like a worker would do)
  80. func(thread *phpThread) {
  81. r, _ := http.NewRequest(http.MethodGet, "sleep.php", nil)
  82. r, _ = NewRequestWithContext(r, WithRequestDocumentRoot("/", false))
  83. assert.NoError(t, updateServerContext(thread, r, true, false))
  84. thread.mainRequest = r
  85. thread.scriptName = scriptPath
  86. },
  87. // beforeScriptExecution => execute the sleep.php script until we reach maxExecutions
  88. func(thread *phpThread) {
  89. executionMutex.Lock()
  90. if executionCount >= maxExecutions {
  91. executionMutex.Unlock()
  92. thread.setInactive()
  93. return
  94. }
  95. executionCount++
  96. workWG.Done()
  97. executionMutex.Unlock()
  98. },
  99. // afterScriptExecution => check the exit status of the script
  100. func(thread *phpThread, exitStatus int) {
  101. if int(exitStatus) != 0 {
  102. panic("script execution failed: " + scriptPath)
  103. }
  104. },
  105. // onShutdown => nothing to do here
  106. nil,
  107. )
  108. }
  109. workWG.Wait()
  110. drainPHPThreads()
  111. assert.Equal(t, maxExecutions, executionCount)
  112. }
  113. // TODO: Make this test more chaotic
  114. func TestStart100ThreadsAndConvertThemToDifferentThreads10Times(t *testing.T) {
  115. logger = zap.NewNop() // the logger needs to not be nil
  116. numThreads := 100
  117. numConversions := 10
  118. startUpTypes := make([]atomic.Uint64, numConversions)
  119. workTypes := make([]atomic.Uint64, numConversions)
  120. shutdownTypes := make([]atomic.Uint64, numConversions)
  121. workWG := sync.WaitGroup{}
  122. assert.NoError(t, initPHPThreads(numThreads))
  123. for i := 0; i < numConversions; i++ {
  124. workWG.Add(numThreads)
  125. numberOfConversion := i
  126. for j := 0; j < numThreads; j++ {
  127. getInactivePHPThread().setActive(
  128. // onStartup => before the thread is ready
  129. func(thread *phpThread) {
  130. startUpTypes[numberOfConversion].Add(1)
  131. },
  132. // beforeScriptExecution => while the thread is running
  133. func(thread *phpThread) {
  134. workTypes[numberOfConversion].Add(1)
  135. thread.setInactive()
  136. workWG.Done()
  137. },
  138. // afterScriptExecution => we don't execute a script
  139. nil,
  140. // onShutdown => after the thread is done
  141. func(thread *phpThread) {
  142. shutdownTypes[numberOfConversion].Add(1)
  143. },
  144. )
  145. }
  146. workWG.Wait()
  147. }
  148. drainPHPThreads()
  149. // each type of thread needs to have started, worked and stopped the same amount of times
  150. for i := 0; i < numConversions; i++ {
  151. assert.Equal(t, numThreads, int(startUpTypes[i].Load()))
  152. assert.Equal(t, numThreads, int(workTypes[i].Load()))
  153. assert.Equal(t, numThreads, int(shutdownTypes[i].Load()))
  154. }
  155. }