Golang program gracefully closes and restarts

What is a graceful

When the online code is updated, we should first close the service, and then start the service. If the traffic volume is relatively large, when closing the service, the current server is likely to have many connections. If the service is directly closed at this time, all these connections will be broken, affecting the user experience, which is definitely not elegant

So we had to figure out a way to smoothly shut down or restart the program

Elegance.

Train of thought

  1. The server starts with an extra coroutine to listen for a shutdown signal
  2. When the coroutine receives a close signal, it rejects new connections and disconnects after processing all current connections
  3. Start a new server process to take over the new connection
  4. Close the current process

implementation

Take the Siluser/Bingo framework as an example

A series of articles on this framework:

  • Use Go to write a simple MVC Web framework
  • Use Go to package a convenient ORM
  • Modify HttprOuter to support middleware
  • Simple Go development scaffolding modeled after Laravel-Artisan

I used the tim1020/ GoDaemon package to implement the smooth restart feature (for most projects, direct use will meet most requirements without modification)

Expected effect:

In the console input bingo run daemon [start | restart | stop] can set the server to start | restart | stop

  1. Let’s start a server (bingo run dev)

See my previous blog on the implementation of bingo commands: Simple Go Development Scaffolding modeled after Laravel-Artisan

Go run run start. Go run start. Go run start

So bingo run dev equals go run start.go dev

// Process http.Server so that graceful stop/restart is supported
func Graceful(s http.Server) error {
	// Set an environment variable
	os.Setenv("__GRACEFUL"."true")
	// Create a custom server
	srv = &server{
		cm:     newConnectionManager(),
		Server: s,
	}

	// Set the server status
	srv.ConnState = func(conn net.Conn, state http.ConnState) {
		switch state {
		case http.StateNew:
			srv.cm.add(1)
		case http.StateActive:
			srv.cm.rmIdleConns(conn.LocalAddr().String())
		case http.StateIdle:
			srv.cm.addIdleConns(conn.LocalAddr().String(), conn)
		case http.StateHijacked, http.StateClosed:
			srv.cm.done()
		}
	}
	l, err := srv.getListener()
	if err == nil {
		err = srv.Server.Serve(l)
	} else {
		fmt.Println(err)
	}
	return err
}
Copy the code

This allows you to start a server and listen for changes in connection status

  1. Start the server as a daemon

When bingo run daemon or bingo run daemon start is used, DaemonInit() is triggered as follows:

func DaemonInit(a) {
	// Get the path to the pid file
	dir, _ := os.Getwd()
	pidFile = dir + "/" + Env.Get("PID_FILE")
	if os.Getenv("__Daemon") != "true" { //master
		cmd := "start" // The default value is start
		if l := len(os.Args); l > 2 {
			cmd = os.Args[l- 1]}switch cmd {
		case "start":
			if isRunning() {
				fmt.Printf("\n %c[0;48;34m%s%c[0m".0x1B."["+strconv.Itoa(pidVal)+"] Bingo is running".0x1B)}else { / / fork the daemon process
				iferr := forkDaemon(); err ! =nil {
					fmt.Println(err)
				}
			}
		case "restart": / / resume:
			if! isRunning() { fmt.Printf("\n %c[0;48;31m%s%c[0m".0x1B."[Warning]bingo not running".0x1B)
				restart(pidVal)
			} else {
				fmt.Printf("\n %c[0;48;34m%s%c[0m".0x1B."["+strconv.Itoa(pidVal)+"] Bingo restart now".0x1B)
				restart(pidVal)
			}
		case "stop": / / stop
			if! isRunning() { fmt.Printf("\n %c[0;48;31m%s%c[0m".0x1B."[Warning]bingo not running".0x1B)}else {
				syscall.Kill(pidVal, syscall.SIGTERM) //kill
			}
		case "-h":
			fmt.Println("Usage: " + appName + " start|restart|stop")
		default:   // No other parameters
			return // return to the caller
		}
		// The main process exits
		os.Exit(0)}go handleSignals()
}
Copy the code

The pidFile file is used to store the pid of the process running the application. It is to determine whether the same program is started in the process of running multiple times

After getting the corresponding operation (start | restart | stop), said one by one

case start:

First, use isRunning() method to determine whether the current program isRunning. How to determine? Fetch the process number from the pidFile mentioned above

It then checks whether the process is running on the current system, and returns true if it is, or false if it is not

If it is not running, the forkDaemon() function is called, which is the heart of the operation

func forkDaemon(a) error {
	args := os.Args
	os.Setenv("__Daemon"."true")
	procAttr := &syscall.ProcAttr{
		Env:   os.Environ(),
		Files: []uintptr{os.Stdin.Fd(), os.Stdout.Fd(), os.Stderr.Fd()},
	}
	pid, err := syscall.ForkExec(args[0], []string{args[0]."dev"}, procAttr)
	iferr ! =nil {
		panic(err)
	}
	savePid(pid)
	fmt.Printf("\n %c[0;48;32m%s%c[0m".0x1B."["+strconv.Itoa(pid)+"] Bingo running...".0x1B)
	fmt.Println()
	return nil
}
Copy the code

The Syscall package does not support Windows, which means that if you want to develop on Windows, you have to use virtual machines or Docker

The main function here is to fork a process using syscall.forkexec ()

The command executed to run this process is the argument here (since our original command was go run start.go dev, args[0] is actually the compiled binary of start.go)

The process number from the fork is stored in the pidFile

So the end result is the bingo run dev effect we talked about in the first step

case restart:

PidFile is used to determine whether the program is running. If it is running, it will continue to execute

The body of the function is also relatively simple, with only two lines

syscall.Kill(pid, syscall.SIGHUP) // Kill -hup, daemon only
forkDaemon()
Copy the code

Line one kills the process and line two starts a new process

case stop:

There’s only one line of code here to kill the process

Extra ideas

During development, every time there is a slight change (such as a slight controller change), the bingo run daemon restart command needs to be executed again to make the new change take effect, which is very troublesome

So I also developed the bingo run watch command to monitor changes and automatically restart the server

I used github.com/fsnotify/fs… Package to implement listening


func startWatchServer(port string, handler http.Handler) {
	// Listen for directory changes and restart the service if any
	// The daemon starts the service. The main process blocks and scans the current directory continuously. If there are any updates, it signals the daemon and the daemon restarts the service
	Start a coroutine running service
	// Listen for directory changes and run bingo run daemon restart
	f, err := fsnotify.NewWatcher()
	iferr ! =nil {
		panic(err)
	}
	defer f.Close()
	dir, _ := os.Getwd()
	wdDir = dir
	fileWatcher = f
	f.Add(dir)

	done := make(chan bool)

	go func(a) {
		procAttr := &syscall.ProcAttr{
			Env:   os.Environ(),
			Files: []uintptr{os.Stdin.Fd(), os.Stdout.Fd(), os.Stderr.Fd()},
		}
		_, err := syscall.ForkExec(os.Args[0], []string{os.Args[0]."daemon"."start"}, procAttr)
		iferr ! =nil {
			fmt.Println(err)
		}
	}()

	go func(a) {
		for {
			select {
			case ev := <-f.Events:
				if ev.Op&fsnotify.Create == fsnotify.Create {
					fmt.Printf("\n %c[0;48;33m%s%c[0m".0x1B."["+time.Now().Format("The 2006-01-02 15:04:05") +"]created file:"+ev.Name, 0x1B)}if ev.Op&fsnotify.Remove == fsnotify.Remove {
					fmt.Printf("\n %c[0;48;31m%s%c[0m".0x1B."["+time.Now().Format("The 2006-01-02 15:04:05") +"]deleted file:"+ev.Name, 0x1B)}if ev.Op&fsnotify.Rename == fsnotify.Rename {
					fmt.Printf("\n %c[0;48;34m%s%c[0m".0x1B."["+time.Now().Format("The 2006-01-02 15:04:05") +"]renamed file:"+ev.Name, 0x1B)}else {
					fmt.Printf("\n %c[0;48;32m%s%c[0m".0x1B."["+time.Now().Format("The 2006-01-02 15:04:05") +"]modified file:"+ev.Name, 0x1B)}// There are changes, put into the restart array
				restartSlice = append(restartSlice, 1)
			case err := <-f.Errors:
				fmt.Println("error:", err)
			}
		}
	}()

	// Prepare to restart the daemon
	go restartDaemonServer()

	<-done
}
Copy the code

First, create a watcher as per fsnotify’s document, then add a listening directory (this is only listening for files in the directory, not listening for subdirectories)

Then open two coroutines:

  1. Listen for file changes and write the number of changes to a slice, which is a blocking for loop

  2. Check slice every 1s to record file changes, restart the server if any, reset the listening directory, and then empty slice, otherwise skip

    Recursively traverses subdirectories to achieve the effect of listening on the entire project directory:

func listeningWatcherDir(dir string) {
	filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
		dir, _ := os.Getwd()
		pidFile = dir + "/" + Env.Get("PID_FILE")
		fileWatcher.Add(path)
		
		// Do not listen to pidFile, or pidFile will be updated every time restart, will constantly trigger the restart function
		fileWatcher.Remove(pidFile)
		return nil})}Copy the code

The purpose of this slice is to avoid multiple server restarts when multiple files are saved and updated

Here is the code to restart the server:

	go func(a) {
				// Run the restart command
				cmd := exec.Command("bingo"."run"."daemon"."restart")
				stdout, err := cmd.StdoutPipe()
				iferr ! =nil {
					fmt.Println(err)
				}
				defer stdout.Close()

				iferr := cmd.Start(); err ! =nil {
					panic(err)
				}
				reader := bufio.NewReader(stdout)
				// Loop through a line in the output stream in real time
				for {
					line, err2 := reader.ReadString('\n')
					iferr2 ! =nil || io.EOF == err2 {
						break
					}
					fmt.Print(line)
				}

				iferr := cmd.Wait(); err ! =nil {
					fmt.Println(err)
				}
				opBytes, _ := ioutil.ReadAll(stdout)
				fmt.Print(string(opBytes))

			}()
Copy the code

Use the exec.Command() method to get a CMD

Calling cmd.stdOutput () yields an output pipe through which the data printed by the command flows

Then use reader := bufio.newReader (stdout) to read the data from the pipe

Using a blocking for loop, read data from the pipe continuously, row by row, row by row

If these lines are not written, the data printed by the fmt.println () method in the new process will not be displayed on the console.

Silsuer/Bingo, welcome star, welcome PR, welcome your comments