Skip to content

Commit

Permalink
cogolabs/terminator
Browse files Browse the repository at this point in the history
  • Loading branch information
jcalabro committed Apr 6, 2022
1 parent 8813233 commit 8dd98ef
Show file tree
Hide file tree
Showing 2 changed files with 128 additions and 0 deletions.
53 changes: 53 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Terminator

<img src="https://user-images.githubusercontent.com/8205547/162049312-28e505f3-100c-47b4-a168-78d4ff9dd19f.jpg" />

Library to automatically run and kill HTTP servers at regular intervals. Includes a jitter on the kill interval to ensure that a group of server processes started at roughly the same time don't all go down at once.

## Usage:

```go
package main

import (
"flag"
"log"
"net/http"
"time"

"github.com/cogolabs/terminator"
)

const (
timeout = 30 * time.Second
)

var (
shutdownAfter = flag.Duration("shutdownAfter", 12*time.Hour, "Duration to wait before shutting down (with jitter)")
shutdownJitter = flag.Duration("shutdownJitter", 2*time.Hour, "Jitter duration in either direction of shutdownAfter")
)

func main() {
flag.Parse()

srv := &http.Server{
Addr: "0.0.0.0:8080",
Handler: nil, // your handler here
ReadTimeout: timeout,
WriteTimeout: timeout,
}

log.Println("starting server")
err := terminator.ServeAndShutdownAfter(&terminator.Options{
Server: srv,
ShutdownAfter: *shutdownAfter,
Jitter: *shutdownJitter,
GracefulShutdownPeriod: 30 * time.Second,
})
if err != nil {
log.Fatalf("error in server: %v\n", err)
}

log.Println("server done")
}
```
75 changes: 75 additions & 0 deletions terminator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package terminator

import (
"context"
"math/rand"
"net/http"
"os"
"os/signal"
"sync"
"syscall"
"time"
)

type Options struct {
// Server is the HTTP server that will be started and eventually
// gracefully shut down
Server *http.Server

// ShutdownAfter indicates how long we should keep the HTTP
// server alive before starting to shutdown
ShutdownAfter time.Duration

// Jitter represents a random offset in either direction of
// ShutdownAfter that we'll add to ShutdownAfter. This ensures
// that not all servers go down at once, thus avoiding outages.
Jitter time.Duration

// GracefulShutdownPeriod represents the maximum amount of time
// we allow in-flight requests to finish before we force shutdown
GracefulShutdownPeriod time.Duration
}

// ServeAndShutdownAfter starts the given HTTP server, waits for the given shutdownAfter duration,
// then gracefully shuts the server down and returns to the caller. The maximum amount of time
// in-progress requests are given is represented by gracefulShutdownTimeout.
func ServeAndShutdownAfter(opts *Options) error {
sig := make(chan os.Signal, 1)
signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)

wg := sync.WaitGroup{}
wg.Add(1)

go func() {
defer wg.Done()

// add a random jitter in the range of (-n, n) seconds to the max shutdown
// time to ensure not all servers in a deployment go down at the same time
rng := rand.New(rand.NewSource(time.Now().UnixNano()))
jitterSecs := int(opts.Jitter.Seconds())
offset := rng.Intn(2*jitterSecs) - jitterSecs
offsetSecs := time.Second * time.Duration(offset)
waitFor := opts.ShutdownAfter + offsetSecs

// wait for a SIGINT to arrive or for the wait duration to elapse
select {
case <-sig:
case <-time.After(waitFor):
}

ctx, cancel := context.WithTimeout(context.Background(), opts.GracefulShutdownPeriod)
defer cancel()

// gracefully shut down the server
opts.Server.SetKeepAlivesEnabled(false)
opts.Server.Shutdown(ctx)
}()

err := opts.Server.ListenAndServe()
if err != nil && err != http.ErrServerClosed {
return err
}

wg.Wait()
return nil
}

0 comments on commit 8dd98ef

Please sign in to comment.