Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

I implemented a hot restart on fasthttp, but it cannot be implemented on atreugo because I can't access net.listen of atreugo.server #39

Closed
imxxiv opened this issue Sep 9, 2019 · 9 comments

Comments

@imxxiv
Copy link

imxxiv commented Sep 9, 2019


Imitate the code for this page, I should implement a hot restart on fasthttp

package main

import (
	"encoding/json"
	"flag"
	"fmt"
	"net"
	"os"
	"os/signal"
	"path/filepath"
	"syscall"
	"github.com/fasthttp/router"
	"github.com/valyala/fasthttp"
	"github.com/valyala/fasthttp/reuseport"
)

type listener struct {
	Addr     string `json:"addr"`
	FD       int    `json:"fd"`
	Filename string `json:"filename"`
}

func importListener(addr string) (net.Listener, error) {
	// Extract the encoded listener metadata from the environment.
	listenerEnv := os.Getenv("LISTENER")
	if listenerEnv == "" {
		return nil, fmt.Errorf("unable to find LISTENER environment variable")
	}

	// Unmarshal the listener metadata.
	var l listener
	err := json.Unmarshal([]byte(listenerEnv), &l)
	if err != nil {
		return nil, err
	}
	if l.Addr != addr {
		return nil, fmt.Errorf("unable to find listener for %v", addr)
	}

	// The file has already been passed to this process, extract the file
	// descriptor and name from the metadata to rebuild/find the *os.File for
	// the listener.
	listenerFile := os.NewFile(uintptr(l.FD), l.Filename)
	if listenerFile == nil {
		return nil, fmt.Errorf("unable to create listener file: %v", err)
	}
	defer listenerFile.Close()

	// Create a net.Listener from the *os.File.
	ln, err := net.FileListener(listenerFile)
	if err != nil {
		return nil, err
	}

	return ln, nil
}

func createListener(addr string) (net.Listener, error) {
	//ln, err := net.Listen("tcp", addr)
	ln, err := reuseport.Listen("tcp6", addr)
	if err != nil {
		return nil, err
	}

	return ln, nil
}

func createOrImportListener(addr string) (net.Listener, error) {
	// Try and import a listener for addr. If it's found, use it.
	ln, err := importListener(addr)
	if err == nil {
		fmt.Printf("Imported listener file descriptor for %v.\n", addr)
		return ln, nil
	}

	// No listener was imported, that means this process has to create one.
	ln, err = createListener(addr)
	if err != nil {
		return nil, err
	}

	fmt.Printf("Created listener file descriptor for %v.\n", addr)
	return ln, nil
}

func getListenerFile(ln net.Listener) (*os.File, error) {
	switch t := ln.(type) {
	case *net.TCPListener:
		return t.File()
	case *net.UnixListener:
		return t.File()
	}
	return nil, fmt.Errorf("unsupported listener: %T", ln)
}

func forkChild(addr string, ln net.Listener) (*os.Process, error) {
	// Get the file descriptor for the listener and marshal the metadata to pass
	// to the child in the environment.
	lnFile, err := getListenerFile(ln)
	if err != nil {
		return nil, err
	}
	defer lnFile.Close()
	l := listener{
		Addr:     addr,
		FD:       3,
		Filename: lnFile.Name(),
	}
	listenerEnv, err := json.Marshal(l)
	if err != nil {
		return nil, err
	}

	// Pass stdin, stdout, and stderr along with the listener to the child.
	files := []*os.File{
		os.Stdin,
		os.Stdout,
		os.Stderr,
		lnFile,
	}

	// Get current environment and add in the listener to it.
	environment := append(os.Environ(), "LISTENER="+string(listenerEnv))

	// Get current process name and directory.
	execName, err := os.Executable()
	if err != nil {
		return nil, err
	}
	execDir := filepath.Dir(execName)

	// Spawn child process.
	p, err := os.StartProcess(execName, []string{execName}, &os.ProcAttr{
		Dir:   execDir,
		Env:   environment,
		Files: files,
		Sys:   &syscall.SysProcAttr{},
	})
	if err != nil {
		return nil, err
	}

	return p, nil
}

func waitForSignals(addr string, ln net.Listener, server *fasthttp.Server) error {
	signalCh := make(chan os.Signal, 1024)
	signal.Notify(signalCh, syscall.SIGHUP, syscall.SIGUSR2, syscall.SIGINT, syscall.SIGQUIT)
	for {
		select {
		case s := <-signalCh:
			fmt.Printf("%v signal received.\n", s)
			switch s {
			case syscall.SIGHUP:
				// Fork a child process.
				p, err := forkChild(addr, ln)
				if err != nil {
					fmt.Printf("Unable to fork child: %v.\n", err)
					continue
				}
				fmt.Printf("Forked child %v.\n", p.Pid)

				// Return any errors during shutdown.
				return server.Shutdown()
			case syscall.SIGUSR2:
				// Fork a child process.
				p, err := forkChild(addr, ln)
				if err != nil {
					fmt.Printf("Unable to fork child: %v.\n", err)
					continue
				}

				// Print the PID of the forked process and keep waiting for more signals.
				fmt.Printf("Forked child %v.\n", p.Pid)
			case syscall.SIGINT, syscall.SIGQUIT:
				// Return any errors during shutdown.
				return server.Shutdown()
			}
		}
	}
}

func handler(ctx *fasthttp.RequestCtx) {
	fmt.Fprintf(ctx, "Hello from %v!\n", os.Getpid())
}

func test(ctx *fasthttp.RequestCtx) {
	fmt.Fprintf(ctx, "Test from %v!\n", os.Getpid())
}

func web(ctx *fasthttp.RequestCtx) {
	fmt.Fprintf(ctx, "WEB from %v!\n", os.Getpid())
}

func startServer(ln net.Listener) *fasthttp.Server {
	r := router.New()
	r.GET("/hello", handler)
	//r.GET("/test", test)
	//r.GET("/web", web)

	httpServer := &fasthttp.Server{
		Name: "fast",
		Handler: r.Handler,
	}
	go httpServer.Serve(ln)

	return httpServer
}

var addr string
var ln net.Listener
var server *fasthttp.Server

func main() {
	// Parse command line flags for the address to listen on.
	flag.StringVar(&addr, "addr", ":8080", "Address to listen on.")

	// Create (or import) a net.Listener and start a goroutine that runs
	// a HTTP server on that net.Listener.
	var err error
	ln, err = createOrImportListener(addr)
	if err != nil {
		fmt.Printf("Unable to create or import a listener: %v.\n", err)
		os.Exit(1)
	}
	server = startServer(ln)

	// Wait for signals to either fork or quit.
	err = waitForSignals(addr, ln, server)
	if err != nil {
		fmt.Printf("Exiting: %v\n", err)
		return
	}
	fmt.Printf("Exiting.\n")
}

when I :

  1. add r.GET("/test", test), and recompile the program
  2. Replace the old program file with the newly compiled program file
  3. kill -1
    the /test can be accessed

When I plan to use this code to atreugo, but find the net.listen of atreugo.server can't be use.

@imxxiv
Copy link
Author

imxxiv commented Sep 9, 2019

Can I use server.ServerPtr() or server.RouterPtr() ?
Because in a project I might want to set router.RedirectTrailingSlash to false.
Thanks. :-)

@savsgio
Copy link
Owner

savsgio commented Sep 10, 2019

What do you mean with I can't access net.listen of atreugo.server??

@imxxiv
Copy link
Author

imxxiv commented Sep 10, 2019

func createListener(addr string) (net.Listener, error) {
	//ln, err := net.Listen("tcp", addr)
	ln, err := reuseport.Listen("tcp6", addr)
	if err != nil {
		return nil, err
	}

	return ln, nil
}
func createOrImportListener(addr string) (net.Listener, error) {
	// Try and import a listener for addr. If it's found, use it.
	ln, err := importListener(addr)
	if err == nil {
		fmt.Printf("Imported listener file descriptor for %v.\n", addr)
		return ln, nil
	}

	// No listener was imported, that means this process has to create one.
	ln, err = createListener(addr)
	if err != nil {
		return nil, err
	}

	fmt.Printf("Created listener file descriptor for %v.\n", addr)
	return ln, nil
}

In order to achieve hot restart, I have to get ln, but getListener() in atreugo is private function

@savsgio
Copy link
Owner

savsgio commented Sep 10, 2019

You don't need access to getListener(), because you could create your own listener and use it with myatreugoserver.Serve(ln).
getListener it's just for ListenAndServe()

If you need a listener with reuseport, you could set to true the Reuseport option in configuration, and run server with ListenAndServe(), avoiding to create a FileListener shared with child processes, because you could run any servers that using the same port (with different listeners) at the same time.

The both options are valid.

@savsgio
Copy link
Owner

savsgio commented Sep 10, 2019

So, if you want to create your own listener, change this function with something like that:

func startServer(ln net.Listener) *atreugo.Atreugo {
	config := &atreugo.Config{
		Name: "fast",
	}
	server := atreugo.New(config)

	server.Path("GET", "/hello", handler)
	//r.GET("/test", test)
	//r.GET("/web", web)

	go server.Serve(ln)

	return server
}

@imxxiv
Copy link
Author

imxxiv commented Sep 10, 2019

Reuseport can listen same port at the same time !

Then in principle, it is very simple to implement hot restart.

  1. set reuseport true and ServeGracefully true, compile and run the program
  2. after add handle, compile and run the program angin, and kill the old pid of the old program

The old server will Gracefully exit, and new server will go on to work.
Is this correct in my understanding?

@savsgio
Copy link
Owner

savsgio commented Sep 10, 2019

Yep 😄 hehe

@savsgio
Copy link
Owner

savsgio commented Sep 13, 2019

Can I use server.ServerPtr() or server.RouterPtr() ?
Because in a project I might want to set router.RedirectTrailingSlash to false.
Thanks. :-)

Finally, I added this https://godoc.org/github.com/savsgio/atreugo#Atreugo.RedirectTrailingSlash.
Now you could change internal options of router.

I will release a new version in a few days.

PD: I don't add ServerPtr and RouterPtr, to protect it of bad uses that could break atreugo

@imxxiv
Copy link
Author

imxxiv commented Sep 14, 2019

I know, Thanks for add RedirectTrailingSlash.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants