Repository: vrecan/death Branch: master Commit: 88275e7df60e Files: 11 Total size: 21.5 KB Directory structure: gitextract_9tv2jfz4/ ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── death.go ├── death_unix_test.go ├── death_windows_test.go ├── deathlog.go ├── go.mod ├── go.sum └── pkgPath_test.go ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ # Created by .ignore support plugin (hsz.mobi) ### Go template # Compiled Object files, Static and Dynamic libs (Shared Objects) *.o *.a *.so # Folders _obj _test .idea # Architecture specific extensions/prefixes *.[568vq] [568vq].out *.cgo1.go *.cgo2.c _cgo_defun.c _cgo_gotypes.go _cgo_export.* _testmain.go *.exe *.test *.prof ================================================ FILE: .travis.yml ================================================ language: go sudo: false go: - "1.14" before_install: - go get github.com/mattn/goveralls - export PATH=$PATH:$HOME/gopath/bin - go install github.com/vrecan/death/./... script: - $GOPATH/bin/goveralls -service=travis-ci -race -v ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2015 Ben Aldrich Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # Death [](https://travis-ci.org/vrecan/death) [](https://coveralls.io/github/vrecan/death?branch=master) [](https://pkg.go.dev/github.com/vrecan/death/v3) [](https://gitter.im/vrecan/death?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
Simple library to make it easier to manage the death of your application.
## Get The Library Use gopkg.in to import death based on your logger. | Version | Go Get URL | source | doc | Notes | | ------- | ------------------------------------------------------------------------------------ | ---------------------------------------------------------------- | ---------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | 3.x | [github.com/vrecan/death/v3](https://github.com/vrecan/death/tree/release-branch.v3) | [source](https://github.com/vrecan/death/tree/release-branch.v3) | [doc](https://pkg.go.dev/github.com/vrecan/death/v3) | This removes the need for an independent logger. By default death will not log but will return an error if all the closers do not properly close. If you want to provide a logger just satisfy the deathlog.Logger interface. This also uses go modules so import it as `github.com/vrecan/death/v3` | | 2.x | [gopkg.in/vrecan/death.v2](https://gopkg.in/vrecan/death.v2) | [source](https://github.com/vrecan/death/tree/v2.0) | [doc](https://godoc.org/gopkg.in/vrecan/death.v2) | This supports loggers who _do not_ return an error from their `Error` and `Warn` functions like [logrus](https://github.com/sirupsen/logrus) | | 1.x | [gopkg.in/vrecan/death.v1](https://gopkg.in/vrecan/death.v1) | [souce](https://github.com/vrecan/death/tree/v1.0) | [doc](https://godoc.org/gopkg.in/vrecan/death.v1) | This supports loggers who _do_ return an error from their `Error` and `Warn` functions like [seelog](https://github.com/cihub/seelog) | Example ```bash go get github.com/vrecan/death/v3 ``` ## Use The Library ```go package main import ( DEATH "github.com/vrecan/death/v3" SYS "syscall" ) func main() { death := DEATH.NewDeath(SYS.SIGINT, SYS.SIGTERM) //pass the signals you want to end your application //when you want to block for shutdown signals death.WaitForDeath() // this will finish when a signal of your type is sent to your application } ``` ### Close Other Objects On ShutdownOne simple feature of death is that it can also close other objects when shutdown starts
```go package main import ( "log" DEATH "github.com/vrecan/death/v3" SYS "syscall" "io" ) func main() { death := DEATH.NewDeath(SYS.SIGINT, SYS.SIGTERM) //pass the signals you want to end your application objects := make([]io.Closer, 0) objects = append(objects, &NewType{}) // this will work as long as the type implements a Close method //when you want to block for shutdown signals err := death.WaitForDeath(objects...) // this will finish when a signal of your type is sent to your application if err != nil { log.Println(err) os.Exit(1) } } type NewType struct { } func (c *NewType) Close() error { return nil } ``` ### Or close using an anonymous function ```go package main import ( DEATH "github.com/vrecan/death/v3" SYS "syscall" ) func main() { death := DEATH.NewDeath(SYS.SIGINT, SYS.SIGTERM) //pass the signals you want to end your application //when you want to block for shutdown signals death.WaitForDeathWithFunc(func(){ //do whatever you want on death }) } ``` # Release Process ## Rules for release branches: - If you are releasing a new major version you need to branch off of master into a branch `release-branch.v#` (example `release-branch.v2` for a 2.x release) - If you are releasing a minor or patch update to an existing major release make sure to merge master into the release branch ## Rules for tagging and publishing the release When you are ready to publish the release make sure you... 1. Merge your changes into the correct release branch. 2. Check out the release branch locally (example: `git pull origin release-branch.v3`) 3. Create a new tag for the specific release version you will publish (example: `git tag v3.0.1`) 4. Push the tag up to github (example: `git push origin v3.0.1`) 5. Go to the release tab in github 6. Select the target branch as the release branch and type in the tag name (tagname should include `v` so example: `v3.0.1`) 7. Write a title and a well worded description on exactly what is in this change 8. Click publish release ================================================ FILE: death.go ================================================ package death //Manage the death of your application. import ( "fmt" "io" "os" "os/signal" "reflect" "strings" "sync" "time" ) // Death manages the death of your application. type Death struct { wg *sync.WaitGroup sigChannel chan os.Signal callChannel chan struct{} timeout time.Duration log Logger } // closer is a wrapper to the struct we are going to close with metadata // to help with debuging close. type closer struct { Index int C io.Closer Name string PKGPath string Err error } // NewDeath Create Death with the signals you want to die from. func NewDeath(signals ...os.Signal) (death *Death) { death = &Death{timeout: 10 * time.Second, sigChannel: make(chan os.Signal, 1), callChannel: make(chan struct{}, 1), wg: &sync.WaitGroup{}, log: DefaultLogger()} signal.Notify(death.sigChannel, signals...) death.wg.Add(1) go death.listenForSignal() return death } // SetTimeout Overrides the time death is willing to wait for a objects to be closed. func (d *Death) SetTimeout(t time.Duration) *Death { d.timeout = t return d } // SetLogger Overrides the default logger (seelog) func (d *Death) SetLogger(l Logger) *Death { d.log = l return d } // WaitForDeath wait for signal and then kill all items that need to die. If they fail to // die when instructed we return an error func (d *Death) WaitForDeath(closable ...io.Closer) (err error) { d.wg.Wait() d.log.Info("Shutdown started...") count := len(closable) d.log.Debug("Closing ", count, " objects") if count > 0 { return d.closeInMass(closable...) } return nil } // WaitForDeathWithFunc allows you to have a single function get called when it's time to // kill your application. func (d *Death) WaitForDeathWithFunc(f func()) { d.wg.Wait() d.log.Info("Shutdown started...") f() } // getPkgPath for an io closer. func getPkgPath(c io.Closer) (name string, pkgPath string) { t := reflect.TypeOf(c) if t.Kind() == reflect.Ptr { t = t.Elem() } return t.Name(), t.PkgPath() } // closeInMass Close all the objects at once and wait for them to finish with a channel. Return an // error if you fail to close all the objects func (d *Death) closeInMass(closable ...io.Closer) (err error) { count := len(closable) sentToClose := make(map[int]closer) //call close async doneClosers := make(chan closer, count) for i, c := range closable { name, pkgPath := getPkgPath(c) closer := closer{Index: i, C: c, Name: name, PKGPath: pkgPath} go d.closeObjects(closer, doneClosers) sentToClose[i] = closer } // wait on channel for notifications. timer := time.NewTimer(d.timeout) failedClosers := []closer{} for { select { case <-timer.C: s := "failed to close: " pkgs := []string{} for _, c := range sentToClose { pkgs = append(pkgs, fmt.Sprintf("%s/%s", c.PKGPath, c.Name)) d.log.Error("Failed to close: ", c.PKGPath, "/", c.Name) } return fmt.Errorf("%s", fmt.Sprintf("%s %s", s, strings.Join(pkgs, ", "))) case closer := <-doneClosers: delete(sentToClose, closer.Index) count-- if closer.Err != nil { failedClosers = append(failedClosers, closer) } d.log.Debug(count, " object(s) left") if count != 0 || len(sentToClose) != 0 { continue } if len(failedClosers) != 0 { errString := generateErrString(failedClosers) return fmt.Errorf("errors from closers: %s", errString) } return nil } } } // closeObjects and return a bool when finished on a channel. func (d *Death) closeObjects(closer closer, done chan<- closer) { err := closer.C.Close() if err != nil { d.log.Error(err) closer.Err = err } done <- closer } // FallOnSword manually initiates the death process. func (d *Death) FallOnSword() { select { case d.callChannel <- struct{}{}: default: } } // ListenForSignal Manage death of application by signal. func (d *Death) listenForSignal() { defer d.wg.Done() for { select { case <-d.sigChannel: return case <-d.callChannel: return } } } // generateErrString generates a string containing a list of tuples of pkgname to error message func generateErrString(failedClosers []closer) (errString string) { for i, fc := range failedClosers { if i == 0 { errString = fmt.Sprintf("%s/%s: %s", fc.PKGPath, fc.Name, fc.Err) continue } errString = fmt.Sprintf("%s, %s/%s: %s", errString, fc.PKGPath, fc.Name, fc.Err) } return errString } ================================================ FILE: death_unix_test.go ================================================ // +build linux bsd darwin package death import ( "fmt" "os" "syscall" "testing" "time" . "github.com/smartystreets/goconvey/convey" ) type Unhashable map[string]interface{} func (u Unhashable) Close() error { return nil } func TestDeath(t *testing.T) { Convey("Validate death handles unhashable types", t, func() { u := make(Unhashable) death := NewDeath(syscall.SIGTERM) syscall.Kill(os.Getpid(), syscall.SIGTERM) err := death.WaitForDeath(u) So(err, ShouldBeNil) }) Convey("Validate death happens cleanly", t, func() { death := NewDeath(syscall.SIGTERM) syscall.Kill(os.Getpid(), syscall.SIGTERM) err := death.WaitForDeath() So(err, ShouldBeNil) }) Convey("Validate death happens with other signals", t, func() { death := NewDeath(syscall.SIGHUP) closeMe := &CloseMe{} syscall.Kill(os.Getpid(), syscall.SIGHUP) err := death.WaitForDeath(closeMe) So(err, ShouldBeNil) So(closeMe.Closed, ShouldEqual, 1) }) Convey("Validate death happens with a manual call", t, func() { death := NewDeath(syscall.SIGHUP) closeMe := &CloseMe{} death.FallOnSword() err := death.WaitForDeath(closeMe) So(err, ShouldBeNil) So(closeMe.Closed, ShouldEqual, 1) }) Convey("Validate multiple sword falls do not block", t, func() { death := NewDeath(syscall.SIGHUP) closeMe := &CloseMe{} death.FallOnSword() death.FallOnSword() err := death.WaitForDeath(closeMe) So(err, ShouldBeNil) So(closeMe.Closed, ShouldEqual, 1) }) Convey("Validate multiple sword falls do not block even after we have exited waitForDeath", t, func() { death := NewDeath(syscall.SIGHUP) closeMe := &CloseMe{} death.FallOnSword() death.FallOnSword() err := death.WaitForDeath(closeMe) So(err, ShouldBeNil) death.FallOnSword() death.FallOnSword() So(closeMe.Closed, ShouldEqual, 1) }) Convey("Validate death gives up after timeout", t, func() { death := NewDeath(syscall.SIGHUP) death.SetTimeout(10 * time.Millisecond) neverClose := &neverClose{} syscall.Kill(os.Getpid(), syscall.SIGHUP) err := death.WaitForDeath(neverClose) So(err, ShouldNotBeNil) }) Convey("Validate death uses new logger", t, func() { death := NewDeath(syscall.SIGHUP) closeMe := &CloseMe{} logger := &MockLogger{} death.SetLogger(logger) syscall.Kill(os.Getpid(), syscall.SIGHUP) err := death.WaitForDeath(closeMe) So(err, ShouldBeNil) So(closeMe.Closed, ShouldEqual, 1) So(logger.Logs, ShouldNotBeEmpty) }) Convey("Close multiple things with one that fails the timer", t, func() { death := NewDeath(syscall.SIGHUP) death.SetTimeout(10 * time.Millisecond) neverClose := &neverClose{} closeMe := &CloseMe{} syscall.Kill(os.Getpid(), syscall.SIGHUP) err := death.WaitForDeath(neverClose, closeMe) So(err, ShouldNotBeNil) So(closeMe.Closed, ShouldEqual, 1) }) Convey("Close with anonymous function", t, func() { death := NewDeath(syscall.SIGHUP) death.SetTimeout(5 * time.Millisecond) closeMe := &CloseMe{} syscall.Kill(os.Getpid(), syscall.SIGHUP) death.WaitForDeathWithFunc(func() { closeMe.Close() So(true, ShouldBeTrue) }) So(closeMe.Closed, ShouldEqual, 1) }) Convey("Validate death errors when closer returns error", t, func() { death := NewDeath(syscall.SIGHUP) killMe := &KillMe{} death.FallOnSword() err := death.WaitForDeath(killMe) So(err, ShouldNotBeNil) }) } func TestGenerateErrString(t *testing.T) { Convey("Generate for multiple errors", t, func() { closers := []closer{ closer{ Err: fmt.Errorf("error 1"), Name: "foo", PKGPath: "my/pkg", }, closer{ Err: fmt.Errorf("error 2"), Name: "bar", PKGPath: "my/otherpkg", }, } expected := "my/pkg/foo: error 1, my/otherpkg/bar: error 2" actual := generateErrString(closers) So(actual, ShouldEqual, expected) }) Convey("Generate for single error", t, func() { closers := []closer{ closer{ Err: fmt.Errorf("error 1"), Name: "foo", PKGPath: "my/pkg", }, } expected := "my/pkg/foo: error 1" actual := generateErrString(closers) So(actual, ShouldEqual, expected) }) } type MockLogger struct { Logs []interface{} } func (l *MockLogger) Info(v ...interface{}) { for _, log := range v { l.Logs = append(l.Logs, log) } } func (l *MockLogger) Debug(v ...interface{}) { for _, log := range v { l.Logs = append(l.Logs, log) } } func (l *MockLogger) Error(v ...interface{}) { for _, log := range v { l.Logs = append(l.Logs, log) } } func (l *MockLogger) Warn(v ...interface{}) { for _, log := range v { l.Logs = append(l.Logs, log) } } type neverClose struct { } func (n *neverClose) Close() error { time.Sleep(2 * time.Minute) return nil } // CloseMe returns nil from close type CloseMe struct { Closed int } func (c *CloseMe) Close() error { c.Closed++ return nil } // KillMe returns an error from close type KillMe struct{} func (c *KillMe) Close() error { return fmt.Errorf("error from closer") } ================================================ FILE: death_windows_test.go ================================================ package death import ( "bytes" "io/ioutil" "os" "os/exec" "path/filepath" "syscall" "testing" "time" . "github.com/smartystreets/goconvey/convey" ) func TestDeath(t *testing.T) { Convey("Validate death happens cleanly on windows with ctrl-c event", t, func() { // create source file const source = ` package main import ( "syscall" "github.com/vrecan/death/v3" ) func main() { death := death.NewDeath(syscall.SIGINT) death.WaitForDeath() } ` tmp, err := ioutil.TempDir("", "TestCtrlBreak") if err != nil { t.Fatal("TempDir failed: ", err) } defer os.RemoveAll(tmp) // write ctrlbreak.go name := filepath.Join(tmp, "ctlbreak") src := name + ".go" f, err := os.Create(src) if err != nil { t.Fatalf("Failed to create %v: %v", src, err) } defer f.Close() f.Write([]byte(source)) // compile it exe := name + ".exe" defer os.Remove(exe) o, err := exec.Command("go", "build", "-o", exe, src).CombinedOutput() if err != nil { t.Fatalf("Failed to compile: %v\n%v", err, string(o)) } // run it cmd := exec.Command(exe) var b bytes.Buffer cmd.Stdout = &b cmd.Stderr = &b cmd.SysProcAttr = &syscall.SysProcAttr{ CreationFlags: syscall.CREATE_NEW_PROCESS_GROUP, } err = cmd.Start() if err != nil { t.Fatalf("Start failed: %v", err) } go func() { time.Sleep(1 * time.Second) sendCtrlBreak(t, cmd.Process.Pid) }() err = cmd.Wait() if err != nil { t.Fatalf("Program exited with error: %v\n%v", err, string(b.Bytes())) } }) } func sendCtrlBreak(t *testing.T, pid int) { d, e := syscall.LoadDLL("kernel32.dll") if e != nil { t.Fatalf("LoadDLL: %v\n", e) } p, e := d.FindProc("GenerateConsoleCtrlEvent") if e != nil { t.Fatalf("FindProc: %v\n", e) } r, _, e := p.Call(syscall.CTRL_BREAK_EVENT, uintptr(pid)) if r == 0 { t.Fatalf("GenerateConsoleCtrlEvent: %v\n", e) } } ================================================ FILE: deathlog.go ================================================ package death // Logger interface to log. type Logger interface { Error(v ...interface{}) Debug(v ...interface{}) Info(v ...interface{}) } type defaultLogger struct{} var logger = defaultLogger{} // DefaultLogger returns a logger that does nothing func DefaultLogger() Logger { return logger } func (d defaultLogger) Error(v ...interface{}) {} func (d defaultLogger) Debug(v ...interface{}) {} func (d defaultLogger) Info(v ...interface{}) {} ================================================ FILE: go.mod ================================================ module github.com/vrecan/death/v3 go 1.18 require github.com/smartystreets/goconvey v1.7.2 require ( github.com/gopherjs/gopherjs v0.0.0-20211219123610-ec9572f70e60 // indirect github.com/jtolds/gls v4.20.0+incompatible // indirect github.com/smartystreets/assertions v1.2.1 // indirect ) ================================================ FILE: go.sum ================================================ github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gopherjs/gopherjs v0.0.0-20211219123610-ec9572f70e60 h1:btMZK5oA0NpSAOOE7zRfq2UEuPC9apJ2UBackURyaTU= github.com/gopherjs/gopherjs v0.0.0-20211219123610-ec9572f70e60/go.mod h1:cz9oNYuRUWGdHmLF2IodMLkAhcPtXeULvcBNagUrxTI= github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo= github.com/smartystreets/assertions v1.2.1 h1:bKNHfEv7tSIjZ8JbKaFjzFINljxG4lzZvmHUnElzOIg= github.com/smartystreets/assertions v1.2.1/go.mod h1:wDmR7qL282YbGsPy6H/yAsesrxfxaaSlJazyFLYVFx8= github.com/smartystreets/goconvey v1.7.2 h1:9RBaZCeXEQ3UselpuwUQHltGVXvdwm6cv1hgR6gDIPg= github.com/smartystreets/goconvey v1.7.2/go.mod h1:Vw0tHAZW6lzCRk3xgdin6fKYcG+G3Pg9vgXWeJpQFMM= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= ================================================ FILE: pkgPath_test.go ================================================ package death import ( "testing" . "github.com/smartystreets/goconvey/convey" ) func TestGetPkgPath(t *testing.T) { Convey("Give pkgPath a ptr", t, func() { c := &Closer{} name, pkgPath := getPkgPath(c) So(name, ShouldEqual, "Closer") So(pkgPath, ShouldEqual, "github.com/vrecan/death/v3") }) Convey("Give pkgPath a interface", t, func() { var closable Closable closable = Closer{} name, pkgPath := getPkgPath(closable) So(name, ShouldEqual, "Closer") So(pkgPath, ShouldEqual, "github.com/vrecan/death/v3") }) Convey("Give pkgPath a copy", t, func() { c := Closer{} name, pkgPath := getPkgPath(c) So(name, ShouldEqual, "Closer") So(pkgPath, ShouldEqual, "github.com/vrecan/death/v3") }) } type Closable interface { Close() error } type Closer struct { } func (c Closer) Close() error { return nil }