Skip to content

Commit

Permalink
add os signal subscriber
Browse files Browse the repository at this point in the history
  • Loading branch information
rmasclef committed Aug 4, 2019
1 parent b31b964 commit 133a137
Show file tree
Hide file tree
Showing 5 changed files with 146 additions and 2 deletions.
11 changes: 9 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,9 @@
# os-signal-subscriber
Signal subscriber that allows you to attach a callback to an `os.Signal` notification
# signal
This repository provides helpers with signals

## Subscriber
Signal subscriber that allows you to attach a callback to an `os.Signal` notification.

Useful to react to any os.Signal.

It returns an `unsubscribe` function that can gracefully stop some http server and clean allocated object
8 changes: 8 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
module github.com/gol4ng/signal

go 1.12

require (
bou.ke/monkey v1.0.1
github.com/stretchr/testify v1.3.0
)
9 changes: 9 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
bou.ke/monkey v1.0.1 h1:zEMLInw9xvNakzUUPjfS4Ds6jYPqCFx3m7bRmG5NH2U=
bou.ke/monkey v1.0.1/go.mod h1:FgHuK96Rv2Nlf+0u1OOVDpCMdsWyOFmeeketDHE7LIg=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
46 changes: 46 additions & 0 deletions subscriber.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package signal

import (
"os"
"os/signal"
)

// This is a really simple signal subscriber
// Signal subscriber that allows you to attach a callback to an `os.Signal` notification.
// Usefull to react to any os.Signal.
// It returns an `unsubscribe` function that stops the goroutine and clean allocated object
//
// Example:
// unsubscriber := signal.subscribe(func(s os.Signal) {
// fmt.Println("process as been asked to be stopped")
// }, os.SIGSTOP)
//
// call "unsubscriber()" in order to detach your callback and clean memory
//
// if no 2nd arg is passed, the `callback` func will be called everytime an os.Signal is triggered
// signal.subscribe(func(s os.Signal) {fmt.Println("called for any signal")})
//
// /!\ CAUTION /!\
// If you call it with second arg to `nil`, no signal will be listened
// signal.subscribe(func(s os.Signal) {fmt.Println("NEVER BE CALLED")}, nil)
func Subscribe(callback func(os.Signal), signals ...os.Signal) func() {
signalChan := make(chan os.Signal, 1)
stopChan := make(chan struct{}, 1)
signal.Notify(signalChan, signals...)
go func() {
for {
select {
case <-stopChan:
signal.Stop(signalChan)
close(stopChan)
close(signalChan)
return
case sig := <-signalChan:
go callback(sig)
}
}
}()
return func() {
stopChan <- struct{}{}
}
}
74 changes: 74 additions & 0 deletions subscriber_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package signal_test

import (
"os"
"os/signal"
"testing"
"time"
"unsafe"

"bou.ke/monkey"

"github.com/stretchr/testify/assert"

sig "github.com/gol4ng/signal"
)

func TestSubscribe(t *testing.T) {
var signalChan chan<- os.Signal
var subscribedSignals = []os.Signal{os.Interrupt, os.Kill}
var realSignal = os.Interrupt
var callbackCalled = false

monkey.Patch(signal.Notify, func(c chan<- os.Signal, signals ...os.Signal) {
// get subscriber internal chan in order to simulate signal receive
signalChan = c
for _, s := range subscribedSignals {
assert.Contains(t, signals, s)
}
})
defer monkey.UnpatchAll()

sig.Subscribe(func(signal os.Signal) {
assert.Equal(t, realSignal, signal, "wrong signal ingested by subscriber.")
callbackCalled = true
}, subscribedSignals...)

// simulate signal receive
signalChan <- realSignal
// wait for goroutine callback func fulfilment (@see subscriber.go `go callback(subscribedSignals)`)
time.Sleep(1 * time.Millisecond)
assert.True(t, callbackCalled, "callback func should be called.")
}

func TestUnSubscribe(t *testing.T) {
var signalChan chan<- os.Signal
var subscribedSignal = os.Interrupt
var stopCalled = false
var callbackCalled = false

monkey.Patch(signal.Notify, func(c chan<- os.Signal, signals ...os.Signal) {
// get subscriber internal chan in order to simulate signal receive
signalChan = c
assert.Equal(t, subscribedSignal, signals[0])
})
monkey.Patch(signal.Stop, func(c chan<- os.Signal) {
assert.Equal(t, signalChan, c)
stopCalled = true
})
defer monkey.UnpatchAll()

unsubscribeFunc := sig.Subscribe(func(signal os.Signal) {
assert.Equal(t, signal, subscribedSignal, "wrong signal ingested by subscriber.")
callbackCalled = true
}, subscribedSignal)

unsubscribeFunc()
// wait for goroutine signal.Stop func call (@see subscriber.go `case <-stopChan:`)
time.Sleep(1 * time.Millisecond)
// `signalChan` is a bidirectional chan as it is equal to the `subscriber signalChan` value
_, ok := <-*(*chan os.Signal)(unsafe.Pointer(&signalChan))
assert.False(t, ok, "signal channel should be closed.")
assert.True(t, stopCalled, "`signal.Stop` should be called when `unsubscribeFunc` is called.")
assert.False(t, callbackCalled, "callback func should not be called as `unsubscribeFunc` is called.")
}

0 comments on commit 133a137

Please sign in to comment.