Попытка выключить gin-сервер… (советы по goroutines и каналам)

Сегодня я пытался изящно выключить мой gin-сервер и запустить его снова через само приложение, без повторного запуска моего приложения.
Я узнал несколько ценных вещей, которыми с удовольствием поделюсь с вами.

Итак, в чем проблема?

Запуск сервера, используя gin.Run() или http.ListenAndServe(), блокирует goroutine, если он находится в главном процессе, поэтому он блокирует главную goroutine:

func main() {
  ginApp := gin.Default()
  ginApp.Run(":80") // This code is blocking
  // next lines will never run because of blocking code.
  FuncForStoppingGinServer() // This function can't be used.
}
Вход в полноэкранный режим Выйти из полноэкранного режима

Использовать другую goroutine

Мы можем просто заблокировать другую goroutine и помочь главной goroutine остаться свободной.

func main() {
  ginApp := gin.Default()
  go ginApp.Run(":80") // This code is not blocking anymore.
  FuncForStoppingGinServer() // This function will run now.
}
Войти в полноэкранный режим Выйти из полноэкранного режима

Но что мы должны делать внутри FuncForStoppingGinServer()?

Ничего!
Ответ заключается в том, что у самого gin нет никакой функции для отключения его сервера!
Но прежде чем разбираться с этим, давайте попробуем еще один неверный способ!!!

Просто убейте его!

Вы думаете о том, как остановить свой сервер gin, используя goroutines и каналы? (забудьте пока об обработке активных соединений)

var ginApp *gin.Engine

func main() {
  // Make an engine
  ginApp = gin.Default()

  // Make a stop channel
  stopChan := make(chan struct{})

  go func() {
    ginApp.Run(":80") // blocking process
    fmt.Println(">> 1 <<")
    for range stopChan {
      return
    }
  }()
  fmt.Println(">> 2 <<")
  // terminate gin server after 3 seconds
  time.Sleep(time.Second * 3)
  stopChan <- struct{}{}
  fmt.Println(">> 3 <<")
}
Войти в полноэкранный режим Выйти из полноэкранного режима

Это звучит как хороший подход, но что, по-вашему, здесь не так?

Какие числа будут выведены? (1, 2, 3)?

Как я уже говорил, ginApp.Run(":80") — блокирующий процесс, и он блокирует свою goroutine, поэтому мы пошлем в канал stopChan,
НО,
мы никогда не сможем прочитать из stopChan, используя наш for range loop, потому что это произошло после блокирующего процесса и «>> 1 <<» не будет напечатано.

А так как наш канал не буферизован, то отправитель, то есть главная горутина, тоже будет заблокирован, пока кто-то не прочитает из него!, ровно перед тем, как напечатать «>> 3 <<«, и он тоже не будет напечатан.

«>> 2 <<» будет выведено в любом случае 😄.

Дополнительный забавный факт: Если мы сделаем наш канал буферизованным (размер 1 достаточен для этого случая), то главная горутина будет посылать на него и идти вперед, затем она завершит свою работу и закроется, когда работа главной горутины будет закончена, вся программа будет существовать! и любая другая горутина умрет вместе с ней, просто сделайте это изменение и посмотрите на результат:

stopChan := make(chan struct{}, 1)
Вход в полноэкранный режим Выход из полноэкранного режима

Я также должен упомянуть, что chan struct{} как тип канала и пустой struct struct{}{} не будут потреблять никакой памяти вообще, и это известно как канал только для сигналов.

Наконец я понял, что таким образом эту проблему не решить:
Есть ли способ остановить длинную блокирующую функцию?

Вы также можете узнать больше об остановке горутин в этом Вопросе: Как остановить горутину
И я рассмотрю больше примеров позже. (Вы можете следовать за мной, чтобы быть замеченным)

Вернемся к проблеме!

Итак, использование Goroutines не решило нашу проблему,
Как насчет того, чтобы попробовать метод http.Server.Shutdown()?

var ginApp *gin.Engine

func main() {
  // Make an engine
  ginApp = gin.Default()

  // Make a http server
  httpServer := &http.Server{
    Addr:    ":80",
    Handler: ginApp,
  }

  // Launch http server in a separate goroutine
  go httpServer.ListenAndServe()

  // Stop the server
  time.Sleep(time.Second * 5)
  fmt.Println("We're going to stop gin server")
  httpServer.Shutdown(context.Background())
}
Вход в полноэкранный режим Выйти из полноэкранного режима

Возможно, вам будет полезно прочитать этот вопрос: Graceful stop of gin server

Это работает, но что все еще не так?

Теперь мы выключаем наш сервер внутри нашей программы, но как мы можем запустить его снова?
Может быть, запустив ListenAndServe() снова?

Как я уже спрашивал в этом вопросе: Как остановить и запустить сервер gin несколько раз за один запуск
Я пытаюсь реализовать что-то вроде этого полукода:

srv := NewServer()
srv.Start()
srv.Stop()
srv.Start()
srv.Stop()
srv.Start()
Вход в полноэкранный режим Выход из полноэкранного режима

Давайте сделаем это путем небольшого рефакторинга.

type Server struct {
  httpServer *http.Server
}

func main() {
  srv := NewHTTPServer()
  srv.Start()
  time.Sleep(time.Second * 2)
  srv.Stop()
  time.Sleep(time.Second * 2)
  srv.Start()

  select {} // Simulate other processes
}

func NewHTTPServer() *Server {
  return &Server{
    httpServer: &http.Server{
      Addr:    ":80",
      Handler: gin.Default(),
    },
  }
}

func (s *Server) Start() {
  go func() {
    if err := s.httpServer.ListenAndServe(); err != nil {
      fmt.Println(">>>", err)
    }
  }()
}

func (s *Server) Stop() {
  s.httpServer.Shutdown(context.Background())
}
Вход в полноэкранный режим Выйти из полноэкранного режима

Запустите этот код и посмотрите, что произойдет!
Выход:

...Gin logs...
>>> http: Server closed
>>> http: Server closed
Вход в полноэкранный режим Выход из полноэкранного режима

Что? Мы вызвали srv.Stop() только один раз, но наш сервер закрылся дважды!
Почему?
Согласно документации по функциям .Shutdown():

После вызова Shutdown на сервере его нельзя использовать повторно; будущие вызовы таких методов, как Serve, будут возвращать ErrServerClosed.

Итак, официально это не работает!

Но есть еще одно маленькое изменение, мы можем заставить сервер структурироваться заново с нуля, каждый раз, когда мы его запускаем.

Добавьте функцию serve в последний код и измените функцию main, остальные коды те же.

func main() {
  srv := serve()
  time.Sleep(time.Second * 2)
  srv.Stop()
  time.Sleep(time.Second * 2)
  srv = serve()
  srv.Start()

  select {} // Simulate other processes
}

func serve() *Server {
  srv := NewHTTPServer()
  // Register handlers or other stuff
  srv.Start()
  return srv
}
Вход в полноэкранный режим Выход из полноэкранного режима

Это то, что мы хотим, но вы можете сделать код чище.

Другие решения

В моем случае я смог использовать Fiber вместо этого, он гораздо лучше вписывается в мои проекты, но это может быть не то решение, которое вы ищете,
Также вы можете взглянуть на эту страницу: Благодатный перезапуск или остановка

Заключительная цитата

Эта статья вызвала у меня разные чувства, это был путь вверх и вниз с некоторыми советами и некоторыми неработающими подходами, я надеюсь, что вы чувствуете то же самое, и я буду рад услышать ваши мысли.

Оцените статью
devanswers.ru
Добавить комментарий