prefork.go 5.1 KB

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