Skip to content

Commit

Permalink
Added tests and reload/stop functions. Moved to go mod.
Browse files Browse the repository at this point in the history
  • Loading branch information
chapsuk committed Mar 5, 2019
1 parent 60ab3c2 commit 6151b06
Show file tree
Hide file tree
Showing 7 changed files with 220 additions and 43 deletions.
11 changes: 11 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
language: go

go:
- "1.12.x"

script:
- go test -v -covermode=count -coverprofile=coverage.out ./...

after_success:
- goveralls -coverprofile=coverage.out -service=travis-ci -repotoken $COVERALLS_TOKEN
- bash <(curl -s https://codecov.io/bash) -f coverage.out
29 changes: 14 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,22 +1,21 @@
# Grace pkg
# Grace

Package with single function for create base context which will be canceled on signals:
`SIGINT`, `SIGTERM`, `SIGHUP`.
[![Build Status](https://travis-ci.org/chapsuk/grace.svg?branch=master)](https://travis-ci.org/chapsuk/grace)
[![GoDoc](https://img.shields.io/badge/GoDoc-Reference-blue.svg
)](https://godoc.org/github.com/chapsuk/grace)
[![codecov](https://codecov.io/gh/chapsuk/grace/branch/master/graph/badge.svg)](https://codecov.io/gh/chapsuk/grace)

## Example
Package grace implements set of helper functions around `syscal.Signals`
for gracefully shutdown and reload (micro|macro|nano) services.

```go
package main
```bash
go get -u github.com/chapsuk/grace
```

import (
"context"
No breaking changes, follows SemVer strictly.

"github.com/chapsuk/grace"
)
## Testing

func main() {
ctx := grace.ShutdownContext(context.Background())
<-ctx.Done()
// do graceful shutdown after context was canceled
}
```go
go test -v -cover ./...
```
76 changes: 76 additions & 0 deletions context.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// Package grace implements set of helper functions around syscal.Signals
// for gracefully shutdown and reload the service.
package grace

import (
"context"
"os"
"os/signal"
"syscall"
)

// ShutdownContext returns child context from passed context which will be canceled
// on incoming signals: SIGINT, SIGTERM, SIGHUP.
// Ends immediately by os.Exit(1) after second signal
func ShutdownContext(c context.Context) context.Context {
return cancelContextOnSignals(c, true, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP)
}

// ReloadContext returns child context which will be canceled on syscall.SIGINT or syscall.SIGTERM signal
func StopContext(c context.Context) context.Context {
return cancelContextOnSignals(c, true, syscall.SIGINT, syscall.SIGTERM)
}

// ReloadContext returns child context which will be canceled on syscall.SIGHUP signal
func ReloadChannel(ctx context.Context) <-chan struct{} {
done := make(chan struct{})
sighups := make(chan struct{})
go func() {
ch := make(chan os.Signal, 1)
signal.Notify(ch, syscall.SIGHUP)
close(done)
for {
select {
case <-ctx.Done():
return
case <-ch:
sighups <- struct{}{}
}
}
}()
<-done
return sighups
}

func cancelContextOnSignals(c context.Context, osExitOnSecond bool, signals ...os.Signal) context.Context {
ctx, cancel := context.WithCancel(c)
done := make(chan struct{})
go listenSignalsFunc(ctx, cancel, done, osExitOnSecond, signals...)()
<-done
return ctx
}

func listenSignalsFunc(
ctx context.Context,
cancel context.CancelFunc,
done chan struct{},
osExitOnSecond bool,
signals ...os.Signal,
) func() {
return func() {
ch := make(chan os.Signal, 1)
signal.Notify(ch, signals...)
close(done)

select {
case <-ctx.Done():
return
case <-ch:
cancel()
if osExitOnSecond {
<-ch
os.Exit(1)
}
}
}
}
112 changes: 112 additions & 0 deletions context_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package grace_test

import (
"context"
"os"
"syscall"
"testing"
"time"

"github.com/chapsuk/grace"
"github.com/chapsuk/keymon"
)

const (
success = 1
failed = 2
)

func TestShutdownContext(t *testing.T) {
osExit := make(chan int)
keymon.Patch(os.Exit, func(code int) { osExit <- code })
defer keymon.Unpatch(os.Exit)

assertSignals(t, grace.ShutdownContext(context.Background()), osExit, syscall.SIGTERM)
assertSignals(t, grace.ShutdownContext(context.Background()), osExit, syscall.SIGHUP)
assertSignals(t, grace.ShutdownContext(context.Background()), osExit, syscall.SIGINT)
assertParent(t, grace.ShutdownContext)
}

func TestStopContext(t *testing.T) {
osExit := make(chan int)
keymon.Patch(os.Exit, func(code int) { osExit <- code })
defer keymon.Unpatch(os.Exit)

assertSignals(t, grace.StopContext(context.Background()), osExit, syscall.SIGTERM)
assertSignals(t, grace.StopContext(context.Background()), osExit, syscall.SIGINT)
assertParent(t, grace.StopContext)
}

func TestReloadChannel(t *testing.T) {
reloadChan := grace.ReloadChannel(context.Background())

for i := 0; i < 5; i++ {
syscall.Kill(syscall.Getpid(), syscall.SIGHUP)
select {
case <-reloadChan:
t.Logf("#%d reload signal as expected", i)
case <-time.After(5 * time.Millisecond):
t.Fatalf("#%d reload signal timeout", i)
}
}

ctx, cancel := context.WithCancel(context.Background())
rchan := grace.ReloadChannel(ctx)

cancel()
select {
case <-rchan:
t.Fatal("Received the reload signal when parent context canceled")
case <-time.After(5 * time.Millisecond):
t.Log("As expected the reload signal not published when context canceled")
}
}

func assertParent(t *testing.T, createFunc func(context.Context) context.Context) {
ctx, cancel := context.WithCancel(context.Background())
cctx := grace.ShutdownContext(ctx)

cancel()
select {
case <-cctx.Done():
t.Log("Returns child context as expected")
case <-time.After(5 * time.Millisecond):
t.Fatal("Returns not child context")
}
}

func assertSignals(t *testing.T, ctx context.Context, osExit chan int, sig syscall.Signal) {
testResult := make(chan int)
go func() {
select {
case <-ctx.Done():
testResult <- success
case <-time.After(time.Second):
testResult <- failed
}
}()

syscall.Kill(syscall.Getpid(), sig)
switch <-testResult {
case failed:
t.Fatalf("Wait context done timeout, signal '%s'", sig)
case success:
t.Logf("Context canceled as expected for '%s' signal", sig)
default:
t.Fatal("Unexpected test result")
}

if osExit != nil {
syscall.Kill(syscall.Getpid(), sig)
select {
case exitCode := <-osExit:
if exitCode != 1 {
t.Fatalf("Incorrect exit code for second '%s' signal, expected: 1 actual: %d",
sig, exitCode)
}
t.Logf("Received second '%s' signal, exit code 1 as expected", sig)
case <-time.After(time.Second):
t.Fatalf("Waiting os.Exit timeout, signal '%s'", sig)
}
}
}
5 changes: 5 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module github.com/chapsuk/grace

go 1.12

require github.com/chapsuk/keymon v0.1.3
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
github.com/chapsuk/keymon v0.1.3 h1:xH+cHxuFVn/zkyEC/J2yDoidKXnC0JVXqTRtXFTEVpY=
github.com/chapsuk/keymon v0.1.3/go.mod h1:hgWGaTfsSAwZGoN1uAzn6UxOUbGZ35oQ2QWIntWktuI=
28 changes: 0 additions & 28 deletions shutdown.go

This file was deleted.

0 comments on commit 6151b06

Please sign in to comment.