Last active
June 5, 2023 18:31
-
-
Save mtilson/00f72d7cbd98e3d1b9cf2c8bb9ec39b7 to your computer and use it in GitHub Desktop.
how to use context for graceful shutdown [golang]
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package main | |
import ( | |
"context" | |
"log" | |
"net" | |
"os" | |
"os/signal" | |
"time" | |
) | |
const ( | |
// PORT to listen to | |
PORT = ":8080" | |
) | |
func main() { | |
// get new empty context with cancel func | |
ctx, cancelFunc := context.WithCancel(context.Background()) | |
// spawn func which will listen for `os.Interrupt` signal (`Ctrl-C`) from OS | |
go goroutineProcessSignals(cancelFunc) | |
// start our server - this is blocking func | |
if err := listenSocket(ctx); err != nil { | |
log.Fatal(err) | |
} | |
} | |
func goroutineProcessSignals(cancelFunc context.CancelFunc) { | |
// channel of type `os.Signal` | |
signalChan := make(chan os.Signal) | |
// notify OS that we would like to receive (register) signal `os.Interrupt` on the `signalChan` channel | |
signal.Notify(signalChan, os.Interrupt) | |
// receive all registered signals | |
// it is not necessary to have loop here, cause we registered only for one signal, but it is useful | |
// example in case we will sign up for other ones, like `SIGHUP` | |
for { | |
// read from `signalChan` channel as we ordered with help of `signal.Notify()` above | |
// here the only `os.Interrupt` signal can be received, as it is the only one we registered | |
// in out `signal.Notify()` call, and because of this it is not necessary to use `for {}` loop | |
// in this case, but if we would like to process more than 1 signal (e.g. `syscall.SIGTERM`) | |
// then we do need this `for {}` loop | |
sig := <-signalChan | |
switch sig { | |
// check if the received signal type is `os.Interrupt` | |
case os.Interrupt: | |
// call our cancel func received as an argument and return | |
log.Println("Signal SIGINT is received, probably due to `Ctrl-C`, exiting ...") | |
cancelFunc() | |
return | |
} | |
} | |
} | |
func listenSocket(ctx context.Context) error { | |
// it is necessary to be able to use in net.ListenTCP() | |
localAddr, err := net.ResolveTCPAddr("tcp", PORT) | |
if err != nil { | |
return err | |
} | |
// l, err := net.Listen("tcp", PORT) // return (Listener, error) | |
// is not possible to use `net.Listen()` as it returns `Listener` interface which has no method to setup timeout | |
// we need to use net.ListenTCP() as it returns `TCPListener` which has method `SetDeadline()` to setup | |
// timeout, which we need to be able to periodically check if channel, returned by `ctx.Done()`, is not | |
// closed by processing `os.Interrupt` case in our goroutineProcessSignals() handler | |
l, err := net.ListenTCP("tcp", localAddr) // return (*TCPListener, error) | |
if err != nil { | |
return err | |
} | |
defer l.Close() | |
log.Println("Start listening on the TCP socket", PORT, ".") | |
// accept all connnections received to the created TCP-socket and also monitor for receiving | |
// `os.Interrupt` signal (by processing `Ctrl-C`) | |
for { | |
select { | |
// `ctx.Done()` return RO channel which will be closed if `cancelFunc()` is called; | |
// we will be able to read from this channel and determine that the channel is closed | |
case <-ctx.Done(): | |
log.Println("Stop listening on the TCP socket", PORT, ".") | |
// `l.Close()` will be called on return here (and on all other return operations) as we | |
// registered it with `defer l.Close()` | |
return nil | |
default: | |
// without this timeout we will wait here (in this default selection) till somebody | |
// connected to the listening socket; this means that we can miss `os.Interrupt` signal, | |
// i.e. it will be processed long after the moment it was generated | |
// timeout can be applied only to TCP listner, it means we cannot use `net.Listen()` | |
// but should use `net.ListenTCP()` | |
// timeout will be applied to the "blocking" operation `l.Accept()` and every time | |
// timeout is expired an error of type `os.IsTimeout` will be returned by `l.Accept()` | |
if err := l.SetDeadline(time.Now().Add(time.Second)); err != nil { | |
// this is an error for registering timeout with SetDeadline() | |
return err | |
} | |
// this is "blocking" operation of our main.listenSocket() function | |
_, err := l.Accept() | |
if err != nil { | |
// if it is due to out timeout expiration we will continue | |
if os.IsTimeout(err) { | |
continue | |
} | |
// exit on other errors; another option is just Log(err) and continue this loop | |
return err | |
} | |
// if `l.Accept()` returned error is `nil`, then we have new connection on the listening socket | |
log.Println("New connection to the listening TCP socket", PORT, ".") | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
2019/12/21 15:54:32 Start listening on the TCP socket ':8080' . | |
2019/12/21 15:54:42 New connection to the listening TCP socket ':8080' . | |
2019/12/21 15:54:56 New connection to the listening TCP socket ':8080' . | |
^C2019/12/21 15:55:00 Signal SIGINT is received, probably due to `Ctrl-C`, exiting ... | |
2019/12/21 15:55:00 Stop listening on the TCP socket ':8080' . |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
I think moving the select
<-ctx.Done()
statement to a goroutine is a better idea.