Adding graceful server shutdown
continuous-integration/drone/push Build is passing Details

main
Drew Bednar 4 months ago
parent 37404b4821
commit 514ba0cba3

@ -16,7 +16,7 @@ tmp_dir = "tmp"
include_dir = [] include_dir = []
include_ext = ["go", "tpl", "tmpl", "html", "go.tmpl"] include_ext = ["go", "tpl", "tmpl", "html", "go.tmpl"]
include_file = [] include_file = []
kill_delay = "0s" kill_delay = "1s"
log = "build-errors.log" log = "build-errors.log"
poll = false poll = false
poll_interval = 0 poll_interval = 0
@ -24,7 +24,7 @@ tmp_dir = "tmp"
pre_cmd = [] pre_cmd = []
rerun = false rerun = false
rerun_delay = 500 rerun_delay = 500
send_interrupt = false send_interrupt = true
stop_on_error = false stop_on_error = false
[color] [color]

@ -52,6 +52,7 @@ The `ratchetd` cmd binary uses Oauth so you will need to create a new Oauth App.
## Additional Resources ## Additional Resources
- [Graceful Shutdown](https://dev.to/mokiat/proper-http-shutdown-in-go-3fji)
- [Content Range Requests](https://web.archive.org/web/20230918195519/https://benramsey.com/blog/2008/05/206-partial-content-and-range-requests/) - [Content Range Requests](https://web.archive.org/web/20230918195519/https://benramsey.com/blog/2008/05/206-partial-content-and-range-requests/)
- [HTTP 204 and 205 Status Codes](https://web.archive.org/web/20230918193536/https://benramsey.com/blog/2008/05/http-status-204-no-content-and-205-reset-content/) - [HTTP 204 and 205 Status Codes](https://web.archive.org/web/20230918193536/https://benramsey.com/blog/2008/05/http-status-204-no-content-and-205-reset-content/)
- [How to Disable FileServer Directory Listings](https://www.alexedwards.net/blog/disable-http-fileserver-directory-listings) - [How to Disable FileServer Directory Listings](https://www.alexedwards.net/blog/disable-http-fileserver-directory-listings)

@ -1,12 +1,17 @@
package main package main
import ( import (
"context"
"crypto/tls" "crypto/tls"
"errors"
"flag" "flag"
"fmt" "fmt"
"io"
"log/slog" "log/slog"
"net/http" "net/http"
"os" "os"
"os/signal"
"syscall"
"time" "time"
rdb "git.runcible.io/learning/ratchet/internal/database" rdb "git.runcible.io/learning/ratchet/internal/database"
@ -22,17 +27,20 @@ import (
// commit string // commit string
// ) // )
func main() { func run(ctx context.Context, w io.Writer, args []string) error {
// CONFIGURATION // CONFIGURATION
// Parse command line options // Parse command line options
addr := flag.String("addr", "0.0.0.0", "HTTP network address") flags := flag.NewFlagSet(args[0], flag.ExitOnError)
port := flag.String("port", "5001", "HTTP port") addr := flags.String("addr", "0.0.0.0", "HTTP network address")
logLevel := flag.String("logging", "INFO", "Logging Level. Valid values [INFO, DEBUG, WARN, ERROR].") port := flags.String("port", "5001", "HTTP port")
dbPath := flag.String("database", "./ratchet.db", "A path to a sqlite3 database") logLevel := flags.String("logging", "INFO", "Logging Level. Valid values [INFO, DEBUG, WARN, ERROR].")
certPath := flag.String("cert", "./tls/cert.pem", "A public cert in .pem format") dbPath := flags.String("database", "./ratchet.db", "A path to a sqlite3 database")
keyPath := flag.String("key", "./tls/key.pem", "A private key in .pem format") certPath := flags.String("cert", "./tls/cert.pem", "A public cert in .pem format")
keyPath := flags.String("key", "./tls/key.pem", "A private key in .pem format")
// must call parse or all values will be the defaults // must call parse or all values will be the defaults
flag.Parse() if err := flags.Parse(args[1:]); err != nil {
return err
}
// DEPENDENCY INJECTION FOR HANDLERS // DEPENDENCY INJECTION FOR HANDLERS
// Setup Logging // Setup Logging
@ -41,17 +49,22 @@ func main() {
db, err := rdb.OpenSqlite3DB(*dbPath) db, err := rdb.OpenSqlite3DB(*dbPath)
if err != nil { if err != nil {
slog.Error(err.Error()) return err
os.Exit(1)
} }
// Close db connection before exiting main. // Close db connection before exiting main.
defer db.Close() defer func() {
slog.Info("Cleaning up database")
_, err := db.Exec("PRAGMA wal_checkpoint(TRUNCATE)")
if err != nil {
slog.Error(fmt.Sprintf("Error checkpointing database: %v", err))
}
db.Close()
}()
//tc, err := server.InitTemplateCache() //tc, err := server.InitTemplateCache()
tc, err := server.InitFSTemplateCache() tc, err := server.InitFSTemplateCache()
if err != nil { if err != nil {
slog.Error(err.Error()) return err
os.Exit(1)
} }
// SessionManager // SessionManager
@ -126,8 +139,37 @@ func main() {
slog.Info(fmt.Sprintf("Listening on https://%s", srv.Addr)) slog.Info(fmt.Sprintf("Listening on https://%s", srv.Addr))
err = srv.ListenAndServeTLS(*certPath, *keyPath) go func() {
if err = srv.ListenAndServeTLS(*certPath, *keyPath); !errors.Is(err, http.ErrServerClosed) {
slog.Error(err.Error()) slog.Error(err.Error())
os.Exit(1) os.Exit(1)
}
slog.Info("Stopped serving connections")
}()
// Handle graceful shutdown
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
// Block until signal is recieved
<-sigChan
shutdownCtx, shutdownRelease := context.WithTimeout(ctx, 10*time.Second)
defer shutdownRelease()
if err := srv.Shutdown(shutdownCtx); err != nil {
slog.Error("Failed to close within context timeout. Forcing server close.")
srv.Close()
return err
}
return nil
}
func main() {
ctx := context.Background()
if err := run(ctx, os.Stdout, os.Args); err != nil {
slog.Error(err.Error())
os.Exit(1)
}
} }

Loading…
Cancel
Save