admin_test.go 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
  1. package caddy_test
  2. import (
  3. "encoding/json"
  4. "io"
  5. "net/http"
  6. "sync"
  7. "testing"
  8. "github.com/caddyserver/caddy/v2/caddytest"
  9. "github.com/dunglas/frankenphp"
  10. "github.com/stretchr/testify/assert"
  11. )
  12. func TestRestartWorkerViaAdminApi(t *testing.T) {
  13. tester := caddytest.NewTester(t)
  14. tester.InitServer(`
  15. {
  16. skip_install_trust
  17. admin localhost:2999
  18. http_port `+testPort+`
  19. frankenphp {
  20. worker ../testdata/worker-with-counter.php 1
  21. }
  22. }
  23. localhost:`+testPort+` {
  24. route {
  25. root ../testdata
  26. rewrite worker-with-counter.php
  27. php
  28. }
  29. }
  30. `, "caddyfile")
  31. tester.AssertGetResponse("http://localhost:"+testPort+"/", http.StatusOK, "requests:1")
  32. tester.AssertGetResponse("http://localhost:"+testPort+"/", http.StatusOK, "requests:2")
  33. assertAdminResponse(t, tester, "POST", "workers/restart", http.StatusOK, "workers restarted successfully\n")
  34. tester.AssertGetResponse("http://localhost:"+testPort+"/", http.StatusOK, "requests:1")
  35. }
  36. func TestShowTheCorrectThreadDebugStatus(t *testing.T) {
  37. tester := caddytest.NewTester(t)
  38. tester.InitServer(`
  39. {
  40. skip_install_trust
  41. admin localhost:2999
  42. http_port `+testPort+`
  43. frankenphp {
  44. num_threads 3
  45. max_threads 6
  46. worker ../testdata/worker-with-counter.php 1
  47. worker ../testdata/index.php 1
  48. }
  49. }
  50. localhost:`+testPort+` {
  51. route {
  52. root ../testdata
  53. rewrite worker-with-counter.php
  54. php
  55. }
  56. }
  57. `, "caddyfile")
  58. debugState := getDebugState(t, tester)
  59. // assert that the correct threads are present in the thread info
  60. assert.Equal(t, debugState.ThreadDebugStates[0].State, "ready")
  61. assert.Contains(t, debugState.ThreadDebugStates[1].Name, "worker-with-counter.php")
  62. assert.Contains(t, debugState.ThreadDebugStates[2].Name, "index.php")
  63. assert.Equal(t, debugState.ReservedThreadCount, 3)
  64. assert.Len(t, debugState.ThreadDebugStates, 3)
  65. }
  66. func TestAutoScaleWorkerThreads(t *testing.T) {
  67. wg := sync.WaitGroup{}
  68. maxTries := 10
  69. requestsPerTry := 200
  70. tester := caddytest.NewTester(t)
  71. tester.InitServer(`
  72. {
  73. skip_install_trust
  74. admin localhost:2999
  75. http_port `+testPort+`
  76. frankenphp {
  77. max_threads 10
  78. num_threads 2
  79. worker ../testdata/sleep.php 1
  80. }
  81. }
  82. localhost:`+testPort+` {
  83. route {
  84. root ../testdata
  85. rewrite sleep.php
  86. php
  87. }
  88. }
  89. `, "caddyfile")
  90. // spam an endpoint that simulates IO
  91. endpoint := "http://localhost:" + testPort + "/?sleep=2&work=1000"
  92. amountOfThreads := len(getDebugState(t, tester).ThreadDebugStates)
  93. // try to spawn the additional threads by spamming the server
  94. for tries := 0; tries < maxTries; tries++ {
  95. wg.Add(requestsPerTry)
  96. for i := 0; i < requestsPerTry; i++ {
  97. go func() {
  98. tester.AssertGetResponse(endpoint, http.StatusOK, "slept for 2 ms and worked for 1000 iterations")
  99. wg.Done()
  100. }()
  101. }
  102. wg.Wait()
  103. amountOfThreads = len(getDebugState(t, tester).ThreadDebugStates)
  104. if amountOfThreads > 2 {
  105. break
  106. }
  107. }
  108. // assert that there are now more threads than before
  109. assert.NotEqual(t, amountOfThreads, 2)
  110. }
  111. // Note this test requires at least 2x40MB available memory for the process
  112. func TestAutoScaleRegularThreadsOnAutomaticThreadLimit(t *testing.T) {
  113. wg := sync.WaitGroup{}
  114. maxTries := 10
  115. requestsPerTry := 200
  116. tester := caddytest.NewTester(t)
  117. tester.InitServer(`
  118. {
  119. skip_install_trust
  120. admin localhost:2999
  121. http_port `+testPort+`
  122. frankenphp {
  123. max_threads auto
  124. num_threads 1
  125. php_ini memory_limit 40M # a reasonable limit for the test
  126. }
  127. }
  128. localhost:`+testPort+` {
  129. route {
  130. root ../testdata
  131. php
  132. }
  133. }
  134. `, "caddyfile")
  135. // spam an endpoint that simulates IO
  136. endpoint := "http://localhost:" + testPort + "/sleep.php?sleep=2&work=1000"
  137. amountOfThreads := len(getDebugState(t, tester).ThreadDebugStates)
  138. // try to spawn the additional threads by spamming the server
  139. for tries := 0; tries < maxTries; tries++ {
  140. wg.Add(requestsPerTry)
  141. for i := 0; i < requestsPerTry; i++ {
  142. go func() {
  143. tester.AssertGetResponse(endpoint, http.StatusOK, "slept for 2 ms and worked for 1000 iterations")
  144. wg.Done()
  145. }()
  146. }
  147. wg.Wait()
  148. amountOfThreads = len(getDebugState(t, tester).ThreadDebugStates)
  149. if amountOfThreads > 1 {
  150. break
  151. }
  152. }
  153. // assert that there are now more threads present
  154. assert.NotEqual(t, amountOfThreads, 1)
  155. }
  156. func assertAdminResponse(t *testing.T, tester *caddytest.Tester, method string, path string, expectedStatus int, expectedBody string) {
  157. adminUrl := "http://localhost:2999/frankenphp/"
  158. r, err := http.NewRequest(method, adminUrl+path, nil)
  159. assert.NoError(t, err)
  160. if expectedBody == "" {
  161. _ = tester.AssertResponseCode(r, expectedStatus)
  162. return
  163. }
  164. _, _ = tester.AssertResponse(r, expectedStatus, expectedBody)
  165. }
  166. func getAdminResponseBody(t *testing.T, tester *caddytest.Tester, method string, path string) string {
  167. adminUrl := "http://localhost:2999/frankenphp/"
  168. r, err := http.NewRequest(method, adminUrl+path, nil)
  169. assert.NoError(t, err)
  170. resp := tester.AssertResponseCode(r, http.StatusOK)
  171. defer resp.Body.Close()
  172. bytes, err := io.ReadAll(resp.Body)
  173. assert.NoError(t, err)
  174. return string(bytes)
  175. }
  176. func getDebugState(t *testing.T, tester *caddytest.Tester) frankenphp.FrankenPHPDebugState {
  177. threadStates := getAdminResponseBody(t, tester, "GET", "threads")
  178. var debugStates frankenphp.FrankenPHPDebugState
  179. err := json.Unmarshal([]byte(threadStates), &debugStates)
  180. assert.NoError(t, err)
  181. return debugStates
  182. }