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