phpmainthread_test.go 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174
  1. package frankenphp
  2. import (
  3. "io"
  4. "math/rand/v2"
  5. "net/http/httptest"
  6. "path/filepath"
  7. "sync"
  8. "sync/atomic"
  9. "testing"
  10. "time"
  11. "github.com/stretchr/testify/assert"
  12. "go.uber.org/zap"
  13. )
  14. var testDataPath, _ = filepath.Abs("./testdata")
  15. func TestStartAndStopTheMainThreadWithOneInactiveThread(t *testing.T) {
  16. logger = zap.NewNop() // the logger needs to not be nil
  17. _, err := initPHPThreads(1, 1, nil) // boot 1 thread
  18. assert.NoError(t, err)
  19. assert.Len(t, phpThreads, 1)
  20. assert.Equal(t, 0, phpThreads[0].threadIndex)
  21. assert.True(t, phpThreads[0].state.is(stateInactive))
  22. drainPHPThreads()
  23. assert.Nil(t, phpThreads)
  24. }
  25. func TestTransitionRegularThreadToWorkerThread(t *testing.T) {
  26. logger = zap.NewNop()
  27. _, err := initPHPThreads(1, 1, nil)
  28. assert.NoError(t, err)
  29. // transition to regular thread
  30. convertToRegularThread(phpThreads[0])
  31. assert.IsType(t, &regularThread{}, phpThreads[0].handler)
  32. // transition to worker thread
  33. worker := getDummyWorker("transition-worker-1.php")
  34. convertToWorkerThread(phpThreads[0], worker)
  35. assert.IsType(t, &workerThread{}, phpThreads[0].handler)
  36. assert.Len(t, worker.threads, 1)
  37. // transition back to inactive thread
  38. convertToInactiveThread(phpThreads[0])
  39. assert.IsType(t, &inactiveThread{}, phpThreads[0].handler)
  40. assert.Len(t, worker.threads, 0)
  41. drainPHPThreads()
  42. assert.Nil(t, phpThreads)
  43. }
  44. func TestTransitionAThreadBetween2DifferentWorkers(t *testing.T) {
  45. logger = zap.NewNop()
  46. _, err := initPHPThreads(1, 1, nil)
  47. assert.NoError(t, err)
  48. firstWorker := getDummyWorker("transition-worker-1.php")
  49. secondWorker := getDummyWorker("transition-worker-2.php")
  50. // convert to first worker thread
  51. convertToWorkerThread(phpThreads[0], firstWorker)
  52. firstHandler := phpThreads[0].handler.(*workerThread)
  53. assert.Same(t, firstWorker, firstHandler.worker)
  54. assert.Len(t, firstWorker.threads, 1)
  55. assert.Len(t, secondWorker.threads, 0)
  56. // convert to second worker thread
  57. convertToWorkerThread(phpThreads[0], secondWorker)
  58. secondHandler := phpThreads[0].handler.(*workerThread)
  59. assert.Same(t, secondWorker, secondHandler.worker)
  60. assert.Len(t, firstWorker.threads, 0)
  61. assert.Len(t, secondWorker.threads, 1)
  62. drainPHPThreads()
  63. assert.Nil(t, phpThreads)
  64. }
  65. // try all possible handler transitions
  66. // takes around 200ms and is supposed to force race conditions
  67. func TestTransitionThreadsWhileDoingRequests(t *testing.T) {
  68. numThreads := 10
  69. numRequestsPerThread := 100
  70. isDone := atomic.Bool{}
  71. wg := sync.WaitGroup{}
  72. worker1Path := testDataPath + "/transition-worker-1.php"
  73. worker2Path := testDataPath + "/transition-worker-2.php"
  74. assert.NoError(t, Init(
  75. WithNumThreads(numThreads),
  76. WithWorkers(worker1Path, 1, map[string]string{}, []string{}),
  77. WithWorkers(worker2Path, 1, map[string]string{}, []string{}),
  78. WithLogger(zap.NewNop()),
  79. ))
  80. // try all possible permutations of transition, transition every ms
  81. transitions := allPossibleTransitions(worker1Path, worker2Path)
  82. for i := 0; i < numThreads; i++ {
  83. go func(thread *phpThread, start int) {
  84. for {
  85. for j := start; j < len(transitions); j++ {
  86. if isDone.Load() {
  87. return
  88. }
  89. transitions[j](thread)
  90. time.Sleep(time.Millisecond)
  91. }
  92. start = 0
  93. }
  94. }(phpThreads[i], i)
  95. }
  96. // randomly do requests to the 3 endpoints
  97. wg.Add(numThreads)
  98. for i := 0; i < numThreads; i++ {
  99. go func(i int) {
  100. for j := 0; j < numRequestsPerThread; j++ {
  101. switch rand.IntN(3) {
  102. case 0:
  103. assertRequestBody(t, "http://localhost/transition-worker-1.php", "Hello from worker 1")
  104. case 1:
  105. assertRequestBody(t, "http://localhost/transition-worker-2.php", "Hello from worker 2")
  106. case 2:
  107. assertRequestBody(t, "http://localhost/transition-regular.php", "Hello from regular thread")
  108. }
  109. }
  110. wg.Done()
  111. }(i)
  112. }
  113. // we are finished as soon as all 1000 requests are done
  114. wg.Wait()
  115. isDone.Store(true)
  116. Shutdown()
  117. }
  118. func getDummyWorker(fileName string) *worker {
  119. if workers == nil {
  120. workers = make(map[string]*worker)
  121. }
  122. worker, _ := newWorker(workerOpt{
  123. fileName: testDataPath + "/" + fileName,
  124. num: 1,
  125. })
  126. return worker
  127. }
  128. func assertRequestBody(t *testing.T, url string, expected string) {
  129. r := httptest.NewRequest("GET", url, nil)
  130. w := httptest.NewRecorder()
  131. req, err := NewRequestWithContext(r, WithRequestDocumentRoot(testDataPath, false))
  132. assert.NoError(t, err)
  133. err = ServeHTTP(w, req)
  134. assert.NoError(t, err)
  135. resp := w.Result()
  136. body, _ := io.ReadAll(resp.Body)
  137. assert.Equal(t, expected, string(body))
  138. }
  139. // create a mix of possible transitions of workers and regular threads
  140. func allPossibleTransitions(worker1Path string, worker2Path string) []func(*phpThread) {
  141. return []func(*phpThread){
  142. convertToRegularThread,
  143. func(thread *phpThread) { thread.shutdown() },
  144. func(thread *phpThread) { thread.boot() },
  145. func(thread *phpThread) { convertToWorkerThread(thread, workers[worker1Path]) },
  146. convertToInactiveThread,
  147. func(thread *phpThread) { convertToWorkerThread(thread, workers[worker2Path]) },
  148. convertToInactiveThread,
  149. }
  150. }