prefork.go 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192
  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. go watchMaster()
  54. // prepare the server for the start
  55. app.startupProcess()
  56. if cfg.ListenerAddrFunc != nil {
  57. cfg.ListenerAddrFunc(ln.Addr())
  58. }
  59. // listen for incoming connections
  60. return app.server.Serve(ln)
  61. }
  62. // 👮 master process 👮
  63. type child struct {
  64. err error
  65. pid int
  66. }
  67. // create variables
  68. maxProcs := runtime.GOMAXPROCS(0)
  69. children := make(map[int]*exec.Cmd)
  70. channel := make(chan child, maxProcs)
  71. // kill child procs when master exits
  72. defer func() {
  73. for _, proc := range children {
  74. if err = proc.Process.Kill(); err != nil {
  75. if !errors.Is(err, os.ErrProcessDone) {
  76. log.Errorf("prefork: failed to kill child: %v", err)
  77. }
  78. }
  79. }
  80. }()
  81. // collect child pids
  82. var childPIDs []int
  83. // launch child procs
  84. for range maxProcs {
  85. cmd := exec.Command(os.Args[0], os.Args[1:]...) //nolint:gosec // It's fine to launch the same process again
  86. if testPreforkMaster {
  87. // When test prefork master,
  88. // just start the child process with a dummy cmd,
  89. // which will exit soon
  90. cmd = dummyCmd()
  91. }
  92. cmd.Stdout = os.Stdout
  93. cmd.Stderr = os.Stderr
  94. // add fiber prefork child flag into child proc env
  95. cmd.Env = append(os.Environ(),
  96. fmt.Sprintf("%s=%s", envPreforkChildKey, envPreforkChildVal),
  97. )
  98. if err = cmd.Start(); err != nil {
  99. return fmt.Errorf("failed to start a child prefork process, error: %w", err)
  100. }
  101. // store child process
  102. pid := cmd.Process.Pid
  103. children[pid] = cmd
  104. childPIDs = append(childPIDs, pid)
  105. // execute fork hook
  106. if app.hooks != nil {
  107. if testOnPrefork {
  108. app.hooks.executeOnForkHooks(dummyPid)
  109. } else {
  110. app.hooks.executeOnForkHooks(pid)
  111. }
  112. }
  113. // notify master if child crashes
  114. go func() {
  115. channel <- child{pid: pid, err: cmd.Wait()}
  116. }()
  117. }
  118. // Run onListen hooks
  119. // Hooks have to be run here as different as non-prefork mode due to they should run as child or master
  120. listenData := app.prepareListenData(addr, tlsConfig != nil, cfg, childPIDs)
  121. app.runOnListenHooks(listenData)
  122. app.startupMessage(listenData, cfg)
  123. if cfg.EnablePrintRoutes {
  124. app.printRoutesMessage()
  125. }
  126. // return error if child crashes
  127. return (<-channel).err
  128. }
  129. // watchMaster watches child procs
  130. func watchMaster() {
  131. if runtime.GOOS == windowsOS {
  132. // finds parent process,
  133. // and waits for it to exit
  134. p, err := os.FindProcess(os.Getppid())
  135. if err == nil {
  136. _, _ = p.Wait() //nolint:errcheck // It is fine to ignore the error here
  137. }
  138. os.Exit(1) //nolint:revive // Calling os.Exit is fine here in the prefork
  139. }
  140. // if it is equal to 1 (init process ID),
  141. // it indicates that the master process has exited
  142. const watchInterval = 500 * time.Millisecond
  143. for range time.NewTicker(watchInterval).C {
  144. if os.Getppid() == 1 {
  145. os.Exit(1) //nolint:revive // Calling os.Exit is fine here in the prefork
  146. }
  147. }
  148. }
  149. var (
  150. dummyPid = 1
  151. dummyChildCmd atomic.Value
  152. )
  153. // dummyCmd is for internal prefork testing
  154. func dummyCmd() *exec.Cmd {
  155. command := "go"
  156. if storeCommand := dummyChildCmd.Load(); storeCommand != nil && storeCommand != "" {
  157. command = storeCommand.(string) //nolint:forcetypeassert,errcheck // We always store a string in here
  158. }
  159. if runtime.GOOS == windowsOS {
  160. return exec.Command("cmd", "/C", command, "version")
  161. }
  162. return exec.Command(command, "version")
  163. }