Implement graceful shutdown
- Shutdown the HTTP server before exiting - Persist data to store before exiting, if applicable
This commit is contained in:
parent
8698736e7d
commit
8e2a2c4dbc
@ -3,6 +3,7 @@ package controller
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"compress/gzip"
|
"compress/gzip"
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
@ -59,7 +60,15 @@ func Handle() {
|
|||||||
if os.Getenv("ROUTER_TEST") == "true" {
|
if os.Getenv("ROUTER_TEST") == "true" {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
log.Fatal(server.ListenAndServe())
|
log.Println("[controller][Handle]", server.ListenAndServe())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shutdown stops the server
|
||||||
|
func Shutdown() {
|
||||||
|
if server != nil {
|
||||||
|
_ = server.Shutdown(context.TODO())
|
||||||
|
server = nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateRouter creates the router for the http server
|
// CreateRouter creates the router for the http server
|
||||||
|
@ -156,6 +156,7 @@ func TestHandle(t *testing.T) {
|
|||||||
config.Set(cfg)
|
config.Set(cfg)
|
||||||
_ = os.Setenv("ROUTER_TEST", "true")
|
_ = os.Setenv("ROUTER_TEST", "true")
|
||||||
_ = os.Setenv("ENVIRONMENT", "dev")
|
_ = os.Setenv("ENVIRONMENT", "dev")
|
||||||
|
defer os.Clearenv()
|
||||||
Handle()
|
Handle()
|
||||||
request, _ := http.NewRequest("GET", "/health", nil)
|
request, _ := http.NewRequest("GET", "/health", nil)
|
||||||
responseRecorder := httptest.NewRecorder()
|
responseRecorder := httptest.NewRecorder()
|
||||||
@ -167,3 +168,12 @@ func TestHandle(t *testing.T) {
|
|||||||
t.Fatal("server should've been set (but because we set ROUTER_TEST, it shouldn't have been started)")
|
t.Fatal("server should've been set (but because we set ROUTER_TEST, it shouldn't have been started)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestShutdown(t *testing.T) {
|
||||||
|
// Pretend that we called controller.Handle(), which initializes the server variable
|
||||||
|
server = &http.Server{}
|
||||||
|
Shutdown()
|
||||||
|
if server != nil {
|
||||||
|
t.Error("server should've been shut down")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
22
main.go
22
main.go
@ -1,17 +1,37 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"log"
|
||||||
"os"
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
"github.com/TwinProduction/gatus/config"
|
"github.com/TwinProduction/gatus/config"
|
||||||
"github.com/TwinProduction/gatus/controller"
|
"github.com/TwinProduction/gatus/controller"
|
||||||
|
"github.com/TwinProduction/gatus/storage"
|
||||||
"github.com/TwinProduction/gatus/watchdog"
|
"github.com/TwinProduction/gatus/watchdog"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
cfg := loadConfiguration()
|
cfg := loadConfiguration()
|
||||||
go watchdog.Monitor(cfg)
|
go watchdog.Monitor(cfg)
|
||||||
controller.Handle()
|
go controller.Handle()
|
||||||
|
// Wait for termination signal
|
||||||
|
sig := make(chan os.Signal, 1)
|
||||||
|
done := make(chan bool, 1)
|
||||||
|
signal.Notify(sig, os.Interrupt, os.Kill, syscall.SIGTERM)
|
||||||
|
go func() {
|
||||||
|
<-sig
|
||||||
|
log.Println("Received interruption signal, attempting to gracefully shut down")
|
||||||
|
controller.Shutdown()
|
||||||
|
err := storage.Get().Save()
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Failed to save storage provider:", err.Error())
|
||||||
|
}
|
||||||
|
done <- true
|
||||||
|
}()
|
||||||
|
<-done
|
||||||
|
log.Println("Shutting down")
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadConfiguration() *config.Config {
|
func loadConfiguration() *config.Config {
|
||||||
|
@ -42,7 +42,19 @@ func Initialize(cfg *Config) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
go provider.(*memory.Store).AutoSave(7 * time.Minute)
|
go autoSave(7 * time.Minute)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// autoSave automatically calls the Save function of the provider at every interval
|
||||||
|
func autoSave(interval time.Duration) {
|
||||||
|
for {
|
||||||
|
time.Sleep(interval)
|
||||||
|
log.Printf("[storage][autoSave] Saving")
|
||||||
|
err := provider.Save()
|
||||||
|
if err != nil {
|
||||||
|
log.Println("[storage][autoSave] Save failed:", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -3,8 +3,6 @@ package memory
|
|||||||
import (
|
import (
|
||||||
"encoding/gob"
|
"encoding/gob"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"log"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/TwinProduction/gatus/core"
|
"github.com/TwinProduction/gatus/core"
|
||||||
"github.com/TwinProduction/gatus/util"
|
"github.com/TwinProduction/gatus/util"
|
||||||
@ -94,17 +92,8 @@ func (s *Store) Clear() {
|
|||||||
|
|
||||||
// Save persists the cache to the store file
|
// Save persists the cache to the store file
|
||||||
func (s *Store) Save() error {
|
func (s *Store) Save() error {
|
||||||
return s.cache.SaveToFile(s.file)
|
if len(s.file) > 0 {
|
||||||
}
|
return s.cache.SaveToFile(s.file)
|
||||||
|
|
||||||
// AutoSave automatically calls the Save function at every interval
|
|
||||||
func (s *Store) AutoSave(interval time.Duration) {
|
|
||||||
for {
|
|
||||||
time.Sleep(interval)
|
|
||||||
log.Printf("[memory][AutoSave] Persisting data to file")
|
|
||||||
err := s.Save()
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("[memory][AutoSave] failed to save to file=%s: %s", s.file, err.Error())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -250,3 +250,22 @@ func TestStore_DeleteAllServiceStatusesNotInKeys(t *testing.T) {
|
|||||||
t.Error("firstService should still exist")
|
t.Error("firstService should still exist")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestStore_Save(t *testing.T) {
|
||||||
|
files := []string{
|
||||||
|
"",
|
||||||
|
t.TempDir() + "/test.db",
|
||||||
|
}
|
||||||
|
for _, file := range files {
|
||||||
|
t.Run(file, func(t *testing.T) {
|
||||||
|
store, err := NewStore(file)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("expected no error, got", err.Error())
|
||||||
|
}
|
||||||
|
err = store.Save()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("expected no error, got", err.Error())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -26,6 +26,9 @@ type Store interface {
|
|||||||
|
|
||||||
// Clear deletes everything from the store
|
// Clear deletes everything from the store
|
||||||
Clear()
|
Clear()
|
||||||
|
|
||||||
|
// Save persists the data if and where it needs to be persisted
|
||||||
|
Save() error
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
Loading…
x
Reference in New Issue
Block a user