admin_test.go 5.4 KB

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