go – Channel and signal usage in graceful application shutdown-ThrowExceptions

Exception or error:

The example below handles graceful server shutdown as documented here. Based on my R&D work after going through other posts and examples, I came up with this piece of work which actually gracefully shuts down the server. However, I cannot justify if I am using channels and signals correctly or not because of my very limited experience/knowledge with Golang. I just want to avoid Goroutine leaks and other unseen potential issues. Could please someone tell me if this piece of work is idiomatic Go code and usage of channels&signals?

internal/server/server.go

package server

import (
    "net/http"
)

type Server struct {
    *http.Server
}

func NewServer() Server {
    return Server{
        &http.Server{Addr: ":8080", Handler: nil},
    }
}

func (s Server) Start() error {
    if err := s.ListenAndServe(); err != http.ErrServerClosed {
        return err
    }

    return nil
}

cmd/api/main.go

package main

import (
    "api/internal/app"
    "api/internal/server"
    "context"
    "fmt"
    "os"
    "os/signal"
    "syscall"
)

func main() {
    // Some other bootstrapping and configuration code here ...
    // ....

    fmt.Printf("app starting")

    sigChan := make(chan os.Signal, 1)

    signal.Notify(sigChan, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)

    ctx, cancel := context.WithCancel(context.Background())

    go listenSignals(cancel, sigChan)

    if err := app.New(server.NewServer()).Start(ctx); err != nil {
        panic(err)
    }

    fmt.Print("app shutdown completed")
}

func listenSignals(cancel context.CancelFunc, sigChan chan os.Signal)  {
    sig := <-sigChan

    fmt.Printf("app shutdown started with %v signal", sig)

    cancel()
}

internal/app/api.go

package app

import (
    "api/internal/server"
    "context"
    "errors"
    "fmt"
    "time"
)

type App struct {
    srv server.Server
}

func New(srv server.Server) App {
    return App{srv: srv}
}

func (app App) Start(ctx context.Context) error {
    doneChan := make(chan struct{})

    go shutdown(ctx, doneChan, app)

    // Start HTTP server    
    if err := a.srv.Start(); err != nil {
        return errors.New("failed to start HTTP server")
    }

    // Start DB server
    // ...

    <-doneChan

    return nil
}

func shutdown(ctx context.Context, doneChan chan<- struct{}, app App) {
    <-ctx.Done()

    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()

    // Shutdown HTTP server  
    err := app.srv.Shutdown(ctx)
    if err != nil {
        fmt.Print("some active HTTP connections are interrupted at shutdown")
    } else {
        fmt.Print("no HTTP connections are interrupted at shutdown")
    }

    // Shutdown DB server
    // ...

    close(doneChan)
}

OUTPUT

INFO[0000] app starting
^C
INFO[0001] app shutdown started with interrupt signal
INFO[0001] no HTTP connections are interrupted at shutdown
INFO[0001] the application shutdown has been completed


INFO[0000] app starting
^C
INFO[0001] app shutdown started with interrupt signal
INFO[0001] some active HTTP connections are interrupted at shutdown
INFO[0001] the application shutdown has been completed

EDIT

I did a bit of goroutine profiling and the finding is as follows below. I am not sure if the outcome is normal! Need your input.

  • The response for a single request returns 6 for runtime.NumGoroutine() – pprof stack trace is here. However, if I send 10 concurrent requests, it is between 17 and 22.

I don’t know how to read this so two questions:

  1. Is 6 goroutines are normal for one request?
  2. Is the increase normal for multiple concurrent requests?
    • I think it is normal because, I am using httprouter and it kicks in for each request.
How to solve:

Leave a Reply

Your email address will not be published. Required fields are marked *