prefork.go 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179
  1. package fiber
  2. import (
  3. "crypto/tls"
  4. "errors"
  5. "fmt"
  6. "os"
  7. "os/exec"
  8. "runtime"
  9. "strconv"
  10. "strings"
  11. "sync/atomic"
  12. "time"
  13. "github.com/valyala/fasthttp/reuseport"
  14. "github.com/gofiber/fiber/v2/log"
  15. )
  16. const (
  17. envPreforkChildKey = "FIBER_PREFORK_CHILD"
  18. envPreforkChildVal = "1"
  19. )
  20. var (
  21. testPreforkMaster = false
  22. testOnPrefork = false
  23. )
  24. // IsChild determines if the current process is a child of Prefork
  25. func IsChild() bool {
  26. return os.Getenv(envPreforkChildKey) == envPreforkChildVal
  27. }
  28. // prefork manages child processes to make use of the OS REUSEPORT or REUSEADDR feature
  29. func (app *App) prefork(network, addr string, tlsConfig *tls.Config) error {
  30. // 👶 child process 👶
  31. if IsChild() {
  32. // use 1 cpu core per child process
  33. runtime.GOMAXPROCS(1)
  34. // Linux will use SO_REUSEPORT and Windows falls back to SO_REUSEADDR
  35. // Only tcp4 or tcp6 is supported when preforking, both are not supported
  36. ln, err := reuseport.Listen(network, addr)
  37. if err != nil {
  38. if !app.config.DisableStartupMessage {
  39. const sleepDuration = 100 * time.Millisecond
  40. time.Sleep(sleepDuration) // avoid colliding with startup message
  41. }
  42. return fmt.Errorf("prefork: %w", err)
  43. }
  44. // wrap a tls config around the listener if provided
  45. if tlsConfig != nil {
  46. ln = tls.NewListener(ln, tlsConfig)
  47. }
  48. // kill current child proc when master exits
  49. go watchMaster()
  50. // prepare the server for the start
  51. app.startupProcess()
  52. // listen for incoming connections
  53. return app.server.Serve(ln)
  54. }
  55. // 👮 master process 👮
  56. type child struct {
  57. pid int
  58. err error
  59. }
  60. // create variables
  61. max := runtime.GOMAXPROCS(0)
  62. childs := make(map[int]*exec.Cmd)
  63. channel := make(chan child, max)
  64. // kill child procs when master exits
  65. defer func() {
  66. for _, proc := range childs {
  67. if err := proc.Process.Kill(); err != nil {
  68. if !errors.Is(err, os.ErrProcessDone) {
  69. log.Errorf("prefork: failed to kill child: %v", err)
  70. }
  71. }
  72. }
  73. }()
  74. // collect child pids
  75. var pids []string
  76. // launch child procs
  77. for i := 0; i < max; i++ {
  78. cmd := exec.Command(os.Args[0], os.Args[1:]...) //nolint:gosec // It's fine to launch the same process again
  79. if testPreforkMaster {
  80. // When test prefork master,
  81. // just start the child process with a dummy cmd,
  82. // which will exit soon
  83. cmd = dummyCmd()
  84. }
  85. cmd.Stdout = os.Stdout
  86. cmd.Stderr = os.Stderr
  87. // add fiber prefork child flag into child proc env
  88. cmd.Env = append(os.Environ(),
  89. fmt.Sprintf("%s=%s", envPreforkChildKey, envPreforkChildVal),
  90. )
  91. if err := cmd.Start(); err != nil {
  92. return fmt.Errorf("failed to start a child prefork process, error: %w", err)
  93. }
  94. // store child process
  95. pid := cmd.Process.Pid
  96. childs[pid] = cmd
  97. pids = append(pids, strconv.Itoa(pid))
  98. // execute fork hook
  99. if app.hooks != nil {
  100. if testOnPrefork {
  101. app.hooks.executeOnForkHooks(dummyPid)
  102. } else {
  103. app.hooks.executeOnForkHooks(pid)
  104. }
  105. }
  106. // notify master if child crashes
  107. go func() {
  108. channel <- child{pid, cmd.Wait()}
  109. }()
  110. }
  111. // Run onListen hooks
  112. // Hooks have to be run here as different as non-prefork mode due to they should run as child or master
  113. app.runOnListenHooks(app.prepareListenData(addr, tlsConfig != nil))
  114. // Print startup message
  115. if !app.config.DisableStartupMessage {
  116. app.startupMessage(addr, tlsConfig != nil, ","+strings.Join(pids, ","))
  117. }
  118. // return error if child crashes
  119. return (<-channel).err
  120. }
  121. // watchMaster watches child procs
  122. func watchMaster() {
  123. if runtime.GOOS == "windows" {
  124. // finds parent process,
  125. // and waits for it to exit
  126. p, err := os.FindProcess(os.Getppid())
  127. if err == nil {
  128. _, _ = p.Wait() //nolint:errcheck // It is fine to ignore the error here
  129. }
  130. os.Exit(1) //nolint:revive // Calling os.Exit is fine here in the prefork
  131. }
  132. // if it is equal to 1 (init process ID),
  133. // it indicates that the master process has exited
  134. const watchInterval = 500 * time.Millisecond
  135. for range time.NewTicker(watchInterval).C {
  136. if os.Getppid() == 1 {
  137. os.Exit(1) //nolint:revive // Calling os.Exit is fine here in the prefork
  138. }
  139. }
  140. }
  141. var (
  142. dummyPid = 1
  143. dummyChildCmd atomic.Value
  144. )
  145. // dummyCmd is for internal prefork testing
  146. func dummyCmd() *exec.Cmd {
  147. command := "go"
  148. if storeCommand := dummyChildCmd.Load(); storeCommand != nil && storeCommand != "" {
  149. command = storeCommand.(string) //nolint:forcetypeassert,errcheck // We always store a string in here
  150. }
  151. if runtime.GOOS == "windows" {
  152. return exec.Command("cmd", "/C", command, "version")
  153. }
  154. return exec.Command(command, "version")
  155. }