Изящно завершение работы сервера Gorilla

Я собираю сервер на ходу, используя мультиплексорную библиотеку горилл, найденную по https://github.com/gorilla/mux. Проблема в том, что я хочу, чтобы он корректно завершал работу, когда я использую Ctrl+C, или когда есть определенный вызов API, например, "/shutdown".

Я уже знаю, что в Go 1.8 изящное отключение уже реализовано. Но как совместить его с мультиплексором гориллы? Кроме того, как совместить его с сигналом SIGINT?

Может кто-нибудь показать мне, как это сделать?

2 ответа

Решение

Канал может быть использован для захвата запроса на отключение через вызов API (/shutdown) или сигнал прерывания (Ctrl+C).

  1. встраивать http.Server в пользовательскую структуру, поэтому мы можем вызвать http Server.Shutdown позже
  2. Добавить поле канала (shutdownReq) для передачи запроса на отключение от вызова API /shutdown
  3. Зарегистрируйте обработчики http, включая /shutdown в gorilla/muxмаршрутизатор, затем назначьте маршрутизатор http.Server.Handler
  4. регистр os.Interrupt/syscall.SIGINT, syscall.SIGTERM обработчик
  5. использование select захватить запрос на отключение через вызов API или interrupt сигнал
  6. Выполните чистое отключение, позвонив Server.Shutdown

Ниже приведен пример кода:

package main

import (
    "context"
    "log"
    "net/http"
    "sync/atomic"
    "syscall"
    "time"

    "os"
    "os/signal"

    "github.com/gorilla/mux"
)

type myServer struct {
    http.Server
    shutdownReq chan bool
    reqCount    uint32
}

func NewServer() *myServer {
    //create server
    s := &myServer{
        Server: http.Server{
            Addr:         ":8080",
            ReadTimeout:  10 * time.Second,
            WriteTimeout: 10 * time.Second,
        },
        shutdownReq: make(chan bool),
    }

    router := mux.NewRouter()

    //register handlers
    router.HandleFunc("/", s.RootHandler)
    router.HandleFunc("/shutdown", s.ShutdownHandler)

    //set http server handler
    s.Handler = router

    return s
}

func (s *myServer) WaitShutdown() {
    irqSig := make(chan os.Signal, 1)
    signal.Notify(irqSig, syscall.SIGINT, syscall.SIGTERM)

    //Wait interrupt or shutdown request through /shutdown
    select {
    case sig := <-irqSig:
        log.Printf("Shutdown request (signal: %v)", sig)
    case sig := <-s.shutdownReq:
        log.Printf("Shutdown request (/shutdown %v)", sig)
    }

    log.Printf("Stoping http server ...")

    //Create shutdown context with 10 second timeout
    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()

    //shutdown the server
    err := s.Shutdown(ctx)
    if err != nil {
        log.Printf("Shutdown request error: %v", err)
    }
}

func (s *myServer) RootHandler(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("Hello Gorilla MUX!\n"))
}

func (s *myServer) ShutdownHandler(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("Shutdown server"))

    //Do nothing if shutdown request already issued
    //if s.reqCount == 0 then set to 1, return true otherwise false
    if !atomic.CompareAndSwapUint32(&s.reqCount, 0, 1) {
        log.Printf("Shutdown through API call in progress...")
        return
    }

    go func() {
        s.shutdownReq <- true
    }()
}

func main() {
    //Start the server
    server := NewServer()

    done := make(chan bool)
    go func() {
        err := server.ListenAndServe()
        if err != nil {
            log.Printf("Listen and serve: %v", err)
        }
        done <- true
    }()

    //wait shutdown
    server.WaitShutdown()

    <-done
    log.Printf("DONE!")
}

Примечание. Пожалуйста, просмотрите эту проблему, которая связана с корректным завершением работы.

Вариант исходного вопроса — как завершить работу в модульном тесте? Наблюдение за сигналом ОС не требуется, таймауты не нужны, поэтому синхронизировать меньше. Кроме того, есть еще одна проблема: модульному тесту обычно приходится ждать, пока сервер начнет прослушивать, прежде чем выполнять тест.

Вот упрощенный ответ @putu для этой цели.

      package main

import (
    "context"
    "fmt"
    "net"
    "net/http"
    "sync"
    "sync/atomic"
    "testing"

    "github.com/gorilla/mux"
)

type (
    myServer struct {
        ctx      context.Context
        srv      http.Server
        stopping uint32
        err      error
        wg       sync.WaitGroup
    }
)

func newServer(ctx context.Context, port int) (s *myServer) {
    s = &myServer{
        ctx: ctx,
        srv: http.Server{
            Addr: fmt.Sprintf(":%d", port),
        },
    }

    // make the routes
    router := mux.NewRouter()
    router.HandleFunc("/", s.getRoot)

    s.srv.Handler = router
    return
}

func (s *myServer) getRoot(w http.ResponseWriter, r *http.Request) {
    // example route
    w.WriteHeader(http.StatusOK)
}

func (s *myServer) start() (err error) {
    addr := s.srv.Addr
    ln, err := net.Listen("tcp", addr)
    if err != nil {
        return
    }

    s.wg.Add(1)

    go func() {
        fmt.Println("Server started")
        err = s.srv.Serve(ln)
        if err != http.ErrServerClosed {
            s.err = err
        }
        s.wg.Done()
    }()

    return
}

func (s *myServer) stop() error {
    if atomic.CompareAndSwapUint32(&s.stopping, 0, 1) {
        s.srv.Shutdown(s.ctx)
        fmt.Println("Server stopped")
    }
    s.wg.Wait()
    return s.err
}

func TestMockServer(t *testing.T) {
    s := newServer(context.Background(), 8000)
    err := s.start()
    if err != nil {
        t.Fatal(err)
    }

    // ...do a test that makes use of the mock server...
    _, err = http.Get("http://localhost:8000")
    if err != nil {
        t.Fatal(err)
    }

    err = s.stop()
    if err != nil {
        t.Fatal(err)
    }
}
Другие вопросы по тегам