├── fake_runner ├── doc.go ├── test_runner.go └── fake_runner.go ├── fake_runner_v2 ├── doc.go ├── test_runner.go └── fake_runner.go ├── ifrit_suite_test.go ├── proxy ├── proxy_suite_test.go ├── proxy.go └── proxy_test.go ├── grouper ├── group_suite_test.go ├── sliding_buffer.go ├── sliding_buffer_test.go ├── members_test.go ├── doc.go ├── entrance_events.go ├── members.go ├── exit_events.go ├── client.go ├── dynamic_group_test.go ├── queue_ordered.go ├── ordered.go ├── parallel.go ├── dynamic_group.go ├── parallel_test.go ├── ordered_test.go └── queue_ordered_test.go ├── restart ├── restart_suite_test.go ├── strategies.go ├── restart.go └── restart_test.go ├── grpc_server ├── grpcserver_suite_test.go ├── server.go └── server_test.go ├── http_server ├── http_server_suite_test.go ├── unix_transport │ ├── unix_transport_suite_test.go │ ├── unix_transport.go │ └── unix_transport_test.go ├── test_certs │ ├── client.csr │ ├── server.csr │ ├── client.crt │ ├── server.crt │ ├── client.key │ ├── server.key │ ├── server-ca.crt │ └── server-ca.key ├── http_server.go └── http_server_test.go ├── .gitignore ├── go.mod ├── ginkgomon ├── helpers.go └── ginkgomon.go ├── ginkgomon_v2 ├── helpers.go └── ginkgomon.go ├── sigmon └── sigmon.go ├── LICENSE ├── doc.go ├── README.md ├── runner.go ├── process_test.go ├── test_helpers └── test_helpers.go └── process.go /fake_runner/doc.go: -------------------------------------------------------------------------------- 1 | // fake_runner contains test fixtures. 2 | package fake_runner 3 | -------------------------------------------------------------------------------- /fake_runner_v2/doc.go: -------------------------------------------------------------------------------- 1 | // fake_runner_v2 contains test fixtures. 2 | package fake_runner_v2 3 | -------------------------------------------------------------------------------- /ifrit_suite_test.go: -------------------------------------------------------------------------------- 1 | package ifrit_test 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo" 5 | . "github.com/onsi/gomega" 6 | 7 | "testing" 8 | ) 9 | 10 | func TestIfrit(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "Ifrit Suite") 13 | } 14 | -------------------------------------------------------------------------------- /proxy/proxy_suite_test.go: -------------------------------------------------------------------------------- 1 | package proxy_test 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo" 5 | . "github.com/onsi/gomega" 6 | 7 | "testing" 8 | ) 9 | 10 | func TestProxy(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "Proxy Suite") 13 | } 14 | -------------------------------------------------------------------------------- /grouper/group_suite_test.go: -------------------------------------------------------------------------------- 1 | package grouper_test 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo" 5 | . "github.com/onsi/gomega" 6 | 7 | "testing" 8 | ) 9 | 10 | func TestGroup(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "Grouper Suite") 13 | } 14 | -------------------------------------------------------------------------------- /restart/restart_suite_test.go: -------------------------------------------------------------------------------- 1 | package restart_test 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo" 5 | . "github.com/onsi/gomega" 6 | 7 | "testing" 8 | ) 9 | 10 | func TestRestart(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "Restart Suite") 13 | } 14 | -------------------------------------------------------------------------------- /grpc_server/grpcserver_suite_test.go: -------------------------------------------------------------------------------- 1 | package grpc_server_test 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo" 5 | . "github.com/onsi/gomega" 6 | 7 | "testing" 8 | ) 9 | 10 | func TestGrpcserver(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "Grpcserver Suite") 13 | } 14 | -------------------------------------------------------------------------------- /http_server/http_server_suite_test.go: -------------------------------------------------------------------------------- 1 | package http_server_test 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo" 5 | . "github.com/onsi/gomega" 6 | 7 | "testing" 8 | ) 9 | 10 | func TestHttpServer(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "HttpServer Suite") 13 | } 14 | -------------------------------------------------------------------------------- /http_server/unix_transport/unix_transport_suite_test.go: -------------------------------------------------------------------------------- 1 | package unix_transport 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestUnixTransport(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "UnixTransport Suite") 13 | } 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 4 | *.o 5 | *.a 6 | *.so 7 | 8 | # Folders 9 | _obj 10 | _test 11 | 12 | *.exe 13 | 14 | *.iml 15 | *.zpi 16 | *.zwi 17 | 18 | *.go-e 19 | 20 | # Test output 21 | *.test 22 | 23 | # Log files 24 | *.log 25 | 26 | # IDE 27 | .idea/ 28 | 29 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/tedsuo/ifrit 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d 7 | github.com/onsi/ginkgo v1.16.5 8 | github.com/onsi/ginkgo/v2 v2.9.4 9 | github.com/onsi/gomega v1.27.6 10 | github.com/tedsuo/ifrit v0.0.0-20230330192023-5cba443a66c4 11 | golang.org/x/net v0.10.0 12 | google.golang.org/grpc v1.55.0 13 | google.golang.org/grpc/examples v0.0.0-20230512210959-5dcfb37c0b43 14 | ) 15 | -------------------------------------------------------------------------------- /restart/strategies.go: -------------------------------------------------------------------------------- 1 | package restart 2 | 3 | import "github.com/tedsuo/ifrit" 4 | 5 | /* 6 | OnError is a restart strategy for Safely Restartable Runners. It will restart the 7 | Runner only if it exits with a matching error. 8 | */ 9 | func OnError(runner ifrit.Runner, err error, errors ...error) ifrit.Runner { 10 | errors = append(errors, err) 11 | return &Restarter{ 12 | Runner: runner, 13 | Load: func(runner ifrit.Runner, err error) ifrit.Runner { 14 | for _, restartableError := range errors { 15 | if err == restartableError { 16 | return runner 17 | } 18 | } 19 | return nil 20 | }, 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /proxy/proxy.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/tedsuo/ifrit" 7 | ) 8 | 9 | func New(proxySignals <-chan os.Signal, runner ifrit.Runner) ifrit.Runner { 10 | return ifrit.RunFunc(func(signals <-chan os.Signal, ready chan<- struct{}) error { 11 | process := ifrit.Background(runner) 12 | <-process.Ready() 13 | close(ready) 14 | go forwardSignals(proxySignals, process) 15 | go forwardSignals(signals, process) 16 | return <-process.Wait() 17 | }) 18 | } 19 | 20 | func forwardSignals(signals <-chan os.Signal, process ifrit.Process) { 21 | exit := process.Wait() 22 | for { 23 | select { 24 | case sig := <-signals: 25 | process.Signal(sig) 26 | case <-exit: 27 | return 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /grouper/sliding_buffer.go: -------------------------------------------------------------------------------- 1 | package grouper 2 | 3 | import "container/list" 4 | 5 | type slidingBuffer struct { 6 | buffer *list.List 7 | capacity int 8 | } 9 | 10 | func newSlidingBuffer(capacity int) slidingBuffer { 11 | return slidingBuffer{list.New(), capacity} 12 | } 13 | 14 | func (b slidingBuffer) Append(item interface{}) { 15 | if b.capacity == 0 { 16 | return 17 | } 18 | 19 | b.buffer.PushBack(item) 20 | if b.buffer.Len() > b.capacity { 21 | b.buffer.Remove(b.buffer.Front()) 22 | } 23 | } 24 | 25 | func (b slidingBuffer) Range(callback func(item interface{})) { 26 | elem := b.buffer.Front() 27 | for elem != nil { 28 | callback(elem.Value) 29 | elem = elem.Next() 30 | } 31 | } 32 | 33 | func (b slidingBuffer) Length() int { 34 | return b.buffer.Len() 35 | } 36 | -------------------------------------------------------------------------------- /grouper/sliding_buffer_test.go: -------------------------------------------------------------------------------- 1 | package grouper 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo" 5 | . "github.com/onsi/gomega" 6 | ) 7 | 8 | const capacity = 3 9 | 10 | var _ = Describe("Sliding Buffer", func() { 11 | var buffer slidingBuffer 12 | BeforeEach(func() { 13 | buffer = newSlidingBuffer(capacity) 14 | }) 15 | 16 | Context("when the number of appends exceeds capacity", func() { 17 | BeforeEach(func() { 18 | for i := 0; i < capacity*2; i++ { 19 | buffer.Append(i) 20 | } 21 | }) 22 | 23 | It("adds to the buffer, up to the capacity", func() { 24 | Ω(buffer.Length()).Should(Equal(capacity)) 25 | }) 26 | 27 | It("Range returns the most recently added items", func() { 28 | expectedIndex := capacity 29 | buffer.Range(func(item interface{}) { 30 | index := item.(int) 31 | Ω(index).Should(Equal(expectedIndex)) 32 | expectedIndex++ 33 | }) 34 | }) 35 | }) 36 | }) 37 | -------------------------------------------------------------------------------- /http_server/test_certs/client.csr: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE REQUEST----- 2 | MIICWDCCAUICAQAwFTETMBEGA1UEAxMKYmJzIGNsaWVudDCCASIwDQYJKoZIhvcN 3 | AQEBBQADggEPADCCAQoCggEBAK6PnzgiHSEij2b0nwc1C4RJTgDoq8rq5BpagYai 4 | a2DpzjygbI9HYGAGesuc2pj1FzySkJuCIhdICw4KZcdAHYoPEEu/HVcK0filjV9U 5 | taTqywi9ojeOYSQrAFDaSNbtnl62raPeYy99YXniyMQLP5Su2DYb5UZfPgTVWIlc 6 | BX0wc8I+v3hsfFLuySNHooJ7FdcQbg4fSBPIDovTmBP4/EAQAmWimn9qJ4X1WHbp 7 | bTAIllNILW+HsHh9I60oiq3FcxPC3L/0/pC451O9ODIx4TGruSL9tPRORGWRh87U 8 | Lk5ePOSUju70emCf3qdV2Vo/93HwWEOo0GtHni8plDQ0QocCAwEAAaAAMAsGCSqG 9 | SIb3DQEBCwOCAQEAY4NFK/TL/gH0SwrDsPq8IxGpO5pKO+Vk+v3as+qtPUIXIy0j 10 | aB/qvEB3nnJ2a2mk1yYvaXbqwUeokweXMalGz9sidxdfvEJOlGJnWPjenPWe5KgC 11 | FBFbUZPqJfRkScFV5z1AIXxrbtrehOH4hNvNLjghWDK14aMPZu4ADHAtf3PeEa/S 12 | LjxDnUhDJQdh0RZn0hwj4qwMN9ll1/V1uT6hiOoZJlHXEx/dTNvWWQ9cSMZBDCY8 13 | eRyLoUc6Rim96h5sLlvX7X+YywDIFPtkM6LCG4mLYpTJ2Kv7guwde80OSRNtbCA+ 14 | 3vFKCJ++AFjB66GttS/wm8eDBZ84mpIItkKFfg== 15 | -----END CERTIFICATE REQUEST----- 16 | -------------------------------------------------------------------------------- /ginkgomon/helpers.go: -------------------------------------------------------------------------------- 1 | package ginkgomon 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/onsi/ginkgo" 8 | . "github.com/onsi/gomega" 9 | "github.com/tedsuo/ifrit" 10 | ) 11 | 12 | func Invoke(runner ifrit.Runner) ifrit.Process { 13 | process := ifrit.Background(runner) 14 | 15 | select { 16 | case <-process.Ready(): 17 | case err := <-process.Wait(): 18 | ginkgo.Fail(fmt.Sprintf("process failed to start: %s", err), 1) 19 | } 20 | 21 | return process 22 | } 23 | 24 | func Interrupt(process ifrit.Process, intervals ...interface{}) { 25 | if process != nil { 26 | process.Signal(os.Interrupt) 27 | EventuallyWithOffset(1, process.Wait(), intervals...).Should(Receive(), "interrupted ginkgomon process failed to exit in time") 28 | } 29 | } 30 | 31 | func Kill(process ifrit.Process, intervals ...interface{}) { 32 | if process != nil { 33 | process.Signal(os.Kill) 34 | EventuallyWithOffset(1, process.Wait(), intervals...).Should(Receive(), "killed ginkgomon process failed to exit in time") 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /ginkgomon_v2/helpers.go: -------------------------------------------------------------------------------- 1 | package ginkgomon_v2 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/onsi/ginkgo/v2" 8 | . "github.com/onsi/gomega" 9 | "github.com/tedsuo/ifrit" 10 | ) 11 | 12 | func Invoke(runner ifrit.Runner) ifrit.Process { 13 | process := ifrit.Background(runner) 14 | 15 | select { 16 | case <-process.Ready(): 17 | case err := <-process.Wait(): 18 | ginkgo.Fail(fmt.Sprintf("process failed to start: %s", err), 1) 19 | } 20 | 21 | return process 22 | } 23 | 24 | func Interrupt(process ifrit.Process, intervals ...interface{}) { 25 | if process != nil { 26 | process.Signal(os.Interrupt) 27 | EventuallyWithOffset(1, process.Wait(), intervals...).Should(Receive(), "interrupted ginkgomon process failed to exit in time") 28 | } 29 | } 30 | 31 | func Kill(process ifrit.Process, intervals ...interface{}) { 32 | if process != nil { 33 | process.Signal(os.Kill) 34 | EventuallyWithOffset(1, process.Wait(), intervals...).Should(Receive(), "killed ginkgomon process failed to exit in time") 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /http_server/test_certs/server.csr: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE REQUEST----- 2 | MIICnDCCAYYCAQAwIjEgMB4GA1UEAxMXYmJzLnNlcnZpY2UuY2YuaW50ZXJuYWww 3 | ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCoXlo12b8ATXkjw7mD/6hr 4 | 3IWIrk7rAi56zdPBqLgbaWUrnOjHSylt3Pu6jkFJQvG4gKJ/ZL0CS1NyuU78Lf10 5 | t2UgTZ0hUJmJBl1rxrig3OROcXYwAhBix1WFulinvcl+PPT9du7hbfYdj9Yi9HU0 6 | GCUXWharI0wLO/Gg4Y+SZZ0G2T2w4TLAiqodiw3JyJaB0ixbnduFQcZ4T7O8TAAq 7 | zkxP1r/Z6gve102vIzAas6mdj/y7uf5/fXcxYuzpkdGjkULazj41SHU3jhRdmWxT 8 | 3mQqbkOYc7/AXrVj64j6p012Ge+aMb+oxnWNCAIjZOmNPJT6PgSizkVbZDlrtYqp 9 | AgMBAAGgNzA1BgkqhkiG9w0BCQ4xKDAmMCQGA1UdEQQdMBuCGSouYmJzLnNlcnZp 10 | Y2UuY2YuaW50ZXJuYWwwCwYJKoZIhvcNAQELA4IBAQA3wv4E8LUPA/Qbypl6nEMe 11 | QWQ4Aze5+FTOqphb05IF/s73G8xAdvrAEQxuwUMOQB7eiuGuMROhFlbj8rXa2r5z 12 | k1IAv5IP1S97tUZW3V+CIN4alQIMNs94Ph2Dcnk4J2JeKbIZPe4dYXsDvi33QO5R 13 | Y1uV5DAi4LXXljZ+eTJTQD1bUg4GgDfNs99hwlksuwXBWM7EbAD1ojh2hHbybYoI 14 | vNyhfZ1FSL3vHac/nfC0LBjmBCEPcWRvzfzfDLzmerte0owNy/ZTKxZyUbqXI1OT 15 | 3eCwOujVLYeWtfA/omuW1hKVGBmDv7ao6xIRlYlKRLIlVHWA8I2NmyafQwVVT/8i 16 | -----END CERTIFICATE REQUEST----- 17 | -------------------------------------------------------------------------------- /fake_runner/test_runner.go: -------------------------------------------------------------------------------- 1 | package fake_runner 2 | 3 | import ( 4 | "os" 5 | 6 | . "github.com/onsi/ginkgo" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | type TestRunner struct { 11 | *FakeRunner 12 | exitChan chan error 13 | } 14 | 15 | func NewTestRunner() *TestRunner { 16 | exitChan := make(chan error) 17 | runner := &FakeRunner{ 18 | RunStub: func(signals <-chan os.Signal, ready chan<- struct{}) error { 19 | return <-exitChan 20 | }, 21 | } 22 | 23 | return &TestRunner{runner, exitChan} 24 | } 25 | 26 | func (r *TestRunner) WaitForCall() <-chan os.Signal { 27 | Eventually(r.RunCallCount).Should(Equal(1)) 28 | signal, _ := r.RunArgsForCall(0) 29 | return signal 30 | } 31 | 32 | func (r *TestRunner) TriggerReady() { 33 | Eventually(r.RunCallCount).Should(Equal(1)) 34 | _, ready := r.RunArgsForCall(0) 35 | close(ready) 36 | } 37 | 38 | func (r *TestRunner) TriggerExit(err error) { 39 | defer GinkgoRecover() 40 | 41 | r.exitChan <- err 42 | } 43 | 44 | func (r *TestRunner) EnsureExit() { 45 | select { 46 | case r.exitChan <- nil: 47 | default: 48 | 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /sigmon/sigmon.go: -------------------------------------------------------------------------------- 1 | package sigmon 2 | 3 | import ( 4 | "os" 5 | "os/signal" 6 | "syscall" 7 | 8 | "github.com/tedsuo/ifrit" 9 | ) 10 | 11 | const SIGNAL_BUFFER_SIZE = 1024 12 | 13 | type sigmon struct { 14 | Signals []os.Signal 15 | Runner ifrit.Runner 16 | } 17 | 18 | func New(runner ifrit.Runner, signals ...os.Signal) ifrit.Runner { 19 | signals = append(signals, syscall.SIGINT, syscall.SIGTERM) 20 | return &sigmon{ 21 | Signals: signals, 22 | Runner: runner, 23 | } 24 | } 25 | 26 | func (s sigmon) Run(signals <-chan os.Signal, ready chan<- struct{}) error { 27 | osSignals := make(chan os.Signal, SIGNAL_BUFFER_SIZE) 28 | signal.Notify(osSignals, s.Signals...) 29 | 30 | process := ifrit.Background(s.Runner) 31 | pReady := process.Ready() 32 | pWait := process.Wait() 33 | 34 | for { 35 | select { 36 | case sig := <-signals: 37 | process.Signal(sig) 38 | case sig := <-osSignals: 39 | process.Signal(sig) 40 | case <-pReady: 41 | close(ready) 42 | pReady = nil 43 | case err := <-pWait: 44 | signal.Stop(osSignals) 45 | return err 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /fake_runner_v2/test_runner.go: -------------------------------------------------------------------------------- 1 | package fake_runner_v2 2 | 3 | import ( 4 | "os" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | type TestRunner struct { 11 | *FakeRunner 12 | exitChan chan error 13 | } 14 | 15 | func NewTestRunner() *TestRunner { 16 | exitChan := make(chan error) 17 | runner := &FakeRunner{ 18 | RunStub: func(signals <-chan os.Signal, ready chan<- struct{}) error { 19 | return <-exitChan 20 | }, 21 | } 22 | 23 | return &TestRunner{runner, exitChan} 24 | } 25 | 26 | func (r *TestRunner) WaitForCall() <-chan os.Signal { 27 | Eventually(r.RunCallCount).Should(Equal(1)) 28 | signal, _ := r.RunArgsForCall(0) 29 | return signal 30 | } 31 | 32 | func (r *TestRunner) TriggerReady() { 33 | Eventually(r.RunCallCount).Should(Equal(1)) 34 | _, ready := r.RunArgsForCall(0) 35 | close(ready) 36 | } 37 | 38 | func (r *TestRunner) TriggerExit(err error) { 39 | defer GinkgoRecover() 40 | 41 | r.exitChan <- err 42 | } 43 | 44 | func (r *TestRunner) EnsureExit() { 45 | select { 46 | case r.exitChan <- nil: 47 | default: 48 | 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /proxy/proxy_test.go: -------------------------------------------------------------------------------- 1 | package proxy_test 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/tedsuo/ifrit" 7 | "github.com/tedsuo/ifrit/fake_runner" 8 | "github.com/tedsuo/ifrit/proxy" 9 | 10 | . "github.com/onsi/ginkgo" 11 | . "github.com/onsi/gomega" 12 | ) 13 | 14 | var _ = Describe("Proxy", func() { 15 | var testRunner *fake_runner.TestRunner 16 | var process ifrit.Process 17 | var proxySignals chan os.Signal 18 | var receivedSignals <-chan os.Signal 19 | 20 | BeforeEach(func() { 21 | proxySignals = make(chan os.Signal, 1) 22 | testRunner = fake_runner.NewTestRunner() 23 | process = ifrit.Background(proxy.New(proxySignals, testRunner)) 24 | receivedSignals = testRunner.WaitForCall() 25 | testRunner.TriggerReady() 26 | }) 27 | 28 | It("sends the proxied signals to the embedded runner", func() { 29 | proxySignals <- os.Interrupt 30 | Eventually(receivedSignals).Should(Receive(Equal(os.Interrupt))) 31 | }) 32 | 33 | It("sends the process signals to the embedded runner", func() { 34 | process.Signal(os.Interrupt) 35 | Eventually(receivedSignals).Should(Receive(Equal(os.Interrupt))) 36 | }) 37 | }) 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Theodore Young 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | A process model for go. 3 | 4 | Ifrit is a small set of interfaces for composing single-purpose units of work 5 | into larger programs. Users divide their program into single purpose units of 6 | work, each of which implements the `Runner` interface Each `Runner` can be 7 | invoked to create a `Process` which can be monitored and signaled to stop. 8 | 9 | The name Ifrit comes from a type of daemon in arabic folklore. It's a play on 10 | the unix term 'daemon' to indicate a process that is managed by the init system. 11 | 12 | Ifrit ships with a standard library which contains packages for common 13 | processes - http servers, integration test helpers - alongside packages which 14 | model process supervision and orchestration. These packages can be combined to 15 | form complex servers which start and shutdown cleanly. 16 | 17 | The advantage of small, single-responsibility processes is that they are simple, 18 | and thus can be made reliable. Ifrit's interfaces are designed to be free 19 | of race conditions and edge cases, allowing larger orcestrated process to also 20 | be made reliable. The overall effect is less code and more reliability as your 21 | system grows with grace. 22 | */ 23 | package ifrit 24 | 25 | 26 | -------------------------------------------------------------------------------- /grouper/members_test.go: -------------------------------------------------------------------------------- 1 | package grouper_test 2 | 3 | import ( 4 | "github.com/tedsuo/ifrit/grouper" 5 | 6 | . "github.com/onsi/ginkgo" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | var _ = Describe("Members", func() { 11 | Describe("Validate", func() { 12 | type duplicateNameExample struct { 13 | memberNames []string 14 | expectedError error 15 | } 16 | 17 | var testInput []duplicateNameExample 18 | 19 | BeforeEach(func() { 20 | testInput = []duplicateNameExample{ 21 | {[]string{"foo", "foo"}, grouper.ErrDuplicateNames{[]string{"foo"}}}, 22 | {[]string{"foo", "bar", "foo", "bar", "none"}, grouper.ErrDuplicateNames{[]string{"foo", "bar"}}}, 23 | {[]string{"foo", "bar"}, nil}, 24 | {[]string{"f", "foo", "fooo"}, nil}, 25 | } 26 | }) 27 | 28 | It("returns any found duplicate names", func() { 29 | for _, example := range testInput { 30 | members := grouper.Members{} 31 | for _, name := range example.memberNames { 32 | members = append(members, grouper.Member{Name: name, Runner: nil}) 33 | } 34 | err := members.Validate() 35 | if err == nil { 36 | Ω(example.expectedError).Should(BeNil()) 37 | } else { 38 | Ω(err).Should(Equal(example.expectedError)) 39 | } 40 | 41 | } 42 | }) 43 | }) 44 | }) 45 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ifrit - a process model for go. 2 | 3 | Ifrit is a small set of interfaces for composing single-purpose units of work 4 | into larger programs. Users divide their program into single purpose units of 5 | work, each of which implements the `Runner` interface Each `Runner` can be 6 | invoked to create a `Process` which can be monitored and signaled to stop. 7 | 8 | The name Ifrit comes from a type of daemon in arabic folklore. It's a play on 9 | the unix term 'daemon' to indicate a process that is managed by the init system. 10 | 11 | Ifrit ships with a standard library which contains packages for common 12 | processes - http servers, integration test helpers - alongside packages which 13 | model process supervision and orchestration. These packages can be combined to 14 | form complex servers which start and shutdown cleanly. 15 | 16 | The advantage of small, single-responsibility processes is that they are simple, 17 | and thus can be made reliable. Ifrit's interfaces are designed to be free 18 | of race conditions and edge cases, allowing larger orcestrated process to also 19 | be made reliable. The overall effect is less code and more reliability as your 20 | system grows with grace. 21 | 22 | The full documentation is written in godoc, and can be found at: 23 | 24 | http://godoc.org/github.com/tedsuo/ifrit 25 | -------------------------------------------------------------------------------- /runner.go: -------------------------------------------------------------------------------- 1 | package ifrit 2 | 3 | import "os" 4 | 5 | /* 6 | A Runner defines the contents of a Process. A Runner implementation performs an 7 | aribtrary unit of work, while waiting for a shutdown signal. The unit of work 8 | should avoid any orchestration. Instead, it should be broken down into simpler 9 | units of work in seperate Runners, which are then orcestrated by the ifrit 10 | standard library. 11 | 12 | An implementation of Runner has the following responibilities: 13 | 14 | - setup within a finite amount of time. 15 | - close the ready channel when setup is complete. 16 | - once ready, perform the unit of work, which may be infinite. 17 | - respond to shutdown signals by exiting within a finite amount of time. 18 | - return nil if shutdown is successful. 19 | - return an error if an exception has prevented a clean shutdown. 20 | 21 | By default, Runners are not considered restartable; Run will only be called once. 22 | See the ifrit/restart package for details on restartable Runners. 23 | */ 24 | type Runner interface { 25 | Run(signals <-chan os.Signal, ready chan<- struct{}) error 26 | } 27 | 28 | /* 29 | The RunFunc type is an adapter to allow the use of ordinary functions as Runners. 30 | If f is a function that matches the Run method signature, RunFunc(f) is a Runner 31 | object that calls f. 32 | */ 33 | type RunFunc func(signals <-chan os.Signal, ready chan<- struct{}) error 34 | 35 | func (r RunFunc) Run(signals <-chan os.Signal, ready chan<- struct{}) error { 36 | return r(signals, ready) 37 | } 38 | -------------------------------------------------------------------------------- /fake_runner/fake_runner.go: -------------------------------------------------------------------------------- 1 | // This file was generated by counterfeiter 2 | package fake_runner 3 | 4 | import ( 5 | "os" 6 | "sync" 7 | 8 | "github.com/tedsuo/ifrit" 9 | ) 10 | 11 | type FakeRunner struct { 12 | RunStub func(signals <-chan os.Signal, ready chan<- struct{}) error 13 | runMutex sync.RWMutex 14 | runArgsForCall []struct { 15 | signals <-chan os.Signal 16 | ready chan<- struct{} 17 | } 18 | runReturns struct { 19 | result1 error 20 | } 21 | } 22 | 23 | func (fake *FakeRunner) Run(signals <-chan os.Signal, ready chan<- struct{}) error { 24 | fake.runMutex.Lock() 25 | fake.runArgsForCall = append(fake.runArgsForCall, struct { 26 | signals <-chan os.Signal 27 | ready chan<- struct{} 28 | }{signals, ready}) 29 | fake.runMutex.Unlock() 30 | if fake.RunStub != nil { 31 | return fake.RunStub(signals, ready) 32 | } else { 33 | return fake.runReturns.result1 34 | } 35 | } 36 | 37 | func (fake *FakeRunner) RunCallCount() int { 38 | fake.runMutex.RLock() 39 | defer fake.runMutex.RUnlock() 40 | return len(fake.runArgsForCall) 41 | } 42 | 43 | func (fake *FakeRunner) RunArgsForCall(i int) (<-chan os.Signal, chan<- struct{}) { 44 | fake.runMutex.RLock() 45 | defer fake.runMutex.RUnlock() 46 | return fake.runArgsForCall[i].signals, fake.runArgsForCall[i].ready 47 | } 48 | 49 | func (fake *FakeRunner) RunReturns(result1 error) { 50 | fake.RunStub = nil 51 | fake.runReturns = struct { 52 | result1 error 53 | }{result1} 54 | } 55 | 56 | var _ ifrit.Runner = new(FakeRunner) 57 | -------------------------------------------------------------------------------- /fake_runner_v2/fake_runner.go: -------------------------------------------------------------------------------- 1 | // This file was generated by counterfeiter 2 | package fake_runner_v2 3 | 4 | import ( 5 | "os" 6 | "sync" 7 | 8 | "github.com/tedsuo/ifrit" 9 | ) 10 | 11 | type FakeRunner struct { 12 | RunStub func(signals <-chan os.Signal, ready chan<- struct{}) error 13 | runMutex sync.RWMutex 14 | runArgsForCall []struct { 15 | signals <-chan os.Signal 16 | ready chan<- struct{} 17 | } 18 | runReturns struct { 19 | result1 error 20 | } 21 | } 22 | 23 | func (fake *FakeRunner) Run(signals <-chan os.Signal, ready chan<- struct{}) error { 24 | fake.runMutex.Lock() 25 | fake.runArgsForCall = append(fake.runArgsForCall, struct { 26 | signals <-chan os.Signal 27 | ready chan<- struct{} 28 | }{signals, ready}) 29 | fake.runMutex.Unlock() 30 | if fake.RunStub != nil { 31 | return fake.RunStub(signals, ready) 32 | } else { 33 | return fake.runReturns.result1 34 | } 35 | } 36 | 37 | func (fake *FakeRunner) RunCallCount() int { 38 | fake.runMutex.RLock() 39 | defer fake.runMutex.RUnlock() 40 | return len(fake.runArgsForCall) 41 | } 42 | 43 | func (fake *FakeRunner) RunArgsForCall(i int) (<-chan os.Signal, chan<- struct{}) { 44 | fake.runMutex.RLock() 45 | defer fake.runMutex.RUnlock() 46 | return fake.runArgsForCall[i].signals, fake.runArgsForCall[i].ready 47 | } 48 | 49 | func (fake *FakeRunner) RunReturns(result1 error) { 50 | fake.RunStub = nil 51 | fake.runReturns = struct { 52 | result1 error 53 | }{result1} 54 | } 55 | 56 | var _ ifrit.Runner = new(FakeRunner) 57 | -------------------------------------------------------------------------------- /grouper/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Grouper implements process orcestration. Runners are organized into groups, 3 | which are then organized into an execution tree. If you have modeled your subsystems 4 | as ifrit runners, startup and shutdown of your entire application can now 5 | be controlled. 6 | 7 | Grouper provides three strategies for system startup: two static group 8 | strategies, and one DynamicGroup. Each static group strategy takes a 9 | list of members, and starts the members in the following manner: 10 | 11 | - Parallel: all processes are started simultaneously. 12 | - Ordered: the next process is started when the previous is ready. 13 | 14 | The DynamicGroup allows up to N processes to be run concurrently. The dynamic 15 | group runs indefinitely until it is closed or signaled. The DynamicGroup provides 16 | a DynamicClient to allow interacting with the group. A dynamic group has the 17 | following properties: 18 | 19 | - A dynamic group allows Members to be inserted until it is closed. 20 | - A dynamic group can be manually closed via it's client. 21 | - A dynamic group is automatically closed once it is signaled. 22 | - Once a dynamic group is closed, it acts like a static group. 23 | 24 | Groups can optionally be configured with a termination signal, and all groups 25 | have the same signaling and shutdown properties: 26 | 27 | - The group propogates all received signals to all running members. 28 | - If a member exits before being signaled, the group propogates the 29 | termination signal. A nil termination signal is not propogated. 30 | */ 31 | package grouper 32 | -------------------------------------------------------------------------------- /http_server/test_certs/client.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIEHDCCAgagAwIBAgIQT5VItrhV52kmcc+uqHDoszALBgkqhkiG9w0BAQswEDEO 3 | MAwGA1UEAxMFYmJzQ0EwHhcNMTUwOTA5MjM1MDQ1WhcNMTcwOTA5MjM1MDQ2WjAV 4 | MRMwEQYDVQQDEwpiYnMgY2xpZW50MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB 5 | CgKCAQEAro+fOCIdISKPZvSfBzULhElOAOiryurkGlqBhqJrYOnOPKBsj0dgYAZ6 6 | y5zamPUXPJKQm4IiF0gLDgplx0Adig8QS78dVwrR+KWNX1S1pOrLCL2iN45hJCsA 7 | UNpI1u2eXrato95jL31heeLIxAs/lK7YNhvlRl8+BNVYiVwFfTBzwj6/eGx8Uu7J 8 | I0eignsV1xBuDh9IE8gOi9OYE/j8QBACZaKaf2onhfVYdultMAiWU0gtb4eweH0j 9 | rSiKrcVzE8Lcv/T+kLjnU704MjHhMau5Iv209E5EZZGHztQuTl485JSO7vR6YJ/e 10 | p1XZWj/3cfBYQ6jQa0eeLymUNDRChwIDAQABo3EwbzAOBgNVHQ8BAf8EBAMCALgw 11 | HQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMB0GA1UdDgQWBBRylMDDtbiT 12 | SCny3HdaQhnDXE5tNzAfBgNVHSMEGDAWgBRRqQ9Ok27bkWDl/PIGqi/yrztwuTAL 13 | BgkqhkiG9w0BAQsDggIBAHNdiIuLqYPIvmdD+zBm6N7GJDalagv+2xBgAUKpaws+ 14 | jOZXXZyT3Dw6LL7eoIP97RNS/1snn6OPtiu4xaB76COUshDakpi9SKqArzbCNX2L 15 | oI1RNm8R1Gn6JOdRemTLP4xPv20XMWQj4h9TyWI0PPFnxXirTeBLUBA+FbFrmJ6Z 16 | wbnF1SrrhT1RW8pJODxirrobeBHubKAucRJZsXL0AzqbW29rrP3wKpjWvmfSt/ab 17 | JPB2CP2GA78WZrf0tKMJyZIA34fF1Ta818LC5z1c5cRcdfdvnDIMop7aaZX2QkCi 18 | kVEAqqcfL4Vv6V/C4OoXkQQTRT+Xdh2eEIuwmJ5mMHfbRPF8g6zDZr8Zi8L63zxJ 19 | bexbFlAiqbAoEh2o9alcgClswJ7DmEV7xFGMlFN0lvx9xy/IvwboD6MqtXD5P97J 20 | PZQF9GR6ASpMiwHuMpMFUAJsfu35Sfmiq+rSb/V4u/3Aj9nmBERAaCCiqjnhn/Gt 21 | UIxG7BxMDmCT1xe6T4sIOpxyJQ6IFkkUXphBRpVgAXuv9oFWlCmy5xG1CtbTdley 22 | aJa5CYIG0xzGmcFvSqtv4BI2rdANv/9G4eadAAIUjMuro20DUteS3Yz/4UE1WAhO 23 | pa4VLaFLJV5+TKRK2KPWQPP2OuNM5S3nE3LZT3KrdJ0egjEgbvsN3viIu7l552xv 24 | -----END CERTIFICATE----- 25 | -------------------------------------------------------------------------------- /http_server/test_certs/server.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIEUjCCAjygAwIBAgIRAOZTfzMzr3IYAPD4Lse2JVswCwYJKoZIhvcNAQELMBAx 3 | DjAMBgNVBAMTBWJic0NBMB4XDTE1MDkwOTIzNTA0M1oXDTE3MDkwOTIzNTA0NFow 4 | IjEgMB4GA1UEAxMXYmJzLnNlcnZpY2UuY2YuaW50ZXJuYWwwggEiMA0GCSqGSIb3 5 | DQEBAQUAA4IBDwAwggEKAoIBAQCoXlo12b8ATXkjw7mD/6hr3IWIrk7rAi56zdPB 6 | qLgbaWUrnOjHSylt3Pu6jkFJQvG4gKJ/ZL0CS1NyuU78Lf10t2UgTZ0hUJmJBl1r 7 | xrig3OROcXYwAhBix1WFulinvcl+PPT9du7hbfYdj9Yi9HU0GCUXWharI0wLO/Gg 8 | 4Y+SZZ0G2T2w4TLAiqodiw3JyJaB0ixbnduFQcZ4T7O8TAAqzkxP1r/Z6gve102v 9 | IzAas6mdj/y7uf5/fXcxYuzpkdGjkULazj41SHU3jhRdmWxT3mQqbkOYc7/AXrVj 10 | 64j6p012Ge+aMb+oxnWNCAIjZOmNPJT6PgSizkVbZDlrtYqpAgMBAAGjgZgwgZUw 11 | DgYDVR0PAQH/BAQDAgC4MB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAd 12 | BgNVHQ4EFgQUZSRxFr5AiFbQLvSj6N0zdkhMSd4wHwYDVR0jBBgwFoAUUakPTpNu 13 | 25Fg5fzyBqov8q87cLkwJAYDVR0RBB0wG4IZKi5iYnMuc2VydmljZS5jZi5pbnRl 14 | cm5hbDALBgkqhkiG9w0BAQsDggIBAFrljxE2Xn94sppdmo2wN3w40hr5aoJTb73u 15 | kjNsWq09jaIcGaNhLu2pMVxe+M79N6RUaCmzjbTIAKbL6kw44ZN8SqscYhf5UA81 16 | y+qrX9zTzhya4XrQX7ZltcuDE6xpp3+PRS7J0kFKfYDrKJG4He+jANqnk4u2NkaM 17 | wW1foDu/HEpNWH6TN3sFrHOOuSOhKoAAPNxZr1ihDWWOuibiEEsmqW7IJ441NohI 18 | 2hoAYHLK8gCp5DuK/Zn/+RKgulq02jveqfM4NH7fehVhmzMn9kzLaoiHVQ6n4bJJ 19 | loXmwzRLnxp514qdTGkuz8R4Pac5GIYaXVcznPBlmFBSkxzdPIo/MnG49gElm4/0 20 | B/3KE+9C0omB0mJ4Q5HYvCZevTVfrxUj9rhjxFUcsCoZl5oKQxIscdvuTwdXmlAg 21 | nMKdZPM3mJl3Khtu1nY83uQBP/vp20Ji25V7O/zIpgf7xbgD1PXoZAjz7QkCcztE 22 | XZX87DAU03kUh4ID76d2P+j+Jx0F1B8aWq3Ltq93KPuOf18zojISTizZnk8SN89V 23 | eLI3ODVUKzHonejZ2elLCwtKJzI6afXrYSZ5M0QEIpKT9uH/EIZ7TOQYW3FDa1GN 24 | d5huxunlOT6HPHCbTghs7LXRdodfDyAweFhuz5HVgkXvEov3K6+aJWOHa/MgSRnr 25 | C1iBl34a 26 | -----END CERTIFICATE----- 27 | -------------------------------------------------------------------------------- /restart/restart.go: -------------------------------------------------------------------------------- 1 | /* 2 | The restart package implements common restart strategies for ifrit processes. 3 | 4 | The API is still experimental and subject to change. 5 | */ 6 | package restart 7 | 8 | import ( 9 | "errors" 10 | "os" 11 | 12 | "github.com/tedsuo/ifrit" 13 | ) 14 | 15 | // ErrNoLoadCallback is returned by Restarter if it is Invoked without a Load function. 16 | var ErrNoLoadCallback = errors.New("ErrNoLoadCallback") 17 | 18 | /* 19 | Restarter takes an inital runner and a Load function. When the inital Runner 20 | exits, the load function is called. If the Load function retuns a Runner, the 21 | Restarter will invoke the Runner. This continues until the Load function returns 22 | nil, or the Restarter is signaled to stop. The Restarter returns the error of 23 | the final Runner it invoked. 24 | */ 25 | type Restarter struct { 26 | Runner ifrit.Runner 27 | Load func(runner ifrit.Runner, err error) ifrit.Runner 28 | } 29 | 30 | func (r Restarter) Run(signals <-chan os.Signal, ready chan<- struct{}) error { 31 | if r.Load == nil { 32 | return ErrNoLoadCallback 33 | } 34 | 35 | process := ifrit.Background(r.Runner) 36 | processReady := process.Ready() 37 | exit := process.Wait() 38 | signaled := false 39 | 40 | for { 41 | select { 42 | case signal := <-signals: 43 | process.Signal(signal) 44 | signaled = true 45 | 46 | case <-processReady: 47 | close(ready) 48 | processReady = nil 49 | 50 | case err := <-exit: 51 | if signaled { 52 | return err 53 | } 54 | 55 | r.Runner = r.Load(r.Runner, err) 56 | if r.Runner == nil { 57 | return err 58 | } 59 | process = ifrit.Background(r.Runner) 60 | exit = process.Wait() 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /http_server/test_certs/client.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEowIBAAKCAQEAro+fOCIdISKPZvSfBzULhElOAOiryurkGlqBhqJrYOnOPKBs 3 | j0dgYAZ6y5zamPUXPJKQm4IiF0gLDgplx0Adig8QS78dVwrR+KWNX1S1pOrLCL2i 4 | N45hJCsAUNpI1u2eXrato95jL31heeLIxAs/lK7YNhvlRl8+BNVYiVwFfTBzwj6/ 5 | eGx8Uu7JI0eignsV1xBuDh9IE8gOi9OYE/j8QBACZaKaf2onhfVYdultMAiWU0gt 6 | b4eweH0jrSiKrcVzE8Lcv/T+kLjnU704MjHhMau5Iv209E5EZZGHztQuTl485JSO 7 | 7vR6YJ/ep1XZWj/3cfBYQ6jQa0eeLymUNDRChwIDAQABAoIBAB3xkRRl9a07anH0 8 | wFrSJJmaoYDSaLW0OVCz+cgIkHbdZH1N35FsYwHV2raWv4DBeizvz1J9ri9kMlFE 9 | Q1U8kFSgZOE5dWT6/C206F1UAJy7kfx72xnAmLVFkxZLe3cy55nDqGGVwlnhHhl0 10 | Z5AJheyRWZFek6PQrqjRmBBn8qEYp8ee2WafLomcr23ruc2PBGX9BCxHzptbVoJ3 11 | HAZl4TXLEASWkiXXhcVVHQZs8QkHfUmzUkjdXwxQWDivgywJZx3rceSO9dyCaDQ4 12 | RXMD5+RCakLecKOiUfsqL2e7MDJWpKhCbA8wTzzRAvJTANFh9yuBkHm6F9ooUC2H 13 | jiFfp5ECgYEAwCJ2cTUtlhUMdxISiFlPij+9YUJ1V41x8UxuhFKInSdqU/0PcqJh 14 | M461Duy5uMb8WI3fKOOzG5JfkRqDIeQ80B7hr8WlKh3i2AMhz+HsLx2Rs0bxKn9O 15 | uAbOpQxL8F1qq+WnttKe1GZqpFwBay8dAhy7QK02tGBrsN2+PCKnhnsCgYEA6JW/ 16 | oRKjNI74HLjTpTiuh4PMsWfg4+2hWusC63X5vVVOD1VAP/JPsNxcPioXH57Snx20 17 | h3rCaWl6DygI7NP+4wGSzLLhVPBZEn0XnLRv5M9qQjiT0PTod26PoujpbvTwrKpq 18 | 2xRyd7mfHypbglXiUcxVHBEsKhBpYKNEXdTYXGUCgYBc5w0QVl9Rh6H8XS+64Dx2 19 | o7VltuXYTNuAiq2Rq/rEyo8+R0nV6zBG5sUjj1GKSCUyiH8UXW14coFlP0WS+LJz 20 | C8ui21WulL7gJjuOMjaq3YhbAH2SR/Z/Q0NeSSDa+8Cdl7FN1G/aUh9Uk+xXsHM9 21 | Vzkv39FozIql/cKDf1ozywKBgG1hh4aTwkdEeXDgh0BrgMDgfhJsnPn0Vm7wmSXt 22 | DK87AAP7/sRUC6BMceEWYFuRkNId4TnZxLZYUXvxQwlFxdgydDxqX8hXZDxqsgET 23 | Zo//76QHAZVCqFUKnOhriuSQsuMxHiG74v1lQW0huXl5NH9tjhUuCkwZ/cTh45QR 24 | NlN1AoGBALeQndNGghgVU1ZisNKPx9wAnlvCtLg8jeldnc5L4NEKCVQM5OUrF+FF 25 | y34OCzS8tKbkQEjgnYdg8Z40+f6hrweJOdfRf+a/ldkUW4MRo9w+ipsecrnpLBWs 26 | cHnMRXg7iq58Ro6DV9feM7yabXuM4ALtleny9OfHl5YYBGsy2FDC 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /http_server/test_certs/server.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEogIBAAKCAQEAqF5aNdm/AE15I8O5g/+oa9yFiK5O6wIues3Twai4G2llK5zo 3 | x0spbdz7uo5BSULxuICif2S9AktTcrlO/C39dLdlIE2dIVCZiQZda8a4oNzkTnF2 4 | MAIQYsdVhbpYp73Jfjz0/Xbu4W32HY/WIvR1NBglF1oWqyNMCzvxoOGPkmWdBtk9 5 | sOEywIqqHYsNyciWgdIsW53bhUHGeE+zvEwAKs5MT9a/2eoL3tdNryMwGrOpnY/8 6 | u7n+f313MWLs6ZHRo5FC2s4+NUh1N44UXZlsU95kKm5DmHO/wF61Y+uI+qdNdhnv 7 | mjG/qMZ1jQgCI2TpjTyU+j4Eos5FW2Q5a7WKqQIDAQABAoIBAG0H7NNClvXTNcd7 8 | T/+y55Yx0Cea+XAmkYLlF2Qppk/aCLIdgoDMo5+Jo1pEFPMkbvRUGOb2jY+WyAtb 9 | BVM5VRDoUoyAtmiEkWiyvny0y4sggJhr0WV1cLcG7tMSwaaeuIUPZHdfNYK2etLZ 10 | +onYVK3PlPVyWYup5+y+fNL6PsdGCRKaDIW9X7MD+Zki4uhOFgES7EqqyH88HgM7 11 | OGlCJXAinlOCFPY6vZzKLbsw6D+Bng4TQBsrfPhdsKJBO9NAbuwGy8F8qxgVDssv 12 | 1+vpdBTqzEHd8+roow27sGoAuPszQDp2Hznh95bXBNhBxiU0zhJb+tEATiJHqtd+ 13 | jlCmxoECgYEAxS8O/Ub69TF5L1+6B28dJ4m3tU8MrTzEUd6HvfTu8t6pZbn4yV5/ 14 | xjrinfsjH3CKlsy8adCVjlfGtNURoPrjEBTNaialFTXT+DtfpnZ4KvfanqiApY+9 15 | xzf4UauGBt1HaIdJXCcY9VMszcHkhlVcWhUACxsf/BYLAvtMXGIbMfUCgYEA2pb2 16 | ggspI6SRhiW70N3rxPeRJLCk8mKFSiOYBQAxrWX2ATv2hcDbmmGnH4drai2AxR/1 17 | glPu+B5JsZ2xlGLoEk/aPR+gCUPI/eyzBN/Vu/C/Ypojud9slc9khMoyPfP+TJnx 18 | FzB+8LF9HRvtl0TSo2eTXmko8c42LM9Q0bO5YWUCgYBqsioENtI4tsqCLeQ1fZRi 19 | /owfWWTcoJMCUc1VpiFd3cn/t3+9RpsRIm/ZWDkLHBSBwMr63tjuKuTkmJ5vYxJW 20 | c0srcznEnlnSah45rsUbSv5K95aU/5CLKef+GTfuovGux/WUHbvNk3Ic7BvB7JKK 21 | U+6wE79c4niW5m/NVXCtuQKBgDgwAeFfmQ2OadG/tU11HfudX3O87ElZxcVO7O6s 22 | JBjcUqXykeXDsy811s2l85hxZd5F3sfHZ2/j6TF8xX7NBbZfTEvV6z82a13KECI6 23 | nygWNDvWP0SyB6lijAYOK2f9Zequz2gUkSyxkuV+nk355OMX5quoAFxXk1llWPLu 24 | bcJ5AoGAY6nYWcxUgv0CGwDa2CskFHXuexlJ4TxdUDsY2oFpDpMTBGgq3rjmLDcU 25 | cojUuBCdKbqq/GIjG9dJx5rSu2/0Qv6AD8d70h/MFKMVi43ozAlNUKajanOanZIp 26 | kAsg32veovSuP8sY2qRkPkpM1rRboPjZG47Njn/O7zsVYjcift0= 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /grouper/entrance_events.go: -------------------------------------------------------------------------------- 1 | package grouper 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/tedsuo/ifrit" 7 | ) 8 | 9 | /* 10 | An EntranceEvent occurs every time an invoked member becomes ready. 11 | */ 12 | type EntranceEvent struct { 13 | Member Member 14 | Process ifrit.Process 15 | } 16 | 17 | type entranceEventChannel chan EntranceEvent 18 | 19 | func newEntranceEventChannel(bufferSize int) entranceEventChannel { 20 | return make(entranceEventChannel, bufferSize) 21 | } 22 | 23 | type entranceEventBroadcaster struct { 24 | channels []entranceEventChannel 25 | buffer slidingBuffer 26 | bufferSize int 27 | lock *sync.Mutex 28 | } 29 | 30 | func newEntranceEventBroadcaster(bufferSize int) *entranceEventBroadcaster { 31 | return &entranceEventBroadcaster{ 32 | channels: make([]entranceEventChannel, 0), 33 | buffer: newSlidingBuffer(bufferSize), 34 | bufferSize: bufferSize, 35 | lock: new(sync.Mutex), 36 | } 37 | } 38 | 39 | func (b *entranceEventBroadcaster) Attach() entranceEventChannel { 40 | b.lock.Lock() 41 | defer b.lock.Unlock() 42 | 43 | channel := newEntranceEventChannel(b.bufferSize) 44 | b.buffer.Range(func(event interface{}) { 45 | channel <- event.(EntranceEvent) 46 | }) 47 | if b.channels != nil { 48 | b.channels = append(b.channels, channel) 49 | } else { 50 | close(channel) 51 | } 52 | return channel 53 | } 54 | 55 | func (b *entranceEventBroadcaster) Broadcast(entrance EntranceEvent) { 56 | b.lock.Lock() 57 | defer b.lock.Unlock() 58 | 59 | b.buffer.Append(entrance) 60 | 61 | for _, entranceChan := range b.channels { 62 | entranceChan <- entrance 63 | } 64 | } 65 | 66 | func (b *entranceEventBroadcaster) Close() { 67 | b.lock.Lock() 68 | defer b.lock.Unlock() 69 | 70 | for _, channel := range b.channels { 71 | close(channel) 72 | } 73 | b.channels = nil 74 | } 75 | -------------------------------------------------------------------------------- /grouper/members.go: -------------------------------------------------------------------------------- 1 | package grouper 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/tedsuo/ifrit" 7 | ) 8 | 9 | /* 10 | A Member associates a unique name with a Runner. 11 | */ 12 | type Member struct { 13 | Name string 14 | ifrit.Runner 15 | } 16 | 17 | /* 18 | Members are treated as an ordered list. Member names must be unique. 19 | */ 20 | type Members []Member 21 | 22 | /* 23 | Validate checks that all member names in the list are unique. It returns an 24 | error of type ErrDuplicateNames if duplicates are detected. 25 | */ 26 | func (m Members) Validate() error { 27 | foundNames := map[string]struct{}{} 28 | foundToken := struct{}{} 29 | duplicateNames := []string{} 30 | 31 | for _, member := range m { 32 | _, present := foundNames[member.Name] 33 | if present { 34 | duplicateNames = append(duplicateNames, member.Name) 35 | continue 36 | } 37 | foundNames[member.Name] = foundToken 38 | } 39 | 40 | if len(duplicateNames) > 0 { 41 | return ErrDuplicateNames{duplicateNames} 42 | } 43 | return nil 44 | } 45 | 46 | /* 47 | ErrDuplicateNames is returned to indicate two or more members with the same name 48 | were detected. Because more than one duplicate name may be detected in a single 49 | pass, ErrDuplicateNames contains a list of all duplicate names found. 50 | */ 51 | type ErrDuplicateNames struct { 52 | DuplicateNames []string 53 | } 54 | 55 | func (e ErrDuplicateNames) Error() string { 56 | var msg string 57 | 58 | switch len(e.DuplicateNames) { 59 | case 0: 60 | msg = fmt.Sprintln("ErrDuplicateNames initialized without any duplicate names.") 61 | case 1: 62 | msg = fmt.Sprintln("Duplicate member name:", e.DuplicateNames[0]) 63 | default: 64 | msg = fmt.Sprintln("Duplicate member names:") 65 | for _, name := range e.DuplicateNames { 66 | msg = fmt.Sprintln(name) 67 | } 68 | } 69 | 70 | return msg 71 | } 72 | -------------------------------------------------------------------------------- /process_test.go: -------------------------------------------------------------------------------- 1 | package ifrit_test 2 | 3 | import ( 4 | "os" 5 | 6 | . "github.com/onsi/ginkgo" 7 | . "github.com/onsi/gomega" 8 | "github.com/tedsuo/ifrit" 9 | "github.com/tedsuo/ifrit/test_helpers" 10 | ) 11 | 12 | var _ = Describe("Process", func() { 13 | Context("when a process is envoked", func() { 14 | var pinger test_helpers.PingChan 15 | var pingProc ifrit.Process 16 | var errChan chan error 17 | 18 | BeforeEach(func() { 19 | pinger = make(test_helpers.PingChan) 20 | pingProc = ifrit.Invoke(pinger) 21 | errChan = make(chan error) 22 | }) 23 | 24 | Describe("Wait()", func() { 25 | BeforeEach(func() { 26 | go func() { 27 | errChan <- <-pingProc.Wait() 28 | }() 29 | go func() { 30 | errChan <- <-pingProc.Wait() 31 | }() 32 | }) 33 | 34 | Context("when the process exits", func() { 35 | BeforeEach(func() { 36 | go func() { 37 | <-pinger 38 | }() 39 | }) 40 | 41 | It("returns the run result upon completion", func() { 42 | err1 := <-errChan 43 | err2 := <-errChan 44 | Ω(err1).Should(Equal(test_helpers.PingerExitedFromPing)) 45 | Ω(err2).Should(Equal(test_helpers.PingerExitedFromPing)) 46 | }) 47 | }) 48 | }) 49 | 50 | Describe("Signal()", func() { 51 | BeforeEach(func() { 52 | pingProc.Signal(os.Kill) 53 | }) 54 | 55 | It("sends the signal to the runner", func() { 56 | err := <-pingProc.Wait() 57 | Ω(err).Should(Equal(test_helpers.PingerExitedFromSignal)) 58 | }) 59 | }) 60 | }) 61 | 62 | Context("when a process exits without closing ready", func() { 63 | var proc ifrit.Process 64 | 65 | BeforeEach(func(done Done) { 66 | proc = ifrit.Invoke(test_helpers.NoReadyRunner) 67 | close(done) 68 | }) 69 | 70 | It("waits normally", func() { 71 | Ω(<-proc.Wait()).Should(Equal(test_helpers.NoReadyExitedNormally)) 72 | }) 73 | }) 74 | }) 75 | -------------------------------------------------------------------------------- /http_server/test_certs/server-ca.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIE/TCCAuegAwIBAgIBATALBgkqhkiG9w0BAQswEDEOMAwGA1UEAxMFYmJzQ0Ew 3 | HhcNMTUwOTA5MjM1MDM3WhcNMjUwOTA5MjM1MDQzWjAQMQ4wDAYDVQQDEwViYnND 4 | QTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAM5n1NdBaqnOu1hlQOI3 5 | jV3xAB5tHN26LIC7CUlnyjMuP0Cd2U1bNw0vo0LvN8XlxVD/d+CRuSqvy8HpBDwl 6 | ctN1R0Ri63r19HabRXIatv28nvcdLGQOe7OXU9K1AfjAODVk3G27Jo1R4IC++b7K 7 | kQdhPzeyhTeJ9B5c9TU8ApiaWLvsEFV/4MqQ3pqTuS/bEpJd1wf3fc+6yrsKMSbM 8 | +PUIbEvOPlKyYldtJlNHI1odHQU35ur0YiGy6MtMI73FrC3Kegp+hirfX3jJmQfi 9 | AcKJd4aVMIsDkBFzJvdLX088C4oS5Wq3zbMdpaie26QyLtrBjB2hKBD77HHCLlvT 10 | yemi3nIFJ4TF0QRrcfQ1qpKgynko0pDyyeF07rH9BgAJ56yU566XX57wKBLhE93p 11 | R77jvCv7UxQyyAYSfEuYnM2h/pPEADKcDv+zK2p9QIo+T9Nj0uw5XayHv8A9A6Ch 12 | /3QEoT8ld7tPbzmC8ewxvTRz5lQ6VT+T4CL6Vpueuisch2pSAy/kaPlVv2F6CB37 13 | FEv3o1R39VzQ8Qump9yM75IFUoXn1TGI8rAf81k4MO/od8Na5vnpGX5BEg2Lot3N 14 | /mj8J6HByArdVvQ5YO1wM08dFIuMUwv7E0A0n8cUM7ZwZ2RxtkBqNwAq2720M8rV 15 | ZOUfWIoy2aH1eVHXhL+rskfLAgMBAAGjZjBkMA4GA1UdDwEB/wQEAwIABjASBgNV 16 | HRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBRRqQ9Ok27bkWDl/PIGqi/yrztwuTAf 17 | BgNVHSMEGDAWgBRRqQ9Ok27bkWDl/PIGqi/yrztwuTALBgkqhkiG9w0BAQsDggIB 18 | AChFANR2ujh6kqU0/+qwFu8wMTan2APsUmaZgfIEt6UfffqK54jBRkdwftrrxkJO 19 | oVklAuVz/3PU4ks3l0hab88cuFA2ZnlDXIwF5SM3i7hm7kTF/xm/TmKbwWkRawKi 20 | TnQI4CRVnQyBju+wLNElzSI958zNDsvyVPZEHNwvKFgB+qcSurdAwv9I6VysIbXP 21 | 0U7GyeJhW9t6jOji6VI/2hUcKxtGEQKEks2iCR+dT747bzdM6Vz50yUUTYvWRo1L 22 | A5W/rTQVNVMxiZCGzdU8tD1KMsBqY0rAcWEBycmq7Go1AJ7erR8rbeOh9JQHNm78 23 | wf2NeM30+tOp89hkdPAxd/zQR2rB29M/V3Pg0+IUZjZHR6CXeJEp9KOtxJKc48sG 24 | MOzojzO/9zWIndFl7GF+PWVKfeVni5kesNVseQr4nUH0O3l35U+iWh3i5PU7GnUO 25 | k0brBYGhziVtbFTMX3GG5jK9ITXwU2GPrFdSUNRyDcuKQi4jhioY8qpvlC83oE1d 26 | ZrCXvJszJ3YN1dPvzZMTdHEn82PT7QcyHufttk49SGvKlnTfQfQzZ1oq6t6X40Ef 27 | GlAoeqFMTsrt8YLZlzoqWmD52qfENG9svtXDGXNLtSYJ1tIK7ONhKrsRdpRO/aT8 28 | eVpknLHuPirElcpnWFJ0Q2zDKxHwjel2j3TpXxEkf7jM 29 | -----END CERTIFICATE----- 30 | -------------------------------------------------------------------------------- /test_helpers/test_helpers.go: -------------------------------------------------------------------------------- 1 | package test_helpers 2 | 3 | import ( 4 | "errors" 5 | "os" 6 | "sync" 7 | 8 | "github.com/tedsuo/ifrit" 9 | ) 10 | 11 | // PingChan stops when you send it a single Ping 12 | type PingChan chan Ping 13 | 14 | type Ping struct{} 15 | 16 | var PingerExitedFromPing = errors.New("pinger exited with a ping") 17 | var PingerExitedFromSignal = errors.New("pinger exited with a signal") 18 | 19 | func (p PingChan) Load(err error) (ifrit.Runner, bool) { 20 | return p, true 21 | } 22 | 23 | func (p PingChan) Run(sigChan <-chan os.Signal, ready chan<- struct{}) error { 24 | close(ready) 25 | select { 26 | case <-sigChan: 27 | return PingerExitedFromSignal 28 | case p <- Ping{}: 29 | return PingerExitedFromPing 30 | } 31 | } 32 | 33 | // NoReadyRunner exits without closing the ready chan 34 | var NoReadyRunner = ifrit.RunFunc(func(sigChan <-chan os.Signal, ready chan<- struct{}) error { 35 | return NoReadyExitedNormally 36 | }) 37 | 38 | var NoReadyExitedNormally = errors.New("no ready exited normally") 39 | 40 | // SignalRecoder records all signals received, and exits on a set of signals. 41 | type SignalRecoder struct { 42 | sync.RWMutex 43 | signals []os.Signal 44 | exitSignals map[os.Signal]struct{} 45 | } 46 | 47 | func NewSignalRecorder(exitSignals ...os.Signal) *SignalRecoder { 48 | exitSignals = append(exitSignals, os.Kill, os.Interrupt) 49 | 50 | signalSet := map[os.Signal]struct{}{} 51 | for _, signal := range exitSignals { 52 | signalSet[signal] = struct{}{} 53 | } 54 | 55 | return &SignalRecoder{ 56 | exitSignals: signalSet, 57 | } 58 | } 59 | 60 | func (r *SignalRecoder) Load(err error) (ifrit.Runner, bool) { 61 | return r, true 62 | } 63 | 64 | func (r *SignalRecoder) Run(sigChan <-chan os.Signal, ready chan<- struct{}) error { 65 | close(ready) 66 | 67 | for { 68 | signal := <-sigChan 69 | 70 | r.Lock() 71 | r.signals = append(r.signals, signal) 72 | r.Unlock() 73 | 74 | _, ok := r.exitSignals[signal] 75 | if ok { 76 | return nil 77 | } 78 | } 79 | } 80 | 81 | func (r *SignalRecoder) ReceivedSignals() []os.Signal { 82 | defer r.RUnlock() 83 | r.RLock() 84 | return r.signals 85 | } 86 | -------------------------------------------------------------------------------- /grouper/exit_events.go: -------------------------------------------------------------------------------- 1 | package grouper 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | ) 7 | 8 | /* 9 | An ExitEvent occurs every time an invoked member exits. 10 | */ 11 | type ExitEvent struct { 12 | Member Member 13 | Err error 14 | } 15 | 16 | type exitEventChannel chan ExitEvent 17 | 18 | func newExitEventChannel(bufferSize int) exitEventChannel { 19 | return make(exitEventChannel, bufferSize) 20 | } 21 | 22 | type exitEventBroadcaster struct { 23 | channels []exitEventChannel 24 | buffer slidingBuffer 25 | bufferSize int 26 | lock *sync.Mutex 27 | } 28 | 29 | func newExitEventBroadcaster(bufferSize int) *exitEventBroadcaster { 30 | return &exitEventBroadcaster{ 31 | channels: make([]exitEventChannel, 0), 32 | buffer: newSlidingBuffer(bufferSize), 33 | bufferSize: bufferSize, 34 | lock: new(sync.Mutex), 35 | } 36 | } 37 | 38 | func (b *exitEventBroadcaster) Attach() exitEventChannel { 39 | b.lock.Lock() 40 | defer b.lock.Unlock() 41 | 42 | channel := newExitEventChannel(b.bufferSize) 43 | b.buffer.Range(func(event interface{}) { 44 | channel <- event.(ExitEvent) 45 | }) 46 | if b.channels != nil { 47 | b.channels = append(b.channels, channel) 48 | } else { 49 | close(channel) 50 | } 51 | return channel 52 | } 53 | 54 | func (b *exitEventBroadcaster) Broadcast(exit ExitEvent) { 55 | b.lock.Lock() 56 | defer b.lock.Unlock() 57 | b.buffer.Append(exit) 58 | for _, exitChan := range b.channels { 59 | exitChan <- exit 60 | } 61 | } 62 | 63 | func (b *exitEventBroadcaster) Close() { 64 | b.lock.Lock() 65 | defer b.lock.Unlock() 66 | 67 | for _, channel := range b.channels { 68 | close(channel) 69 | } 70 | b.channels = nil 71 | } 72 | 73 | type ErrorTrace []ExitEvent 74 | 75 | func (trace ErrorTrace) Error() string { 76 | msg := "Exit trace for group:\n" 77 | 78 | for _, exit := range trace { 79 | if exit.Err == nil { 80 | msg += fmt.Sprintf("%s exited with nil\n", exit.Member.Name) 81 | } else { 82 | msg += fmt.Sprintf("%s exited with error: %s\n", exit.Member.Name, exit.Err.Error()) 83 | } 84 | } 85 | 86 | return msg 87 | } 88 | 89 | func (trace ErrorTrace) ErrorOrNil() error { 90 | for _, exit := range trace { 91 | if exit.Err != nil { 92 | return trace 93 | } 94 | } 95 | 96 | return nil 97 | } 98 | -------------------------------------------------------------------------------- /process.go: -------------------------------------------------------------------------------- 1 | package ifrit 2 | 3 | import "os" 4 | 5 | /* 6 | A Process represents a Runner that has been started. It is safe to call any 7 | method on a Process even after the Process has exited. 8 | */ 9 | type Process interface { 10 | // Ready returns a channel which will close once the runner is active 11 | Ready() <-chan struct{} 12 | 13 | // Wait returns a channel that will emit a single error once the Process exits. 14 | Wait() <-chan error 15 | 16 | // Signal sends a shutdown signal to the Process. It does not block. 17 | Signal(os.Signal) 18 | } 19 | 20 | /* 21 | Invoke executes a Runner and returns a Process once the Runner is ready. Waiting 22 | for ready allows program initializtion to be scripted in a procedural manner. 23 | To orcestrate the startup and monitoring of multiple Processes, please refer to 24 | the ifrit/grouper package. 25 | */ 26 | func Invoke(r Runner) Process { 27 | p := Background(r) 28 | 29 | select { 30 | case <-p.Ready(): 31 | case <-p.Wait(): 32 | } 33 | 34 | return p 35 | } 36 | 37 | /* 38 | Envoke is deprecated in favor of Invoke, on account of it not being a real word. 39 | */ 40 | func Envoke(r Runner) Process { 41 | return Invoke(r) 42 | } 43 | 44 | /* 45 | Background executes a Runner and returns a Process immediately, without waiting. 46 | */ 47 | func Background(r Runner) Process { 48 | p := newProcess(r) 49 | go p.run() 50 | return p 51 | } 52 | 53 | type process struct { 54 | runner Runner 55 | signals chan os.Signal 56 | ready chan struct{} 57 | exited chan struct{} 58 | exitStatus error 59 | } 60 | 61 | func newProcess(runner Runner) *process { 62 | return &process{ 63 | runner: runner, 64 | signals: make(chan os.Signal), 65 | ready: make(chan struct{}), 66 | exited: make(chan struct{}), 67 | } 68 | } 69 | 70 | func (p *process) run() { 71 | p.exitStatus = p.runner.Run(p.signals, p.ready) 72 | close(p.exited) 73 | } 74 | 75 | func (p *process) Ready() <-chan struct{} { 76 | return p.ready 77 | } 78 | 79 | func (p *process) Wait() <-chan error { 80 | exitChan := make(chan error, 1) 81 | 82 | go func() { 83 | <-p.exited 84 | exitChan <- p.exitStatus 85 | }() 86 | 87 | return exitChan 88 | } 89 | 90 | func (p *process) Signal(signal os.Signal) { 91 | go func() { 92 | select { 93 | case p.signals <- signal: 94 | case <-p.exited: 95 | } 96 | }() 97 | } 98 | -------------------------------------------------------------------------------- /restart/restart_test.go: -------------------------------------------------------------------------------- 1 | package restart_test 2 | 3 | import ( 4 | "os" 5 | 6 | . "github.com/onsi/ginkgo" 7 | . "github.com/onsi/gomega" 8 | "github.com/tedsuo/ifrit" 9 | "github.com/tedsuo/ifrit/fake_runner" 10 | "github.com/tedsuo/ifrit/restart" 11 | ) 12 | 13 | var _ = Describe("Restart", func() { 14 | var testRunner *fake_runner.TestRunner 15 | var restarter restart.Restarter 16 | var process ifrit.Process 17 | 18 | BeforeEach(func() { 19 | testRunner = fake_runner.NewTestRunner() 20 | restarter = restart.Restarter{ 21 | Runner: testRunner, 22 | Load: func(runner ifrit.Runner, err error) ifrit.Runner { 23 | return nil 24 | }, 25 | } 26 | }) 27 | 28 | JustBeforeEach(func() { 29 | process = ifrit.Background(restarter) 30 | }) 31 | 32 | AfterEach(func() { 33 | process.Signal(os.Kill) 34 | testRunner.EnsureExit() 35 | Eventually(process.Wait()).Should(Receive()) 36 | }) 37 | 38 | Describe("Process Behavior", func() { 39 | 40 | It("waits for the internal runner to be ready", func() { 41 | Consistently(process.Ready()).ShouldNot(BeClosed()) 42 | testRunner.TriggerReady() 43 | Eventually(process.Ready()).Should(BeClosed()) 44 | }) 45 | }) 46 | 47 | Describe("Load", func() { 48 | 49 | Context("when load returns a runner", func() { 50 | var loadedRunner *fake_runner.TestRunner 51 | var loadedRunners chan *fake_runner.TestRunner 52 | 53 | BeforeEach(func() { 54 | loadedRunners = make(chan *fake_runner.TestRunner, 1) 55 | restarter.Load = func(runner ifrit.Runner, err error) ifrit.Runner { 56 | select { 57 | case runner := <-loadedRunners: 58 | return runner 59 | default: 60 | return nil 61 | } 62 | } 63 | loadedRunner = fake_runner.NewTestRunner() 64 | loadedRunners <- loadedRunner 65 | }) 66 | 67 | AfterEach(func() { 68 | loadedRunner.EnsureExit() 69 | }) 70 | 71 | It("executes the returned Runner", func() { 72 | testRunner.TriggerExit(nil) 73 | loadedRunner.TriggerExit(nil) 74 | }) 75 | }) 76 | 77 | Context("when load returns nil", func() { 78 | BeforeEach(func() { 79 | restarter.Load = func(runner ifrit.Runner, err error) ifrit.Runner { 80 | return nil 81 | } 82 | }) 83 | 84 | It("exits after running the initial Runner", func() { 85 | testRunner.TriggerExit(nil) 86 | Eventually(process.Wait()).Should(Receive(BeNil())) 87 | }) 88 | }) 89 | 90 | Context("when the load callback is nil", func() { 91 | BeforeEach(func() { 92 | restarter.Load = nil 93 | }) 94 | 95 | It("exits with NoLoadCallback error", func() { 96 | Eventually(process.Wait()).Should(Receive(Equal(restart.ErrNoLoadCallback))) 97 | }) 98 | }) 99 | }) 100 | }) 101 | -------------------------------------------------------------------------------- /http_server/http_server.go: -------------------------------------------------------------------------------- 1 | package http_server 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "net" 7 | "net/http" 8 | "os" 9 | "time" 10 | 11 | "github.com/tedsuo/ifrit" 12 | ) 13 | 14 | const ( 15 | TCP = "tcp" 16 | UNIX = "unix" 17 | ) 18 | 19 | type httpServer struct { 20 | protocol string 21 | address string 22 | handler http.Handler 23 | 24 | tlsConfig *tls.Config 25 | } 26 | 27 | func newServerWithListener(protocol, address string, handler http.Handler, tlsConfig *tls.Config) ifrit.Runner { 28 | return &httpServer{ 29 | address: address, 30 | handler: handler, 31 | tlsConfig: tlsConfig, 32 | protocol: protocol, 33 | } 34 | } 35 | 36 | func NewUnixServer(address string, handler http.Handler) ifrit.Runner { 37 | return newServerWithListener(UNIX, address, handler, nil) 38 | } 39 | 40 | func New(address string, handler http.Handler) ifrit.Runner { 41 | return newServerWithListener(TCP, address, handler, nil) 42 | } 43 | 44 | func NewUnixTLSServer(address string, handler http.Handler, tlsConfig *tls.Config) ifrit.Runner { 45 | return newServerWithListener(UNIX, address, handler, tlsConfig) 46 | } 47 | 48 | func NewTLSServer(address string, handler http.Handler, tlsConfig *tls.Config) ifrit.Runner { 49 | return newServerWithListener(TCP, address, handler, tlsConfig) 50 | } 51 | 52 | func (s *httpServer) Run(signals <-chan os.Signal, ready chan<- struct{}) error { 53 | server := http.Server{ 54 | Handler: s.handler, 55 | TLSConfig: s.tlsConfig, 56 | } 57 | 58 | listener, err := s.getListener(server.TLSConfig) 59 | if err != nil { 60 | return err 61 | } 62 | 63 | serverErrChan := make(chan error, 1) 64 | go func() { 65 | serverErrChan <- server.Serve(listener) 66 | }() 67 | 68 | close(ready) 69 | 70 | for { 71 | select { 72 | case err = <-serverErrChan: 73 | return err 74 | 75 | case <-signals: 76 | listener.Close() 77 | 78 | ctx, _ := context.WithTimeout(context.Background(), 1*time.Minute) 79 | server.Shutdown(ctx) 80 | 81 | return nil 82 | } 83 | } 84 | } 85 | 86 | func (s *httpServer) getListener(tlsConfig *tls.Config) (net.Listener, error) { 87 | listener, err := net.Listen(s.protocol, s.address) 88 | if err != nil { 89 | return nil, err 90 | } 91 | if tlsConfig == nil { 92 | return listener, nil 93 | } 94 | switch s.protocol { 95 | case TCP: 96 | listener = tls.NewListener(tcpKeepAliveListener{listener.(*net.TCPListener)}, tlsConfig) 97 | default: 98 | listener = tls.NewListener(listener, tlsConfig) 99 | } 100 | 101 | return listener, nil 102 | } 103 | 104 | type tcpKeepAliveListener struct { 105 | *net.TCPListener 106 | } 107 | 108 | func (ln tcpKeepAliveListener) Accept() (c net.Conn, err error) { 109 | tc, err := ln.AcceptTCP() 110 | if err != nil { 111 | return 112 | } 113 | tc.SetKeepAlive(true) 114 | tc.SetKeepAlivePeriod(3 * time.Minute) 115 | return tc, nil 116 | } 117 | -------------------------------------------------------------------------------- /http_server/unix_transport/unix_transport.go: -------------------------------------------------------------------------------- 1 | package unix_transport 2 | 3 | import ( 4 | "crypto/tls" 5 | "errors" 6 | "fmt" 7 | "net" 8 | "net/http" 9 | "net/http/httputil" 10 | "net/url" 11 | "strings" 12 | ) 13 | 14 | func NewWithTLS(socketPath string, tlsConfig *tls.Config) *http.Transport { 15 | unixTransport := &http.Transport{TLSClientConfig: tlsConfig} 16 | 17 | unixTransport.RegisterProtocol("unix", NewUnixRoundTripperTls(socketPath, tlsConfig)) 18 | return unixTransport 19 | } 20 | 21 | func New(socketPath string) *http.Transport { 22 | unixTransport := &http.Transport{} 23 | unixTransport.RegisterProtocol("unix", NewUnixRoundTripper(socketPath)) 24 | return unixTransport 25 | } 26 | 27 | type UnixRoundTripper struct { 28 | path string 29 | conn httputil.ClientConn 30 | useTls bool 31 | tlsConfig *tls.Config 32 | } 33 | 34 | func NewUnixRoundTripper(path string) *UnixRoundTripper { 35 | return &UnixRoundTripper{path: path} 36 | } 37 | 38 | func NewUnixRoundTripperTls(path string, tlsConfig *tls.Config) *UnixRoundTripper { 39 | return &UnixRoundTripper{ 40 | path: path, 41 | useTls: true, 42 | tlsConfig: tlsConfig, 43 | } 44 | } 45 | 46 | // The RoundTripper (http://golang.org/pkg/net/http/#RoundTripper) for the socket transport dials the socket 47 | // each time a request is made. 48 | func (roundTripper UnixRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { 49 | var conn net.Conn 50 | var err error 51 | if roundTripper.useTls { 52 | 53 | conn, err = tls.Dial("unix", roundTripper.path, roundTripper.tlsConfig) 54 | if err != nil { 55 | return nil, err 56 | } 57 | if conn == nil { 58 | return nil, errors.New("net/http: Transport.DialTLS returned (nil, nil)") 59 | } 60 | if tc, ok := conn.(*tls.Conn); ok { 61 | // Handshake here, in case DialTLS didn't. TLSNextProto below 62 | // depends on it for knowing the connection state. 63 | if err := tc.Handshake(); err != nil { 64 | go conn.Close() 65 | return nil, err 66 | } 67 | } 68 | } else { 69 | conn, err = net.Dial("unix", roundTripper.path) 70 | if err != nil { 71 | return nil, err 72 | } 73 | } 74 | 75 | socketClientConn := httputil.NewClientConn(conn, nil) 76 | defer socketClientConn.Close() 77 | 78 | newReq, err := roundTripper.rewriteRequest(req) 79 | if err != nil { 80 | return nil, err 81 | } 82 | 83 | return socketClientConn.Do(newReq) 84 | } 85 | 86 | func (roundTripper *UnixRoundTripper) rewriteRequest(req *http.Request) (*http.Request, error) { 87 | requestPath := req.URL.Path 88 | if !strings.HasPrefix(requestPath, roundTripper.path) { 89 | return nil, fmt.Errorf("Wrong unix socket [unix://%s]. Expected unix socket is [%s]", requestPath, roundTripper.path) 90 | } 91 | 92 | reqPath := strings.TrimPrefix(requestPath, roundTripper.path) 93 | newReqUrl := fmt.Sprintf("unix://%s", reqPath) 94 | 95 | var err error 96 | newURL, err := url.Parse(newReqUrl) 97 | if err != nil { 98 | return nil, err 99 | } 100 | 101 | req.URL.Path = newURL.Path 102 | req.URL.Host = roundTripper.path 103 | return req, nil 104 | 105 | } 106 | -------------------------------------------------------------------------------- /http_server/test_certs/server-ca.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIJKAIBAAKCAgEAzmfU10Fqqc67WGVA4jeNXfEAHm0c3bosgLsJSWfKMy4/QJ3Z 3 | TVs3DS+jQu83xeXFUP934JG5Kq/LwekEPCVy03VHRGLrevX0dptFchq2/bye9x0s 4 | ZA57s5dT0rUB+MA4NWTcbbsmjVHggL75vsqRB2E/N7KFN4n0Hlz1NTwCmJpYu+wQ 5 | VX/gypDempO5L9sSkl3XB/d9z7rKuwoxJsz49QhsS84+UrJiV20mU0cjWh0dBTfm 6 | 6vRiIbLoy0wjvcWsLcp6Cn6GKt9feMmZB+IBwol3hpUwiwOQEXMm90tfTzwLihLl 7 | arfNsx2lqJ7bpDIu2sGMHaEoEPvsccIuW9PJ6aLecgUnhMXRBGtx9DWqkqDKeSjS 8 | kPLJ4XTusf0GAAnnrJTnrpdfnvAoEuET3elHvuO8K/tTFDLIBhJ8S5iczaH+k8QA 9 | MpwO/7Mran1Aij5P02PS7DldrIe/wD0DoKH/dAShPyV3u09vOYLx7DG9NHPmVDpV 10 | P5PgIvpWm566KxyHalIDL+Ro+VW/YXoIHfsUS/ejVHf1XNDxC6an3IzvkgVShefV 11 | MYjysB/zWTgw7+h3w1rm+ekZfkESDYui3c3+aPwnocHICt1W9Dlg7XAzTx0Ui4xT 12 | C/sTQDSfxxQztnBnZHG2QGo3ACrbvbQzytVk5R9YijLZofV5UdeEv6uyR8sCAwEA 13 | AQKCAgAyAPuSTnnNu5StfJI0e6rW2FzkAiEdIk5HvYgpbWiR76FJQTR0xiVXH3RY 14 | 8eU8H0cnMUzUbdlDGyWsy4vIGnZv/hiO27wapN1doo43b3tnizujuECZ2NxlDMM0 15 | 578biU3UuaOhPdbAI9bUue99JkvuUsPi9W/KnbQzaufIxsoGOFZI6I6od/t4d5JG 16 | NoWFr3gXV36RooarPaCBoZ14ve2OR0UdNFDq7eZfMcU4JaLN4QG30uwxfZUMfvBg 17 | 2AhMoEfvK/9W7YIJfuX9ODHHtBwjCfSX+SqycuGDKLeG746efnh5Mcy8htzhiMwq 18 | RpoFdFbfjkYoo3M2ciG9CYL7ohpm/SGh7o2+OsvC2emCA3tlUpDVAo/KaZDq2hGI 19 | q8umqTJZKgWS1YlvtEGgvqTPIpee0I8htAh3mIORQCSGWExaR9qhXQS4QBR1hmEs 20 | bKCnwPSLTC7OT1FlpZN+24LnaYgdJJnqcU4elzTLE0Ip+esajFNqafjNQ9mkrfoS 21 | 8/ZfIHBl1n2gddpBbFDhxx9FyfEpKG1PC9vmoKoYGRs2OjCSNje+xnmuxzT0CkRJ 22 | hll4M5P16BamiXPkUicSMFMjk3iy2Cz+keyxzyLmAY518g/Y0XiZuGuhCWY+0XAM 23 | 3dHaDE3D9+66rnmVs5DlWiHcBrPjp0mjbpOKkBKP7rgsKB6DcQKCAQEA76JMSSKV 24 | P+tNvrvs8By/93dUvnp2zWmrom3XBW+84G9Jq8sUP2HWgc4ptrZzb6I8ltfFdz9D 25 | 9hcdCmzNTyE08XySOUT8KMckrLJ7vJ6so9oIc8sQGQhpSFaSprnsTjsNgKSMT8Vz 26 | DkTGdvagKoBnkXtUqQTmh4AVtmltjQ7eRBkfEe02/LT8V/7QWUNoHxpUxKo0uOAv 27 | mGiKdsjqBQqZv0XoVvOslGGUafc1LDGYa+wJ/tRek5OhTs+Ja9dsNS6kRTwnwgxg 28 | pNCbT9aOpamzY183PDyPd0K8sihOo0bi8+jFos2XJ+s1gh7Lpzo7dYSHAV6QPnqi 29 | e2smgCfvGmBLWQKCAQEA3ICTkVxMeAwHn0YlPwyoNSpSv9IIKXtrtXEXHsuTr/4E 30 | EEPRlSUnW5PcOs1qnSOn/snjcRCDlYgKNAB9e9Oft1iIHjtcjfAyLYeLSXmZFTT8 31 | ig+n6jnN3aIGLhUoXUF2jCD17EbqHh9Nn01leHVnbPSe1XNAnCuLLvQEhFqEimYI 32 | tWVukel/y66zNsbbNwuahni4qczJ0tws9+ZPhv6mF973/EqUNdtnR8GdPYfB88OB 33 | T9+94tOJKmGIeKYFilwv0/Yv3MGFr8hCfh1+Rdc7HydKVO99xakBqyZIDiNgJi+k 34 | DasKqRT7xS/tPstg5W64ZGrVG54CFDbl+LJ3l0qbwwKCAQBXMED7Vybgo9ecrzmN 35 | P5ilDHj7+QjiwjDdn4NdigM50aQHapNKYGmwvvc0cHvdwTS0WXuSYKV0k4JQebfV 36 | s6pUttNpHO12VMbGQwZ8YWtFDp6GqvqHcSUFWeJv4TPWXuwRw0z99URgi6t+O0uZ 37 | SzDjoPDzskHCSVsdDIz8hs1eD5nbZujb12n9Bkx+PeVTc7wl9hvKrF8E3/yJLZ7g 38 | CLRaALCBepVvQ3XBfF2PX9gqZC5a1qA2p79IMoC4iR/o1biVLEb29pPvQ6tOyC0M 39 | n4sSe+FX/Fzisph/ZeT+yVroDehizNTThGPqnRPSG4DoyDhqyiJHaU1XBGx0spLv 40 | kNu5AoIBAG4A+x2elUuifL756KT+tH/pgGTP7GB9gSuAos8rMp+vMunGW61zXqIy 41 | LsFrL0/7tNjIcV47pdmYh7aPtAptdhWq5iVm6fKprO7H2zYporRQvdhGnTed5NU3 42 | +qtMxNlZSkH4Q5e/fRbP+RKLMx8YOyPBGehU2hvPIV2oQQSif9Lnulp8ot/KRIYh 43 | vqiKJlzcvhdt7HpRfzJhw1FdJbmsGsJ36vGDZ1NDBNBoiABlVN4+X6mwbnPVom1x 44 | QPsnoEX5Xab5/8C7Du22E24FWrSO/qC5Ij0jSXStrvZqX7Il9da3F7n15Ziarkwe 45 | b9ZO8iUjynvWTEC94D2jcbFTn5Prla8CggEBANb9n08Vz/XqFt3qpbRTlaHYjfbs 46 | +wqdKVx4dvykMtoYDxEEXzMQgx6hpO9Z7HwcVf/2T+vydI7aViha6C3pv/G9zIQZ 47 | NwF0dKRylm69n/p9SCRSLg074ZNohiBCXKsAaJuwBNEvzJua6iEdy+SFb6Gh1GsB 48 | oXkpQMFhG5iu3WD8hDHslGhAhVy4nrGdgRFGDEKAuIXuNhCsf2zFHbB0tcvxlx6W 49 | u+KVcLi5xEcVghkrapwjEK4iTOCIyHxyC2kbAs1S8pqToQuiUpRw0bOmC3PeCuxi 50 | 65ibowPHV2aFsSH37+dj/q8UN0EPIAbxnAqJfKxBiVtgG4dv3SaJXEkaCvE= 51 | -----END RSA PRIVATE KEY----- 52 | -------------------------------------------------------------------------------- /grpc_server/server.go: -------------------------------------------------------------------------------- 1 | package grpc_server 2 | 3 | import ( 4 | "crypto/tls" 5 | "net" 6 | "os" 7 | 8 | "fmt" 9 | "reflect" 10 | 11 | "errors" 12 | 13 | "github.com/tedsuo/ifrit" 14 | "google.golang.org/grpc" 15 | "google.golang.org/grpc/credentials" 16 | ) 17 | 18 | type grpcServerRunner struct { 19 | listenAddress string 20 | handler interface{} 21 | serverRegistrar interface{} 22 | tlsConfig *tls.Config 23 | } 24 | 25 | // NewGRPCServer returns an ifrit.Runner for your GRPC server process, given artifacts normally generated from a 26 | // protobuf service definition by protoc. 27 | // 28 | // tlsConfig is optional. If nil the server will run insecure. 29 | // 30 | // handler must be an implementation of the interface generated by protoc. 31 | // 32 | // serverRegistrar must be the "RegisterXXXServer" method generated by protoc. 33 | // 34 | // Type checking occurs at runtime. Poorly typed `handler` or `serverRegistrar` parameters will result in an error 35 | // when the Runner is Invoked. 36 | func NewGRPCServer(listenAddress string, tlsConfig *tls.Config, handler, serverRegistrar interface{}) ifrit.Runner { 37 | return &grpcServerRunner{ 38 | listenAddress: listenAddress, 39 | handler: handler, 40 | serverRegistrar: serverRegistrar, 41 | tlsConfig: tlsConfig, 42 | } 43 | } 44 | 45 | func (s *grpcServerRunner) Validate() error { 46 | if s.serverRegistrar == nil || s.handler == nil { 47 | return errors.New("NewGRPCServer: `serverRegistrar` and `handler` must be non nil") 48 | } 49 | 50 | vServerRegistrar := reflect.ValueOf(s.serverRegistrar) 51 | vHandler := reflect.ValueOf(s.handler) 52 | 53 | registrarType := vServerRegistrar.Type() 54 | handlerType := vHandler.Type() 55 | 56 | // registrar type must be `func(*grpc.Server, X)` 57 | if registrarType.Kind() != reflect.Func { 58 | return fmt.Errorf("NewGRPCServer: `serverRegistrar` should be %s but is %s", 59 | reflect.Func, registrarType.Kind()) 60 | } 61 | if registrarType.NumIn() != 2 { 62 | return fmt.Errorf("NewGRPCServer: `serverRegistrar` should have 2 parameters but it has %d parameters", 63 | registrarType.NumIn()) 64 | } 65 | if registrarType.NumOut() != 0 { 66 | return fmt.Errorf("NewGRPCServer: `serverRegistrar` should return no value but it returns %d values", 67 | registrarType.NumOut()) 68 | } 69 | 70 | // registrar's first parameter type must be a grpc server 71 | if reflect.TypeOf((*grpc.Server)(nil)) != registrarType.In(0) { 72 | return fmt.Errorf("NewGRPCServer: type of `serverRegistrar`'s first parameter must be `*grpc.Server` but is %s", 73 | registrarType.In(0)) 74 | } 75 | 76 | // registrar's second parameter type must be implemented by handler type. 77 | if registrarType.In(1).Kind() != reflect.Interface || !handlerType.Implements(registrarType.In(1)) { 78 | return fmt.Errorf("NewGRPCServer: type of `serverRegistrar`'s second parameter %s is not implemented by `handler` type %s", 79 | registrarType.In(1), handlerType) 80 | } 81 | return nil 82 | } 83 | 84 | func (s *grpcServerRunner) Run(signals <-chan os.Signal, ready chan<- struct{}) error { 85 | err := s.Validate() 86 | if err != nil { 87 | return err 88 | } 89 | 90 | vServerRegistrar := reflect.ValueOf(s.serverRegistrar) 91 | vHandler := reflect.ValueOf(s.handler) 92 | 93 | lis, err := net.Listen("tcp", s.listenAddress) 94 | if err != nil { 95 | return err 96 | } 97 | 98 | opts := []grpc.ServerOption{} 99 | if s.tlsConfig != nil { 100 | opts = append(opts, grpc.Creds(credentials.NewTLS(s.tlsConfig))) 101 | } 102 | server := grpc.NewServer(opts...) 103 | args := []reflect.Value{reflect.ValueOf(server), vHandler} 104 | vServerRegistrar.Call(args) 105 | 106 | errCh := make(chan error) 107 | go func() { 108 | errCh <- server.Serve(lis) 109 | }() 110 | 111 | close(ready) 112 | 113 | select { 114 | case <-signals: 115 | case err = <-errCh: 116 | } 117 | 118 | server.GracefulStop() 119 | return err 120 | } 121 | -------------------------------------------------------------------------------- /grouper/client.go: -------------------------------------------------------------------------------- 1 | package grouper 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/tedsuo/ifrit" 7 | ) 8 | 9 | /* 10 | DynamicClient provides a client with group controls and event notifications. 11 | A client can use the insert channel to add members to the group. When the group 12 | becomes full, the insert channel blocks until a running process exits the group. 13 | Once there are no more members to be added, the client can close the dynamic 14 | group, preventing new members from being added. 15 | */ 16 | type DynamicClient interface { 17 | 18 | /* 19 | EntranceListener provides a new buffered channel of entrance events, which are 20 | emited every time an inserted process is ready. To help prevent race conditions, 21 | every new channel is populated with previously emited events, up to it's buffer 22 | size. 23 | */ 24 | EntranceListener() <-chan EntranceEvent 25 | 26 | /* 27 | ExitListener provides a new buffered channel of exit events, which are emited 28 | every time an inserted process exits. To help prevent race conditions, every 29 | new channel is populated with previously emited events, up to it's buffer size. 30 | */ 31 | ExitListener() <-chan ExitEvent 32 | 33 | /* 34 | CloseNotifier provides a new unbuffered channel, which will emit a single event 35 | once the group has been closed. 36 | */ 37 | CloseNotifier() <-chan struct{} 38 | /* 39 | Inserter provides an unbuffered channel for adding members to a group. When the 40 | group becomes full, the insert channel blocks until a running process exits. 41 | Once the group is closed, insert channels block forever. 42 | */ 43 | Inserter() chan<- Member 44 | 45 | /* 46 | Close causes a dynamic group to become a static group. This means that no new 47 | members may be inserted, and the group will exit once all members have 48 | completed. 49 | */ 50 | Close() 51 | 52 | Get(name string) (ifrit.Process, bool) 53 | } 54 | 55 | type memberRequest struct { 56 | Name string 57 | Response chan ifrit.Process 58 | } 59 | 60 | /* 61 | dynamicClient implements DynamicClient. 62 | */ 63 | type dynamicClient struct { 64 | insertChannel chan Member 65 | getMemberChannel chan memberRequest 66 | completeNotifier chan struct{} 67 | closeNotifier chan struct{} 68 | closeOnce *sync.Once 69 | entranceBroadcaster *entranceEventBroadcaster 70 | exitBroadcaster *exitEventBroadcaster 71 | } 72 | 73 | func newClient(bufferSize int) dynamicClient { 74 | return dynamicClient{ 75 | insertChannel: make(chan Member), 76 | getMemberChannel: make(chan memberRequest), 77 | completeNotifier: make(chan struct{}), 78 | closeNotifier: make(chan struct{}), 79 | closeOnce: new(sync.Once), 80 | entranceBroadcaster: newEntranceEventBroadcaster(bufferSize), 81 | exitBroadcaster: newExitEventBroadcaster(bufferSize), 82 | } 83 | } 84 | 85 | func (c dynamicClient) Get(name string) (ifrit.Process, bool) { 86 | req := memberRequest{ 87 | Name: name, 88 | Response: make(chan ifrit.Process), 89 | } 90 | select { 91 | case c.getMemberChannel <- req: 92 | p, ok := <-req.Response 93 | if !ok { 94 | return nil, false 95 | } 96 | return p, true 97 | case <-c.completeNotifier: 98 | return nil, false 99 | } 100 | } 101 | 102 | func (c dynamicClient) memberRequests() chan memberRequest { 103 | return c.getMemberChannel 104 | } 105 | 106 | func (c dynamicClient) Inserter() chan<- Member { 107 | return c.insertChannel 108 | } 109 | 110 | func (c dynamicClient) insertEventListener() <-chan Member { 111 | return c.insertChannel 112 | } 113 | 114 | func (c dynamicClient) EntranceListener() <-chan EntranceEvent { 115 | return c.entranceBroadcaster.Attach() 116 | } 117 | 118 | func (c dynamicClient) broadcastEntrance(event EntranceEvent) { 119 | c.entranceBroadcaster.Broadcast(event) 120 | } 121 | 122 | func (c dynamicClient) closeEntranceBroadcaster() { 123 | c.entranceBroadcaster.Close() 124 | } 125 | 126 | func (c dynamicClient) ExitListener() <-chan ExitEvent { 127 | return c.exitBroadcaster.Attach() 128 | } 129 | 130 | func (c dynamicClient) broadcastExit(event ExitEvent) { 131 | c.exitBroadcaster.Broadcast(event) 132 | } 133 | 134 | func (c dynamicClient) closeExitBroadcaster() { 135 | c.exitBroadcaster.Close() 136 | } 137 | 138 | func (c dynamicClient) closeBroadcasters() error { 139 | c.entranceBroadcaster.Close() 140 | c.exitBroadcaster.Close() 141 | close(c.completeNotifier) 142 | return nil 143 | } 144 | 145 | func (c dynamicClient) Close() { 146 | c.closeOnce.Do(func() { 147 | close(c.closeNotifier) 148 | }) 149 | } 150 | 151 | func (c dynamicClient) CloseNotifier() <-chan struct{} { 152 | return c.closeNotifier 153 | } 154 | -------------------------------------------------------------------------------- /grouper/dynamic_group_test.go: -------------------------------------------------------------------------------- 1 | package grouper_test 2 | 3 | import ( 4 | "os" 5 | "syscall" 6 | "time" 7 | 8 | . "github.com/onsi/ginkgo" 9 | . "github.com/onsi/gomega" 10 | "github.com/tedsuo/ifrit" 11 | "github.com/tedsuo/ifrit/fake_runner" 12 | "github.com/tedsuo/ifrit/grouper" 13 | ) 14 | 15 | var _ = Describe("dynamicGroup", func() { 16 | var ( 17 | client grouper.DynamicClient 18 | pool grouper.DynamicGroup 19 | poolProcess ifrit.Process 20 | 21 | childRunner1 *fake_runner.TestRunner 22 | childRunner2 *fake_runner.TestRunner 23 | childRunner3 *fake_runner.TestRunner 24 | ) 25 | 26 | BeforeEach(func() { 27 | childRunner1 = fake_runner.NewTestRunner() 28 | childRunner2 = fake_runner.NewTestRunner() 29 | childRunner3 = fake_runner.NewTestRunner() 30 | }) 31 | AfterEach(func() { 32 | childRunner1.EnsureExit() 33 | childRunner2.EnsureExit() 34 | childRunner3.EnsureExit() 35 | }) 36 | 37 | Describe("Get", func() { 38 | var member1, member2, member3 grouper.Member 39 | 40 | BeforeEach(func() { 41 | member1 = grouper.Member{"child1", childRunner1} 42 | member2 = grouper.Member{"child2", childRunner2} 43 | member3 = grouper.Member{"child3", childRunner3} 44 | 45 | pool = grouper.NewDynamic(nil, 3, 2) 46 | client = pool.Client() 47 | poolProcess = ifrit.Invoke(pool) 48 | 49 | insert := client.Inserter() 50 | Eventually(insert).Should(BeSent(member1)) 51 | Eventually(insert).Should(BeSent(member2)) 52 | Eventually(insert).Should(BeSent(member3)) 53 | }) 54 | 55 | It("returns a process when the member is present", func() { 56 | signal1 := childRunner1.WaitForCall() 57 | p, ok := client.Get("child1") 58 | Ω(ok).Should(BeTrue()) 59 | p.Signal(syscall.SIGUSR2) 60 | Eventually(signal1).Should(Receive(Equal(syscall.SIGUSR2))) 61 | }) 62 | 63 | It("returns false when the member is not present", func() { 64 | _, ok := client.Get("blah") 65 | Ω(ok).Should(BeFalse()) 66 | }) 67 | }) 68 | 69 | Describe("Insert", func() { 70 | var member1, member2, member3 grouper.Member 71 | 72 | BeforeEach(func() { 73 | member1 = grouper.Member{"child1", childRunner1} 74 | member2 = grouper.Member{"child2", childRunner2} 75 | member3 = grouper.Member{"child3", childRunner3} 76 | 77 | pool = grouper.NewDynamic(nil, 3, 2) 78 | client = pool.Client() 79 | poolProcess = ifrit.Invoke(pool) 80 | 81 | insert := client.Inserter() 82 | Eventually(insert).Should(BeSent(member1)) 83 | Eventually(insert).Should(BeSent(member2)) 84 | Eventually(insert).Should(BeSent(member3)) 85 | }) 86 | 87 | AfterEach(func() { 88 | poolProcess.Signal(os.Kill) 89 | Eventually(poolProcess.Wait()).Should(Receive()) 90 | }) 91 | 92 | It("announces the events as processes move through their lifecycle", func() { 93 | entrance1, entrance2, entrance3 := grouper.EntranceEvent{}, grouper.EntranceEvent{}, grouper.EntranceEvent{} 94 | exit1, exit2, exit3 := grouper.ExitEvent{}, grouper.ExitEvent{}, grouper.ExitEvent{} 95 | 96 | entrances := client.EntranceListener() 97 | exits := client.ExitListener() 98 | 99 | childRunner2.TriggerReady() 100 | Eventually(entrances).Should(Receive(&entrance2)) 101 | Ω(entrance2.Member).Should(Equal(member2)) 102 | 103 | childRunner1.TriggerReady() 104 | Eventually(entrances).Should(Receive(&entrance1)) 105 | Ω(entrance1.Member).Should(Equal(member1)) 106 | 107 | childRunner3.TriggerReady() 108 | Eventually(entrances).Should(Receive(&entrance3)) 109 | Ω(entrance3.Member).Should(Equal(member3)) 110 | 111 | childRunner2.TriggerExit(nil) 112 | Eventually(exits).Should(Receive(&exit2)) 113 | Ω(exit2.Member).Should(Equal(member2)) 114 | 115 | childRunner1.TriggerExit(nil) 116 | Eventually(exits).Should(Receive(&exit1)) 117 | Ω(exit1.Member).Should(Equal(member1)) 118 | 119 | childRunner3.TriggerExit(nil) 120 | Eventually(exits).Should(Receive(&exit3)) 121 | Ω(exit3.Member).Should(Equal(member3)) 122 | }) 123 | 124 | It("announces the most recent events that have already occured, up to the buffer size", func() { 125 | childRunner1.TriggerReady() 126 | childRunner2.TriggerReady() 127 | childRunner3.TriggerReady() 128 | time.Sleep(time.Millisecond) 129 | 130 | entrances := client.EntranceListener() 131 | 132 | Eventually(entrances).Should(Receive()) 133 | Eventually(entrances).Should(Receive()) 134 | 135 | Consistently(entrances).ShouldNot(Receive()) 136 | 137 | childRunner1.TriggerExit(nil) 138 | childRunner2.TriggerExit(nil) 139 | childRunner3.TriggerExit(nil) 140 | time.Sleep(time.Millisecond) 141 | 142 | exits := client.ExitListener() 143 | Eventually(exits).Should(Receive()) 144 | Eventually(exits).Should(Receive()) 145 | 146 | Consistently(exits).ShouldNot(Receive()) 147 | }) 148 | }) 149 | }) 150 | -------------------------------------------------------------------------------- /grouper/queue_ordered.go: -------------------------------------------------------------------------------- 1 | package grouper 2 | 3 | import ( 4 | "os" 5 | "reflect" 6 | 7 | "github.com/tedsuo/ifrit" 8 | ) 9 | 10 | /* 11 | NewQueuedOrdered starts its members in order, each member starting when the previous 12 | becomes ready. On shutdown however, unlike the ordered group, it shuts the started 13 | processes down in forward order. 14 | */ 15 | func NewQueueOrdered(terminationSignal os.Signal, members Members) ifrit.Runner { 16 | return &queueOrdered{ 17 | terminationSignal: terminationSignal, 18 | pool: make(map[string]ifrit.Process), 19 | members: members, 20 | } 21 | } 22 | 23 | type queueOrdered struct { 24 | terminationSignal os.Signal 25 | pool map[string]ifrit.Process 26 | members Members 27 | } 28 | 29 | func (g *queueOrdered) Run(signals <-chan os.Signal, ready chan<- struct{}) error { 30 | err := g.validate() 31 | if err != nil { 32 | return err 33 | } 34 | 35 | signal, errTrace := g.queuedStart(signals) 36 | if errTrace != nil { 37 | return g.stop(g.terminationSignal, signals, errTrace) 38 | } 39 | 40 | if signal != nil { 41 | return g.stop(signal, signals, errTrace) 42 | } 43 | 44 | close(ready) 45 | 46 | signal, errTrace = g.waitForSignal(signals, errTrace) 47 | return g.stop(signal, signals, errTrace) 48 | } 49 | 50 | func (g *queueOrdered) validate() error { 51 | return g.members.Validate() 52 | } 53 | 54 | func (g *queueOrdered) queuedStart(signals <-chan os.Signal) (os.Signal, ErrorTrace) { 55 | for _, member := range g.members { 56 | p := ifrit.Background(member) 57 | cases := make([]reflect.SelectCase, 0, len(g.pool)+3) 58 | for i := 0; i < len(g.pool); i++ { 59 | cases = append(cases, reflect.SelectCase{ 60 | Dir: reflect.SelectRecv, 61 | Chan: reflect.ValueOf(g.pool[g.members[i].Name].Wait()), 62 | }) 63 | } 64 | 65 | cases = append(cases, reflect.SelectCase{ 66 | Dir: reflect.SelectRecv, 67 | Chan: reflect.ValueOf(p.Ready()), 68 | }) 69 | 70 | cases = append(cases, reflect.SelectCase{ 71 | Dir: reflect.SelectRecv, 72 | Chan: reflect.ValueOf(p.Wait()), 73 | }) 74 | 75 | cases = append(cases, reflect.SelectCase{ 76 | Dir: reflect.SelectRecv, 77 | Chan: reflect.ValueOf(signals), 78 | }) 79 | 80 | chosen, recv, _ := reflect.Select(cases) 81 | g.pool[member.Name] = p 82 | switch chosen { 83 | case len(cases) - 1: 84 | // signals 85 | return recv.Interface().(os.Signal), nil 86 | case len(cases) - 2: 87 | // p.Wait 88 | var err error 89 | if !recv.IsNil() { 90 | err = recv.Interface().(error) 91 | } 92 | return nil, ErrorTrace{ 93 | ExitEvent{Member: member, Err: err}, 94 | } 95 | case len(cases) - 3: 96 | // p.Ready 97 | default: 98 | // other member has exited 99 | var err error = nil 100 | if e := recv.Interface(); e != nil { 101 | err = e.(error) 102 | } 103 | return nil, ErrorTrace{ 104 | ExitEvent{Member: g.members[chosen], Err: err}, 105 | } 106 | } 107 | } 108 | 109 | return nil, nil 110 | } 111 | 112 | func (g *queueOrdered) waitForSignal(signals <-chan os.Signal, errTrace ErrorTrace) (os.Signal, ErrorTrace) { 113 | cases := make([]reflect.SelectCase, 0, len(g.pool)+1) 114 | for i := 0; i < len(g.pool); i++ { 115 | cases = append(cases, reflect.SelectCase{ 116 | Dir: reflect.SelectRecv, 117 | Chan: reflect.ValueOf(g.pool[g.members[i].Name].Wait()), 118 | }) 119 | } 120 | cases = append(cases, reflect.SelectCase{ 121 | Dir: reflect.SelectRecv, 122 | Chan: reflect.ValueOf(signals), 123 | }) 124 | 125 | chosen, recv, _ := reflect.Select(cases) 126 | if chosen == len(cases)-1 { 127 | return recv.Interface().(os.Signal), errTrace 128 | } 129 | 130 | var err error 131 | if !recv.IsNil() { 132 | err = recv.Interface().(error) 133 | } 134 | 135 | errTrace = append(errTrace, ExitEvent{ 136 | Member: g.members[chosen], 137 | Err: err, 138 | }) 139 | 140 | return g.terminationSignal, errTrace 141 | } 142 | 143 | func (g *queueOrdered) stop(signal os.Signal, signals <-chan os.Signal, errTrace ErrorTrace) error { 144 | errOccurred := false 145 | exited := map[string]struct{}{} 146 | if len(errTrace) > 0 { 147 | for _, exitEvent := range errTrace { 148 | exited[exitEvent.Member.Name] = struct{}{} 149 | if exitEvent.Err != nil { 150 | errOccurred = true 151 | } 152 | } 153 | } 154 | 155 | for i := 0; i < len(g.pool); i++ { 156 | m := g.members[i] 157 | if _, found := exited[m.Name]; found { 158 | continue 159 | } 160 | if p, ok := g.pool[m.Name]; ok { 161 | p.Signal(signal) 162 | Exited: 163 | for { 164 | select { 165 | case err := <-p.Wait(): 166 | errTrace = append(errTrace, ExitEvent{ 167 | Member: m, 168 | Err: err, 169 | }) 170 | if err != nil { 171 | errOccurred = true 172 | } 173 | break Exited 174 | case sig := <-signals: 175 | if sig != signal { 176 | signal = sig 177 | p.Signal(signal) 178 | } 179 | } 180 | } 181 | } 182 | } 183 | 184 | if errOccurred { 185 | return errTrace 186 | } 187 | 188 | return nil 189 | } 190 | -------------------------------------------------------------------------------- /http_server/unix_transport/unix_transport_test.go: -------------------------------------------------------------------------------- 1 | package unix_transport 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "io/ioutil" 7 | "net" 8 | "net/http" 9 | "net/http/httptest" 10 | "strings" 11 | 12 | "github.com/nu7hatch/gouuid" 13 | . "github.com/onsi/ginkgo" 14 | . "github.com/onsi/gomega" 15 | "github.com/onsi/gomega/ghttp" 16 | ) 17 | 18 | var _ = Describe("Unix transport", func() { 19 | 20 | var ( 21 | socket string 22 | client http.Client 23 | ) 24 | 25 | Context("with server listening", func() { 26 | 27 | var ( 28 | unixSocketListener net.Listener 29 | unixSocketServer *ghttp.Server 30 | resp *http.Response 31 | err error 32 | ) 33 | 34 | BeforeEach(func() { 35 | uuid, err := uuid.NewV4() 36 | Expect(err).NotTo(HaveOccurred()) 37 | 38 | socket = fmt.Sprintf("/tmp/%s.sock", uuid) 39 | unixSocketListener, err = net.Listen("unix", socket) 40 | Expect(err).NotTo(HaveOccurred()) 41 | 42 | unixSocketServer = ghttp.NewUnstartedServer() 43 | 44 | unixSocketServer.HTTPTestServer = &httptest.Server{ 45 | Listener: unixSocketListener, 46 | Config: &http.Server{Handler: unixSocketServer}, 47 | } 48 | unixSocketServer.Start() 49 | 50 | client = http.Client{Transport: New(socket)} 51 | }) 52 | 53 | Context("when a simple GET request is sent", func() { 54 | BeforeEach(func() { 55 | unixSocketServer.AppendHandlers( 56 | ghttp.CombineHandlers( 57 | ghttp.VerifyRequest("GET", "/_ping"), 58 | ghttp.RespondWith(http.StatusOK, "true"), 59 | ), 60 | ) 61 | 62 | resp, err = client.Get("unix://" + socket + "/_ping") 63 | }) 64 | 65 | It("responds with correct status", func() { 66 | Expect(err).NotTo(HaveOccurred()) 67 | Expect(resp.StatusCode).To(Equal(http.StatusOK)) 68 | 69 | }) 70 | 71 | It("responds with correct body", func() { 72 | bytes, err := ioutil.ReadAll(resp.Body) 73 | Expect(err).NotTo(HaveOccurred()) 74 | Expect(string(bytes)).To(Equal("true")) 75 | }) 76 | }) 77 | 78 | Context("when a POST request is sent", func() { 79 | const ( 80 | ReqBody = `"id":"some-id"` 81 | RespBody = `{"Image" : "ubuntu"}` 82 | ) 83 | 84 | assertBodyEquals := func(body io.ReadCloser, expectedContent string) { 85 | bytes, err := ioutil.ReadAll(body) 86 | Expect(err).NotTo(HaveOccurred()) 87 | Expect(string(bytes)).To(Equal(expectedContent)) 88 | 89 | } 90 | 91 | asserHeaderContains := func(header http.Header, key, value string) { 92 | Expect(header[key]).To(ConsistOf(value)) 93 | } 94 | 95 | BeforeEach(func() { 96 | validateBody := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 97 | assertBodyEquals(req.Body, ReqBody) 98 | }) 99 | 100 | validateQueryParams := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 101 | Expect(req.URL.RawQuery).To(Equal("fromImage=ubunut&tag=latest")) 102 | }) 103 | 104 | handleRequest := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 105 | w.Header().Set("Content-Type", "application/json") 106 | w.Write([]byte(RespBody)) 107 | }) 108 | 109 | unixSocketServer.AppendHandlers( 110 | ghttp.CombineHandlers( 111 | ghttp.VerifyRequest("POST", "/containers/create"), 112 | ghttp.VerifyContentType("application/json"), 113 | validateBody, 114 | validateQueryParams, 115 | handleRequest, 116 | ), 117 | ) 118 | body := strings.NewReader(ReqBody) 119 | req, err := http.NewRequest("POST", "unix://"+socket+"/containers/create?fromImage=ubunut&tag=latest", body) 120 | req.Header.Add("Content-Type", "application/json") 121 | Expect(err).NotTo(HaveOccurred()) 122 | 123 | resp, err = client.Do(req) 124 | Expect(err).NotTo(HaveOccurred()) 125 | 126 | }) 127 | 128 | It("responds with correct status", func() { 129 | Expect(resp.StatusCode).To(Equal(http.StatusOK)) 130 | }) 131 | 132 | It("responds with correct headers", func() { 133 | asserHeaderContains(resp.Header, "Content-Type", "application/json") 134 | }) 135 | 136 | It("responds with correct body", func() { 137 | assertBodyEquals(resp.Body, RespBody) 138 | }) 139 | 140 | }) 141 | 142 | Context("when socket in reques URI is incorrect", func() { 143 | It("errors", func() { 144 | resp, err = client.Get("unix:///fake/socket.sock/_ping") 145 | Expect(err).To(HaveOccurred()) 146 | Expect(err.Error()).To(ContainSubstring("Wrong unix socket")) 147 | }) 148 | }) 149 | 150 | AfterEach(func() { 151 | unixSocketServer.Close() 152 | }) 153 | }) 154 | 155 | Context("with no server listening", func() { 156 | BeforeEach(func() { 157 | socket = "/not/existing.sock" 158 | client = http.Client{Transport: New(socket)} 159 | }) 160 | 161 | It("errors", func() { 162 | _, err := client.Get("unix:///not/existing.sock/_ping") 163 | Expect(err).To(HaveOccurred()) 164 | Expect(err.Error()).To(Or( 165 | ContainSubstring(fmt.Sprintf("dial unix %s: connect: no such file or directory", socket)), 166 | ContainSubstring(fmt.Sprintf("dial unix %s: no such file or directory", socket)), 167 | )) 168 | }) 169 | }) 170 | }) 171 | -------------------------------------------------------------------------------- /grouper/ordered.go: -------------------------------------------------------------------------------- 1 | package grouper 2 | 3 | import ( 4 | "os" 5 | "reflect" 6 | 7 | "github.com/tedsuo/ifrit" 8 | ) 9 | 10 | /* 11 | NewOrdered starts it's members in order, each member starting when the previous 12 | becomes ready. On shutdown, it will shut the started processes down in reverse order. 13 | Use an ordered group to describe a list of dependent processes, where each process 14 | depends upon the previous being available in order to function correctly. 15 | */ 16 | func NewOrdered(terminationSignal os.Signal, members Members) ifrit.Runner { 17 | return &orderedGroup{ 18 | terminationSignal: terminationSignal, 19 | pool: make(map[string]ifrit.Process), 20 | members: members, 21 | } 22 | } 23 | 24 | type orderedGroup struct { 25 | terminationSignal os.Signal 26 | pool map[string]ifrit.Process 27 | members Members 28 | } 29 | 30 | func (g *orderedGroup) Run(signals <-chan os.Signal, ready chan<- struct{}) error { 31 | err := g.validate() 32 | if err != nil { 33 | return err 34 | } 35 | 36 | signal, errTrace := g.orderedStart(signals) 37 | if errTrace != nil { 38 | return g.stop(g.terminationSignal, signals, errTrace) 39 | } 40 | 41 | if signal != nil { 42 | return g.stop(signal, signals, errTrace) 43 | } 44 | 45 | close(ready) 46 | 47 | signal, errTrace = g.waitForSignal(signals, errTrace) 48 | return g.stop(signal, signals, errTrace) 49 | } 50 | 51 | func (g *orderedGroup) validate() error { 52 | return g.members.Validate() 53 | } 54 | 55 | func (g *orderedGroup) orderedStart(signals <-chan os.Signal) (os.Signal, ErrorTrace) { 56 | for _, member := range g.members { 57 | p := ifrit.Background(member) 58 | cases := make([]reflect.SelectCase, 0, len(g.pool)+3) 59 | for i := 0; i < len(g.pool); i++ { 60 | cases = append(cases, reflect.SelectCase{ 61 | Dir: reflect.SelectRecv, 62 | Chan: reflect.ValueOf(g.pool[g.members[i].Name].Wait()), 63 | }) 64 | } 65 | cases = append(cases, reflect.SelectCase{ 66 | Dir: reflect.SelectRecv, 67 | Chan: reflect.ValueOf(p.Ready()), 68 | }) 69 | 70 | cases = append(cases, reflect.SelectCase{ 71 | Dir: reflect.SelectRecv, 72 | Chan: reflect.ValueOf(p.Wait()), 73 | }) 74 | 75 | cases = append(cases, reflect.SelectCase{ 76 | Dir: reflect.SelectRecv, 77 | Chan: reflect.ValueOf(signals), 78 | }) 79 | 80 | chosen, recv, _ := reflect.Select(cases) 81 | g.pool[member.Name] = p 82 | switch chosen { 83 | case len(cases) - 1: 84 | // signals 85 | return recv.Interface().(os.Signal), nil 86 | case len(cases) - 2: 87 | // p.Wait 88 | var err error 89 | if !recv.IsNil() { 90 | err = recv.Interface().(error) 91 | } 92 | return nil, ErrorTrace{ 93 | ExitEvent{Member: member, Err: err}, 94 | } 95 | case len(cases) - 3: 96 | // p.Ready 97 | default: 98 | // other member has exited 99 | var err error = nil 100 | if e := recv.Interface(); e != nil { 101 | err = e.(error) 102 | } 103 | return nil, ErrorTrace{ 104 | ExitEvent{Member: g.members[chosen], Err: err}, 105 | } 106 | } 107 | } 108 | 109 | return nil, nil 110 | } 111 | 112 | func (g *orderedGroup) waitForSignal(signals <-chan os.Signal, errTrace ErrorTrace) (os.Signal, ErrorTrace) { 113 | cases := make([]reflect.SelectCase, 0, len(g.pool)+1) 114 | for i := 0; i < len(g.pool); i++ { 115 | cases = append(cases, reflect.SelectCase{ 116 | Dir: reflect.SelectRecv, 117 | Chan: reflect.ValueOf(g.pool[g.members[i].Name].Wait()), 118 | }) 119 | } 120 | cases = append(cases, reflect.SelectCase{ 121 | Dir: reflect.SelectRecv, 122 | Chan: reflect.ValueOf(signals), 123 | }) 124 | 125 | chosen, recv, _ := reflect.Select(cases) 126 | if chosen == len(cases)-1 { 127 | return recv.Interface().(os.Signal), errTrace 128 | } 129 | 130 | var err error 131 | if !recv.IsNil() { 132 | err = recv.Interface().(error) 133 | } 134 | 135 | errTrace = append(errTrace, ExitEvent{ 136 | Member: g.members[chosen], 137 | Err: err, 138 | }) 139 | 140 | return g.terminationSignal, errTrace 141 | } 142 | 143 | func (g *orderedGroup) stop(signal os.Signal, signals <-chan os.Signal, errTrace ErrorTrace) error { 144 | errOccurred := false 145 | exited := map[string]struct{}{} 146 | if len(errTrace) > 0 { 147 | for _, exitEvent := range errTrace { 148 | exited[exitEvent.Member.Name] = struct{}{} 149 | if exitEvent.Err != nil { 150 | errOccurred = true 151 | } 152 | } 153 | } 154 | 155 | for i := len(g.pool) - 1; i >= 0; i-- { 156 | m := g.members[i] 157 | if _, found := exited[m.Name]; found { 158 | continue 159 | } 160 | if p, ok := g.pool[m.Name]; ok { 161 | p.Signal(signal) 162 | Exited: 163 | for { 164 | select { 165 | case err := <-p.Wait(): 166 | errTrace = append(errTrace, ExitEvent{ 167 | Member: m, 168 | Err: err, 169 | }) 170 | if err != nil { 171 | errOccurred = true 172 | } 173 | break Exited 174 | case sig := <-signals: 175 | if sig != signal { 176 | signal = sig 177 | p.Signal(signal) 178 | } 179 | } 180 | } 181 | } 182 | } 183 | 184 | if errOccurred { 185 | return errTrace 186 | } 187 | 188 | return nil 189 | } 190 | -------------------------------------------------------------------------------- /grouper/parallel.go: -------------------------------------------------------------------------------- 1 | package grouper 2 | 3 | import ( 4 | "os" 5 | "reflect" 6 | 7 | "github.com/tedsuo/ifrit" 8 | ) 9 | 10 | /* 11 | NewParallel starts it's members simultaneously. Use a parallel group to describe a set 12 | of concurrent but independent processes. 13 | */ 14 | func NewParallel(terminationSignal os.Signal, members Members) ifrit.Runner { 15 | return parallelGroup{ 16 | terminationSignal: terminationSignal, 17 | pool: make(map[string]ifrit.Process), 18 | members: members, 19 | } 20 | } 21 | 22 | type parallelGroup struct { 23 | terminationSignal os.Signal 24 | pool map[string]ifrit.Process 25 | members Members 26 | } 27 | 28 | func (g parallelGroup) Run(signals <-chan os.Signal, ready chan<- struct{}) error { 29 | err := g.validate() 30 | if err != nil { 31 | return err 32 | } 33 | 34 | signal, errTrace := g.parallelStart(signals) 35 | if errTrace != nil { 36 | return g.stop(g.terminationSignal, signals, errTrace).ErrorOrNil() 37 | } 38 | 39 | if signal != nil { 40 | return g.stop(signal, signals, errTrace).ErrorOrNil() 41 | } 42 | 43 | close(ready) 44 | 45 | signal, errTrace = g.waitForSignal(signals, errTrace) 46 | return g.stop(signal, signals, errTrace).ErrorOrNil() 47 | } 48 | 49 | func (o parallelGroup) validate() error { 50 | return o.members.Validate() 51 | } 52 | 53 | func (g *parallelGroup) parallelStart(signals <-chan os.Signal) (os.Signal, ErrorTrace) { 54 | numMembers := len(g.members) 55 | 56 | cases := make([]reflect.SelectCase, 2*numMembers+1) 57 | 58 | for i, member := range g.members { 59 | process := ifrit.Background(member) 60 | 61 | g.pool[member.Name] = process 62 | 63 | cases[2*i] = reflect.SelectCase{ 64 | Dir: reflect.SelectRecv, 65 | Chan: reflect.ValueOf(process.Wait()), 66 | } 67 | 68 | cases[2*i+1] = reflect.SelectCase{ 69 | Dir: reflect.SelectRecv, 70 | Chan: reflect.ValueOf(process.Ready()), 71 | } 72 | } 73 | 74 | cases[2*numMembers] = reflect.SelectCase{ 75 | Dir: reflect.SelectRecv, 76 | Chan: reflect.ValueOf(signals), 77 | } 78 | 79 | numReady := 0 80 | for { 81 | chosen, recv, _ := reflect.Select(cases) 82 | 83 | switch { 84 | case chosen == 2*numMembers: 85 | return recv.Interface().(os.Signal), nil 86 | case chosen%2 == 0: 87 | recvError, _ := recv.Interface().(error) 88 | return nil, ErrorTrace{ExitEvent{Member: g.members[chosen/2], Err: recvError}} 89 | default: 90 | cases[chosen].Chan = reflect.Zero(cases[chosen].Chan.Type()) 91 | numReady++ 92 | if numReady == numMembers { 93 | return nil, nil 94 | } 95 | } 96 | } 97 | } 98 | 99 | func (g *parallelGroup) waitForSignal(signals <-chan os.Signal, errTrace ErrorTrace) (os.Signal, ErrorTrace) { 100 | cases := make([]reflect.SelectCase, 0, len(g.pool)+1) 101 | for i := 0; i < len(g.pool); i++ { 102 | cases = append(cases, reflect.SelectCase{ 103 | Dir: reflect.SelectRecv, 104 | Chan: reflect.ValueOf(g.pool[g.members[i].Name].Wait()), 105 | }) 106 | } 107 | cases = append(cases, reflect.SelectCase{ 108 | Dir: reflect.SelectRecv, 109 | Chan: reflect.ValueOf(signals), 110 | }) 111 | 112 | chosen, recv, _ := reflect.Select(cases) 113 | if chosen == len(cases)-1 { 114 | return recv.Interface().(os.Signal), errTrace 115 | } 116 | 117 | var err error 118 | if !recv.IsNil() { 119 | err = recv.Interface().(error) 120 | } 121 | 122 | errTrace = append(errTrace, ExitEvent{ 123 | Member: g.members[chosen], 124 | Err: err, 125 | }) 126 | 127 | return g.terminationSignal, errTrace 128 | } 129 | 130 | func (g *parallelGroup) stop(signal os.Signal, signals <-chan os.Signal, errTrace ErrorTrace) ErrorTrace { 131 | errOccurred := false 132 | exited := map[string]struct{}{} 133 | if len(errTrace) > 0 { 134 | for _, exitEvent := range errTrace { 135 | exited[exitEvent.Member.Name] = struct{}{} 136 | if exitEvent.Err != nil { 137 | errOccurred = true 138 | } 139 | } 140 | } 141 | 142 | cases := make([]reflect.SelectCase, 0, len(g.members)) 143 | liveMembers := make([]Member, 0, len(g.members)) 144 | for _, member := range g.members { 145 | if _, found := exited[member.Name]; found { 146 | continue 147 | } 148 | 149 | process := g.pool[member.Name] 150 | 151 | process.Signal(signal) 152 | 153 | cases = append(cases, reflect.SelectCase{ 154 | Dir: reflect.SelectRecv, 155 | Chan: reflect.ValueOf(process.Wait()), 156 | }) 157 | 158 | liveMembers = append(liveMembers, member) 159 | } 160 | 161 | cases = append(cases, reflect.SelectCase{ 162 | Dir: reflect.SelectRecv, 163 | Chan: reflect.ValueOf(signals), 164 | }) 165 | 166 | // account for the signals channel 167 | for numExited := 1; numExited < len(cases); numExited++ { 168 | chosen, recv, _ := reflect.Select(cases) 169 | cases[chosen].Chan = reflect.Zero(cases[chosen].Chan.Type()) 170 | recvError, _ := recv.Interface().(error) 171 | 172 | if chosen == len(cases)-1 { 173 | signal = recv.Interface().(os.Signal) 174 | for _, member := range liveMembers { 175 | g.pool[member.Name].Signal(signal) 176 | } 177 | continue 178 | } 179 | 180 | errTrace = append(errTrace, ExitEvent{ 181 | Member: liveMembers[chosen], 182 | Err: recvError, 183 | }) 184 | 185 | if recvError != nil { 186 | errOccurred = true 187 | } 188 | } 189 | 190 | if errOccurred { 191 | return errTrace 192 | } 193 | 194 | return nil 195 | } 196 | -------------------------------------------------------------------------------- /ginkgomon/ginkgomon.go: -------------------------------------------------------------------------------- 1 | /* 2 | Ginkgomon provides ginkgo test helpers. 3 | */ 4 | package ginkgomon 5 | 6 | import ( 7 | "fmt" 8 | "io" 9 | "os" 10 | "os/exec" 11 | "time" 12 | 13 | "github.com/onsi/ginkgo" 14 | . "github.com/onsi/gomega" 15 | "github.com/onsi/gomega/gbytes" 16 | "github.com/onsi/gomega/gexec" 17 | ) 18 | 19 | // Config defines a ginkgomon Runner. 20 | type Config struct { 21 | Command *exec.Cmd // process to be executed 22 | Name string // prefixes all output lines 23 | AnsiColorCode string // colors the output 24 | StartCheck string // text to match to indicate sucessful start. 25 | StartCheckTimeout time.Duration // how long to wait to see StartCheck 26 | Cleanup func() // invoked once the process exits 27 | } 28 | 29 | /* 30 | The ginkgomon Runner invokes a new process using gomega's gexec package. 31 | 32 | If a start check is defined, the runner will wait until it sees the start check 33 | before declaring ready. 34 | 35 | Runner implements gexec.Exiter and gbytes.BufferProvider, so you can test exit 36 | codes and process output using the appropriate gomega matchers: 37 | http://onsi.github.io/gomega/#gexec-testing-external-processes 38 | */ 39 | type Runner struct { 40 | Command *exec.Cmd 41 | Name string 42 | AnsiColorCode string 43 | StartCheck string 44 | StartCheckTimeout time.Duration 45 | Cleanup func() 46 | session *gexec.Session 47 | sessionReady chan struct{} 48 | } 49 | 50 | // New creates a ginkgomon Runner from a config object. Runners must be created 51 | // with New to properly initialize their internal state. 52 | func New(config Config) *Runner { 53 | return &Runner{ 54 | Name: config.Name, 55 | Command: config.Command, 56 | AnsiColorCode: config.AnsiColorCode, 57 | StartCheck: config.StartCheck, 58 | StartCheckTimeout: config.StartCheckTimeout, 59 | Cleanup: config.Cleanup, 60 | sessionReady: make(chan struct{}), 61 | } 62 | } 63 | 64 | // ExitCode returns the exit code of the process, or -1 if the process has not 65 | // exited. It can be used with the gexec.Exit matcher. 66 | func (r *Runner) ExitCode() int { 67 | if r.sessionReady == nil { 68 | ginkgo.Fail(fmt.Sprintf("ginkgomon.Runner '%s' improperly created without using New", r.Name)) 69 | } 70 | <-r.sessionReady 71 | return r.session.ExitCode() 72 | } 73 | 74 | // Buffer returns a gbytes.Buffer, for use with the gbytes.Say matcher. 75 | func (r *Runner) Buffer() *gbytes.Buffer { 76 | if r.sessionReady == nil { 77 | ginkgo.Fail(fmt.Sprintf("ginkgomon.Runner '%s' improperly created without using New", r.Name)) 78 | } 79 | <-r.sessionReady 80 | return r.session.Buffer() 81 | } 82 | 83 | // Err returns the gbytes.Buffer associated with the stderr stream. 84 | // For use with the gbytes.Say matcher. 85 | func (r *Runner) Err() *gbytes.Buffer { 86 | if r.sessionReady == nil { 87 | ginkgo.Fail(fmt.Sprintf("ginkgomon.Runner '%s' improperly created without using New", r.Name)) 88 | } 89 | <-r.sessionReady 90 | return r.session.Err 91 | } 92 | 93 | func (r *Runner) Run(sigChan <-chan os.Signal, ready chan<- struct{}) error { 94 | defer ginkgo.GinkgoRecover() 95 | 96 | allOutput := gbytes.NewBuffer() 97 | 98 | debugWriter := gexec.NewPrefixedWriter( 99 | fmt.Sprintf("\x1b[32m[d]\x1b[%s[%s]\x1b[0m ", r.AnsiColorCode, r.Name), 100 | ginkgo.GinkgoWriter, 101 | ) 102 | 103 | session, err := gexec.Start( 104 | r.Command, 105 | gexec.NewPrefixedWriter( 106 | fmt.Sprintf("\x1b[32m[o]\x1b[%s[%s]\x1b[0m ", r.AnsiColorCode, r.Name), 107 | io.MultiWriter(allOutput, ginkgo.GinkgoWriter), 108 | ), 109 | gexec.NewPrefixedWriter( 110 | fmt.Sprintf("\x1b[91m[e]\x1b[%s[%s]\x1b[0m ", r.AnsiColorCode, r.Name), 111 | io.MultiWriter(allOutput, ginkgo.GinkgoWriter), 112 | ), 113 | ) 114 | 115 | Ω(err).ShouldNot(HaveOccurred(), fmt.Sprintf("%s failed to start with err: %s", r.Name, err)) 116 | 117 | fmt.Fprintf(debugWriter, "spawned %s (pid: %d)\n", r.Command.Path, r.Command.Process.Pid) 118 | 119 | r.session = session 120 | if r.sessionReady != nil { 121 | close(r.sessionReady) 122 | } 123 | 124 | startCheckDuration := r.StartCheckTimeout 125 | if startCheckDuration == 0 { 126 | startCheckDuration = 5 * time.Second 127 | } 128 | 129 | var startCheckTimeout <-chan time.Time 130 | if r.StartCheck != "" { 131 | startCheckTimeout = time.After(startCheckDuration) 132 | } 133 | 134 | detectStartCheck := allOutput.Detect(r.StartCheck) 135 | 136 | for { 137 | select { 138 | case <-detectStartCheck: // works even with empty string 139 | allOutput.CancelDetects() 140 | startCheckTimeout = nil 141 | detectStartCheck = nil 142 | close(ready) 143 | 144 | case <-startCheckTimeout: 145 | // clean up hanging process 146 | session.Kill().Wait() 147 | 148 | // fail to start 149 | return fmt.Errorf( 150 | "did not see %s in command's output within %s. full output:\n\n%s", 151 | r.StartCheck, 152 | startCheckDuration, 153 | string(allOutput.Contents()), 154 | ) 155 | 156 | case signal := <-sigChan: 157 | session.Signal(signal) 158 | 159 | case <-session.Exited: 160 | if r.Cleanup != nil { 161 | r.Cleanup() 162 | } 163 | 164 | if session.ExitCode() == 0 { 165 | return nil 166 | } 167 | 168 | return fmt.Errorf("exit status %d", session.ExitCode()) 169 | } 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /ginkgomon_v2/ginkgomon.go: -------------------------------------------------------------------------------- 1 | /* 2 | Ginkgomon_v2 provides ginkgo test helpers that are compatible with Ginkgo v2+ 3 | */ 4 | package ginkgomon_v2 5 | 6 | import ( 7 | "fmt" 8 | "io" 9 | "os" 10 | "os/exec" 11 | "time" 12 | 13 | "github.com/onsi/ginkgo/v2" 14 | . "github.com/onsi/gomega" 15 | "github.com/onsi/gomega/gbytes" 16 | "github.com/onsi/gomega/gexec" 17 | ) 18 | 19 | // Config defines a ginkgomon Runner. 20 | type Config struct { 21 | Command *exec.Cmd // process to be executed 22 | Name string // prefixes all output lines 23 | AnsiColorCode string // colors the output 24 | StartCheck string // text to match to indicate sucessful start. 25 | StartCheckTimeout time.Duration // how long to wait to see StartCheck 26 | Cleanup func() // invoked once the process exits 27 | } 28 | 29 | /* 30 | The ginkgomon Runner invokes a new process using gomega's gexec package. 31 | 32 | If a start check is defined, the runner will wait until it sees the start check 33 | before declaring ready. 34 | 35 | Runner implements gexec.Exiter and gbytes.BufferProvider, so you can test exit 36 | codes and process output using the appropriate gomega matchers: 37 | http://onsi.github.io/gomega/#gexec-testing-external-processes 38 | */ 39 | type Runner struct { 40 | Command *exec.Cmd 41 | Name string 42 | AnsiColorCode string 43 | StartCheck string 44 | StartCheckTimeout time.Duration 45 | Cleanup func() 46 | session *gexec.Session 47 | sessionReady chan struct{} 48 | } 49 | 50 | // New creates a ginkgomon Runner from a config object. Runners must be created 51 | // with New to properly initialize their internal state. 52 | func New(config Config) *Runner { 53 | return &Runner{ 54 | Name: config.Name, 55 | Command: config.Command, 56 | AnsiColorCode: config.AnsiColorCode, 57 | StartCheck: config.StartCheck, 58 | StartCheckTimeout: config.StartCheckTimeout, 59 | Cleanup: config.Cleanup, 60 | sessionReady: make(chan struct{}), 61 | } 62 | } 63 | 64 | // ExitCode returns the exit code of the process, or -1 if the process has not 65 | // exited. It can be used with the gexec.Exit matcher. 66 | func (r *Runner) ExitCode() int { 67 | if r.sessionReady == nil { 68 | ginkgo.Fail(fmt.Sprintf("ginkgomon.Runner '%s' improperly created without using New", r.Name)) 69 | } 70 | <-r.sessionReady 71 | return r.session.ExitCode() 72 | } 73 | 74 | // Buffer returns a gbytes.Buffer, for use with the gbytes.Say matcher. 75 | func (r *Runner) Buffer() *gbytes.Buffer { 76 | if r.sessionReady == nil { 77 | ginkgo.Fail(fmt.Sprintf("ginkgomon.Runner '%s' improperly created without using New", r.Name)) 78 | } 79 | <-r.sessionReady 80 | return r.session.Buffer() 81 | } 82 | 83 | // Err returns the gbytes.Buffer associated with the stderr stream. 84 | // For use with the gbytes.Say matcher. 85 | func (r *Runner) Err() *gbytes.Buffer { 86 | if r.sessionReady == nil { 87 | ginkgo.Fail(fmt.Sprintf("ginkgomon.Runner '%s' improperly created without using New", r.Name)) 88 | } 89 | <-r.sessionReady 90 | return r.session.Err 91 | } 92 | 93 | func (r *Runner) Run(sigChan <-chan os.Signal, ready chan<- struct{}) error { 94 | defer ginkgo.GinkgoRecover() 95 | 96 | allOutput := gbytes.NewBuffer() 97 | 98 | debugWriter := gexec.NewPrefixedWriter( 99 | fmt.Sprintf("\x1b[32m[d]\x1b[%s[%s]\x1b[0m ", r.AnsiColorCode, r.Name), 100 | ginkgo.GinkgoWriter, 101 | ) 102 | 103 | session, err := gexec.Start( 104 | r.Command, 105 | gexec.NewPrefixedWriter( 106 | fmt.Sprintf("\x1b[32m[o]\x1b[%s[%s]\x1b[0m ", r.AnsiColorCode, r.Name), 107 | io.MultiWriter(allOutput, ginkgo.GinkgoWriter), 108 | ), 109 | gexec.NewPrefixedWriter( 110 | fmt.Sprintf("\x1b[91m[e]\x1b[%s[%s]\x1b[0m ", r.AnsiColorCode, r.Name), 111 | io.MultiWriter(allOutput, ginkgo.GinkgoWriter), 112 | ), 113 | ) 114 | 115 | Ω(err).ShouldNot(HaveOccurred(), fmt.Sprintf("%s failed to start with err: %s", r.Name, err)) 116 | 117 | fmt.Fprintf(debugWriter, "spawned %s (pid: %d)\n", r.Command.Path, r.Command.Process.Pid) 118 | 119 | r.session = session 120 | if r.sessionReady != nil { 121 | close(r.sessionReady) 122 | } 123 | 124 | startCheckDuration := r.StartCheckTimeout 125 | if startCheckDuration == 0 { 126 | startCheckDuration = 5 * time.Second 127 | } 128 | 129 | var startCheckTimeout <-chan time.Time 130 | if r.StartCheck != "" { 131 | startCheckTimeout = time.After(startCheckDuration) 132 | } 133 | 134 | detectStartCheck := allOutput.Detect(r.StartCheck) 135 | 136 | for { 137 | select { 138 | case <-detectStartCheck: // works even with empty string 139 | allOutput.CancelDetects() 140 | startCheckTimeout = nil 141 | detectStartCheck = nil 142 | close(ready) 143 | 144 | case <-startCheckTimeout: 145 | // clean up hanging process 146 | session.Kill().Wait() 147 | 148 | // fail to start 149 | return fmt.Errorf( 150 | "did not see %s in command's output within %s. full output:\n\n%s", 151 | r.StartCheck, 152 | startCheckDuration, 153 | string(allOutput.Contents()), 154 | ) 155 | 156 | case signal := <-sigChan: 157 | session.Signal(signal) 158 | 159 | case <-session.Exited: 160 | if r.Cleanup != nil { 161 | r.Cleanup() 162 | } 163 | 164 | if session.ExitCode() == 0 { 165 | return nil 166 | } 167 | 168 | return fmt.Errorf("exit status %d", session.ExitCode()) 169 | } 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /grouper/dynamic_group.go: -------------------------------------------------------------------------------- 1 | package grouper 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/tedsuo/ifrit" 8 | ) 9 | 10 | /* 11 | A DynamicGroup begins empty, and runs members as they are inserted. A 12 | dynamic group will continue to run, even when there are no members running 13 | within it, until it is signaled to stop. Once a dynamic group is signaled to 14 | stop, it will no longer accept new members, and waits for the currently running 15 | members to complete before exiting. 16 | */ 17 | type DynamicGroup interface { 18 | ifrit.Runner 19 | Client() DynamicClient 20 | } 21 | 22 | type dynamicGroup struct { 23 | client dynamicClient 24 | terminationSignal os.Signal 25 | poolSize int 26 | } 27 | 28 | /* 29 | NewDynamic creates a DynamicGroup. 30 | 31 | The maxCapacity argument sets the maximum number of concurrent processes. 32 | 33 | The eventBufferSize argument sets the number of entrance and exit events to be 34 | retained by the system. When a new event listener attaches, it will receive 35 | any previously emitted events, up to the eventBufferSize. Older events will be 36 | thrown away. The event buffer is meant to be used to avoid race conditions when 37 | the total number of members is known in advance. 38 | 39 | The signal argument sets the termination signal. If a member exits before 40 | being signaled, the group propogates the termination signal. A nil termination 41 | signal is not propogated. 42 | */ 43 | func NewDynamic(terminationSignal os.Signal, maxCapacity int, eventBufferSize int) DynamicGroup { 44 | return &dynamicGroup{ 45 | client: newClient(eventBufferSize), 46 | poolSize: maxCapacity, 47 | terminationSignal: terminationSignal, 48 | } 49 | } 50 | 51 | func (p *dynamicGroup) Client() DynamicClient { 52 | return p.client 53 | } 54 | 55 | func (p *dynamicGroup) Run(signals <-chan os.Signal, ready chan<- struct{}) error { 56 | processes := newProcessSet() 57 | insertEvents := p.client.insertEventListener() 58 | memberRequests := p.client.memberRequests() 59 | closeNotifier := p.client.CloseNotifier() 60 | entranceEvents := make(entranceEventChannel) 61 | exitEvents := make(exitEventChannel) 62 | 63 | invoking := 0 64 | close(ready) 65 | 66 | for { 67 | select { 68 | case shutdown := <-signals: 69 | processes.Signal(shutdown) 70 | p.client.Close() 71 | 72 | case <-closeNotifier: 73 | closeNotifier = nil 74 | insertEvents = nil 75 | if processes.Length() == 0 { 76 | return p.client.closeBroadcasters() 77 | } 78 | if invoking == 0 { 79 | p.client.closeEntranceBroadcaster() 80 | } 81 | 82 | case memberRequest := <-memberRequests: 83 | p, ok := processes.Get(memberRequest.Name) 84 | if ok { 85 | memberRequest.Response <- p 86 | } 87 | close(memberRequest.Response) 88 | 89 | case newMember, ok := <-insertEvents: 90 | if !ok { 91 | p.client.Close() 92 | insertEvents = nil 93 | break 94 | } 95 | 96 | process := ifrit.Background(newMember) 97 | processes.Add(newMember.Name, process) 98 | 99 | if processes.Length() == p.poolSize { 100 | insertEvents = nil 101 | } 102 | 103 | invoking++ 104 | 105 | go waitForEvents(newMember, process, entranceEvents, exitEvents) 106 | 107 | case entranceEvent := <-entranceEvents: 108 | invoking-- 109 | p.client.broadcastEntrance(entranceEvent) 110 | 111 | if closeNotifier == nil && invoking == 0 { 112 | p.client.closeEntranceBroadcaster() 113 | entranceEvents = nil 114 | } 115 | 116 | case exitEvent := <-exitEvents: 117 | processes.Remove(exitEvent.Member.Name) 118 | p.client.broadcastExit(exitEvent) 119 | 120 | if !processes.Signaled() && p.terminationSignal != nil { 121 | processes.Signal(p.terminationSignal) 122 | p.client.Close() 123 | insertEvents = nil 124 | } 125 | 126 | if processes.Complete() || (processes.Length() == 0 && insertEvents == nil) { 127 | return p.client.closeBroadcasters() 128 | } 129 | 130 | if !processes.Signaled() && closeNotifier != nil { 131 | insertEvents = p.client.insertEventListener() 132 | } 133 | } 134 | } 135 | } 136 | 137 | func waitForEvents( 138 | member Member, 139 | process ifrit.Process, 140 | entrance entranceEventChannel, 141 | exit exitEventChannel, 142 | ) { 143 | select { 144 | case <-process.Ready(): 145 | entrance <- EntranceEvent{ 146 | Member: member, 147 | Process: process, 148 | } 149 | 150 | exit <- ExitEvent{ 151 | Member: member, 152 | Err: <-process.Wait(), 153 | } 154 | 155 | case err := <-process.Wait(): 156 | entrance <- EntranceEvent{ 157 | Member: member, 158 | Process: process, 159 | } 160 | 161 | exit <- ExitEvent{ 162 | Member: member, 163 | Err: err, 164 | } 165 | } 166 | } 167 | 168 | type processSet struct { 169 | processes map[string]ifrit.Process 170 | shutdown os.Signal 171 | } 172 | 173 | func newProcessSet() *processSet { 174 | return &processSet{ 175 | processes: map[string]ifrit.Process{}, 176 | } 177 | } 178 | 179 | func (g *processSet) Signaled() bool { 180 | return g.shutdown != nil 181 | } 182 | 183 | func (g *processSet) Signal(signal os.Signal) { 184 | g.shutdown = signal 185 | 186 | for _, p := range g.processes { 187 | p.Signal(signal) 188 | } 189 | } 190 | 191 | func (g *processSet) Length() int { 192 | return len(g.processes) 193 | } 194 | 195 | func (g *processSet) Complete() bool { 196 | return len(g.processes) == 0 && g.shutdown != nil 197 | } 198 | 199 | func (g *processSet) Get(name string) (ifrit.Process, bool) { 200 | p, ok := g.processes[name] 201 | return p, ok 202 | } 203 | 204 | func (g *processSet) Add(name string, process ifrit.Process) { 205 | _, ok := g.processes[name] 206 | if ok { 207 | panic(fmt.Errorf("member inserted twice: %#v", name)) 208 | } 209 | g.processes[name] = process 210 | } 211 | 212 | func (g *processSet) Remove(name string) { 213 | delete(g.processes, name) 214 | } 215 | -------------------------------------------------------------------------------- /grouper/parallel_test.go: -------------------------------------------------------------------------------- 1 | package grouper_test 2 | 3 | import ( 4 | "errors" 5 | "os" 6 | "syscall" 7 | "time" 8 | 9 | "github.com/tedsuo/ifrit" 10 | "github.com/tedsuo/ifrit/fake_runner" 11 | "github.com/tedsuo/ifrit/ginkgomon" 12 | "github.com/tedsuo/ifrit/grouper" 13 | 14 | . "github.com/onsi/ginkgo" 15 | . "github.com/onsi/gomega" 16 | ) 17 | 18 | var _ = Describe("Parallel Group", func() { 19 | var ( 20 | groupRunner ifrit.Runner 21 | groupProcess ifrit.Process 22 | members grouper.Members 23 | 24 | childRunner1 *fake_runner.TestRunner 25 | childRunner2 *fake_runner.TestRunner 26 | childRunner3 *fake_runner.TestRunner 27 | 28 | Δ time.Duration = 10 * time.Millisecond 29 | ) 30 | 31 | BeforeEach(func() { 32 | childRunner1 = fake_runner.NewTestRunner() 33 | childRunner2 = fake_runner.NewTestRunner() 34 | childRunner3 = fake_runner.NewTestRunner() 35 | 36 | members = grouper.Members{ 37 | {"child1", childRunner1}, 38 | {"child2", childRunner2}, 39 | {"child3", childRunner3}, 40 | } 41 | 42 | groupRunner = grouper.NewParallel(os.Interrupt, members) 43 | }) 44 | 45 | AfterEach(func() { 46 | childRunner1.EnsureExit() 47 | childRunner2.EnsureExit() 48 | childRunner3.EnsureExit() 49 | 50 | ginkgomon.Kill(groupProcess) 51 | }) 52 | 53 | Describe("Start", func() { 54 | BeforeEach(func() { 55 | groupProcess = ifrit.Background(groupRunner) 56 | }) 57 | 58 | It("runs all runners at the same time", func() { 59 | Eventually(childRunner1.RunCallCount).Should(Equal(1)) 60 | Eventually(childRunner2.RunCallCount).Should(Equal(1)) 61 | Eventually(childRunner3.RunCallCount).Should(Equal(1)) 62 | 63 | Consistently(groupProcess.Ready()).ShouldNot(BeClosed()) 64 | 65 | childRunner1.TriggerReady() 66 | childRunner2.TriggerReady() 67 | childRunner3.TriggerReady() 68 | 69 | Eventually(groupProcess.Ready()).Should(BeClosed()) 70 | }) 71 | 72 | Describe("when all the runners are ready", func() { 73 | var ( 74 | signal1 <-chan os.Signal 75 | signal2 <-chan os.Signal 76 | signal3 <-chan os.Signal 77 | ) 78 | 79 | BeforeEach(func() { 80 | signal1 = childRunner1.WaitForCall() 81 | childRunner1.TriggerReady() 82 | signal2 = childRunner2.WaitForCall() 83 | childRunner2.TriggerReady() 84 | signal3 = childRunner3.WaitForCall() 85 | childRunner3.TriggerReady() 86 | 87 | Eventually(groupProcess.Ready()).Should(BeClosed()) 88 | }) 89 | 90 | Describe("when it receives a signal", func() { 91 | BeforeEach(func() { 92 | groupProcess.Signal(syscall.SIGUSR2) 93 | }) 94 | 95 | It("sends the signal to all child runners", func() { 96 | Eventually(signal1).Should(Receive(Equal(syscall.SIGUSR2))) 97 | Eventually(signal2).Should(Receive(Equal(syscall.SIGUSR2))) 98 | Eventually(signal3).Should(Receive(Equal(syscall.SIGUSR2))) 99 | }) 100 | 101 | It("doesn't send any more signals to remaining child processes", func() { 102 | Eventually(signal3).Should(Receive(Equal(syscall.SIGUSR2))) 103 | childRunner2.TriggerExit(nil) 104 | Consistently(signal3).ShouldNot(Receive()) 105 | }) 106 | }) 107 | 108 | Describe("when a process exits cleanly", func() { 109 | BeforeEach(func() { 110 | childRunner1.TriggerExit(nil) 111 | }) 112 | 113 | It("sends an interrupt signal to the other processes", func() { 114 | Eventually(signal2).Should(Receive(Equal(os.Interrupt))) 115 | Eventually(signal3).Should(Receive(Equal(os.Interrupt))) 116 | }) 117 | 118 | It("does not exit", func() { 119 | Consistently(groupProcess.Wait(), Δ).ShouldNot(Receive()) 120 | }) 121 | 122 | Describe("when another process exits", func() { 123 | BeforeEach(func() { 124 | childRunner2.TriggerExit(nil) 125 | }) 126 | 127 | It("doesn't send any more signals to remaining child processes", func() { 128 | Eventually(signal3).Should(Receive(Equal(os.Interrupt))) 129 | Consistently(signal3).ShouldNot(Receive()) 130 | }) 131 | }) 132 | 133 | Describe("when all of the processes have exited cleanly", func() { 134 | BeforeEach(func() { 135 | childRunner2.TriggerExit(nil) 136 | childRunner3.TriggerExit(nil) 137 | }) 138 | 139 | It("exits cleanly", func() { 140 | Eventually(groupProcess.Wait()).Should(Receive(BeNil())) 141 | }) 142 | }) 143 | 144 | Describe("when one of the processes exits with an error", func() { 145 | BeforeEach(func() { 146 | childRunner2.TriggerExit(errors.New("Fail")) 147 | childRunner3.TriggerExit(nil) 148 | }) 149 | 150 | It("returns an error indicating which child processes failed", func() { 151 | var err error 152 | Eventually(groupProcess.Wait()).Should(Receive(&err)) 153 | Ω(err).Should(ConsistOf( 154 | grouper.ExitEvent{grouper.Member{"child1", childRunner1}, nil}, 155 | grouper.ExitEvent{grouper.Member{"child2", childRunner2}, errors.New("Fail")}, 156 | grouper.ExitEvent{grouper.Member{"child3", childRunner3}, nil}, 157 | )) 158 | }) 159 | }) 160 | }) 161 | }) 162 | 163 | Describe("Failed start", func() { 164 | Context("when some processes exit before being ready", func() { 165 | BeforeEach(func() { 166 | signal1 := childRunner1.WaitForCall() 167 | childRunner1.TriggerReady() 168 | signal3 := childRunner3.WaitForCall() 169 | childRunner3.TriggerReady() 170 | 171 | childRunner2.TriggerExit(errors.New("Fail")) 172 | 173 | Consistently(groupProcess.Ready()).ShouldNot(BeClosed()) 174 | 175 | Eventually(signal1).Should(Receive(Equal(os.Interrupt))) 176 | Eventually(signal3).Should(Receive(Equal(os.Interrupt))) 177 | 178 | childRunner1.TriggerExit(nil) 179 | childRunner3.TriggerExit(nil) 180 | }) 181 | 182 | It("exits after stopping all processes", func() { 183 | var err error 184 | 185 | Eventually(groupProcess.Wait()).Should(Receive(&err)) 186 | Ω(err).Should(ConsistOf( 187 | grouper.ExitEvent{grouper.Member{"child2", childRunner2}, errors.New("Fail")}, 188 | grouper.ExitEvent{grouper.Member{"child1", childRunner1}, nil}, 189 | grouper.ExitEvent{grouper.Member{"child3", childRunner3}, nil}, 190 | )) 191 | }) 192 | }) 193 | 194 | Context("when all processes exit before any are ready", func() { 195 | BeforeEach(func() { 196 | childRunner1.TriggerExit(errors.New("Fail")) 197 | childRunner2.TriggerExit(nil) 198 | childRunner3.TriggerExit(nil) 199 | 200 | Consistently(groupProcess.Ready()).ShouldNot(BeClosed()) 201 | }) 202 | 203 | It("exits after stopping all processes", func() { 204 | var err error 205 | 206 | Eventually(groupProcess.Wait()).Should(Receive(&err)) 207 | 208 | Ω(err).Should(ConsistOf( 209 | grouper.ExitEvent{grouper.Member{"child1", childRunner1}, errors.New("Fail")}, 210 | grouper.ExitEvent{grouper.Member{"child2", childRunner2}, nil}, 211 | grouper.ExitEvent{grouper.Member{"child3", childRunner3}, nil}, 212 | )) 213 | }) 214 | }) 215 | }) 216 | }) 217 | }) 218 | -------------------------------------------------------------------------------- /grpc_server/server_test.go: -------------------------------------------------------------------------------- 1 | package grpc_server_test 2 | 3 | import ( 4 | "crypto/tls" 5 | "fmt" 6 | 7 | "google.golang.org/grpc" 8 | "google.golang.org/grpc/credentials" 9 | 10 | "os" 11 | "path" 12 | 13 | . "github.com/onsi/ginkgo" 14 | . "github.com/onsi/gomega" 15 | "github.com/tedsuo/ifrit" 16 | "github.com/tedsuo/ifrit/ginkgomon" 17 | "github.com/tedsuo/ifrit/grpc_server" 18 | "golang.org/x/net/context" 19 | "google.golang.org/grpc/examples/helloworld/helloworld" 20 | ) 21 | 22 | var _ = Describe("GRPCServer", func() { 23 | var ( 24 | listenAddress string 25 | runner ifrit.Runner 26 | serverProcess ifrit.Process 27 | tlsConfig *tls.Config 28 | ) 29 | 30 | BeforeEach(func() { 31 | var err error 32 | 33 | basePath := path.Join(os.Getenv("GOPATH"), "src", "github.com", "tedsuo", "ifrit", "http_server", "test_certs") 34 | certFile := path.Join(basePath, "server.crt") 35 | keyFile := path.Join(basePath, "server.key") 36 | 37 | tlsCert, err := tls.LoadX509KeyPair(certFile, keyFile) 38 | Expect(err).NotTo(HaveOccurred()) 39 | 40 | tlsConfig = &tls.Config{ 41 | InsecureSkipVerify: true, 42 | Certificates: []tls.Certificate{tlsCert}, 43 | } 44 | 45 | listenAddress = fmt.Sprintf("localhost:%d", 10000+GinkgoParallelNode()) 46 | }) 47 | 48 | Context("given an instatiated runner", func() { 49 | BeforeEach(func() { 50 | runner = grpc_server.NewGRPCServer(listenAddress, tlsConfig, &server{}, helloworld.RegisterGreeterServer) 51 | }) 52 | JustBeforeEach(func() { 53 | serverProcess = ginkgomon.Invoke(runner) 54 | }) 55 | 56 | AfterEach(func() { 57 | ginkgomon.Kill(serverProcess) 58 | }) 59 | 60 | It("serves on the listen address", func() { 61 | conn, err := grpc.Dial(listenAddress, grpc.WithTransportCredentials(credentials.NewTLS(tlsConfig))) 62 | Expect(err).NotTo(HaveOccurred()) 63 | 64 | helloClient := helloworld.NewGreeterClient(conn) 65 | _, err = helloClient.SayHello(context.Background(), &helloworld.HelloRequest{Name: "Fred"}) 66 | Expect(err).NotTo(HaveOccurred()) 67 | }) 68 | 69 | Context("when the server trys to listen on a busy port", func() { 70 | var alternateRunner ifrit.Runner 71 | 72 | BeforeEach(func() { 73 | alternateRunner = grpc_server.NewGRPCServer(listenAddress, tlsConfig, &server{}, helloworld.RegisterGreeterServer) 74 | }) 75 | 76 | It("exits with an error", func() { 77 | var err error 78 | process := ifrit.Background(alternateRunner) 79 | Eventually(process.Wait()).Should(Receive(&err)) 80 | Expect(err).To(HaveOccurred()) 81 | }) 82 | }) 83 | }) 84 | 85 | Context("when there is no tls config", func() { 86 | BeforeEach(func() { 87 | runner = grpc_server.NewGRPCServer(listenAddress, nil, &server{}, helloworld.RegisterGreeterServer) 88 | }) 89 | JustBeforeEach(func() { 90 | serverProcess = ginkgomon.Invoke(runner) 91 | }) 92 | 93 | AfterEach(func() { 94 | ginkgomon.Kill(serverProcess) 95 | }) 96 | 97 | It("serves on the listen address", func() { 98 | conn, err := grpc.Dial(listenAddress, grpc.WithInsecure()) 99 | Expect(err).NotTo(HaveOccurred()) 100 | 101 | helloClient := helloworld.NewGreeterClient(conn) 102 | _, err = helloClient.SayHello(context.Background(), &helloworld.HelloRequest{Name: "Fred"}) 103 | Expect(err).NotTo(HaveOccurred()) 104 | }) 105 | 106 | }) 107 | 108 | Context("when the inputs to NewGRPCServer are invalid", func() { 109 | var ( 110 | err error 111 | ) 112 | JustBeforeEach(func() { 113 | process := ifrit.Background(runner) 114 | Eventually(process.Wait()).Should(Receive(&err)) 115 | Expect(err).To(HaveOccurred()) 116 | }) 117 | 118 | Context("when the registrar is an integer", func() { 119 | BeforeEach(func() { 120 | runner = grpc_server.NewGRPCServer(listenAddress, tlsConfig, &server{}, 42) 121 | }) 122 | It("fails", func() { 123 | Expect(err.Error()).To(ContainSubstring("should be func but is int")) 124 | }) 125 | }) 126 | 127 | Context("when the registrar is nil", func() { 128 | BeforeEach(func() { 129 | runner = grpc_server.NewGRPCServer(listenAddress, tlsConfig, &server{}, nil) 130 | }) 131 | It("fails", func() { 132 | Expect(err.Error()).To(ContainSubstring("`serverRegistrar` and `handler` must be non nil")) 133 | }) 134 | }) 135 | 136 | Context("when the registrar is nil", func() { 137 | BeforeEach(func() { 138 | runner = grpc_server.NewGRPCServer(listenAddress, tlsConfig, nil, helloworld.RegisterGreeterServer) 139 | }) 140 | It("fails", func() { 141 | Expect(err.Error()).To(ContainSubstring("`serverRegistrar` and `handler` must be non nil")) 142 | }) 143 | }) 144 | 145 | Context("when the registrar is an empty func", func() { 146 | BeforeEach(func() { 147 | runner = grpc_server.NewGRPCServer(listenAddress, tlsConfig, &server{}, func() {}) 148 | }) 149 | It("fails", func() { 150 | Expect(err.Error()).To(ContainSubstring("should have 2 parameters but it has 0 parameters")) 151 | }) 152 | }) 153 | 154 | Context("when the registrar has bad parameters", func() { 155 | BeforeEach(func() { 156 | runner = grpc_server.NewGRPCServer(listenAddress, tlsConfig, &server{}, func(a, b int) {}) 157 | }) 158 | It("fails", func() { 159 | Expect(err.Error()).To(ContainSubstring("first parameter must be `*grpc.Server` but is int")) 160 | }) 161 | }) 162 | 163 | Context("when the registrar's first parameter is bad", func() { 164 | BeforeEach(func() { 165 | runner = grpc_server.NewGRPCServer(listenAddress, tlsConfig, &server{}, func(a, b int) {}) 166 | }) 167 | It("fails", func() { 168 | Expect(err.Error()).To(ContainSubstring("first parameter must be `*grpc.Server` but is int")) 169 | }) 170 | }) 171 | 172 | Context("when the registrar's second parameter is not an interface", func() { 173 | BeforeEach(func() { 174 | runner = grpc_server.NewGRPCServer(listenAddress, tlsConfig, &server{}, func(a *grpc.Server, b int) {}) 175 | }) 176 | It("fails", func() { 177 | Expect(err.Error()).To(ContainSubstring("is not implemented by `handler`")) 178 | }) 179 | }) 180 | 181 | Context("when the registrar's second parameter is not implemented", func() { 182 | BeforeEach(func() { 183 | runner = grpc_server.NewGRPCServer(listenAddress, tlsConfig, &server{}, func(a *grpc.Server, b testInterface) {}) 184 | }) 185 | It("fails", func() { 186 | Expect(err.Error()).To(ContainSubstring("is not implemented by `handler`")) 187 | }) 188 | }) 189 | 190 | Context("when the handler is a *struct but doesn't implement the registrar's second parameter", func() { 191 | BeforeEach(func() { 192 | runner = grpc_server.NewGRPCServer(listenAddress, tlsConfig, ¬Server{}, helloworld.RegisterGreeterServer) 193 | }) 194 | It("fails", func() { 195 | Expect(err.Error()).To(ContainSubstring("is not implemented by `handler`")) 196 | }) 197 | }) 198 | 199 | Context("when the handler is a int but doesn't implement the registrar's second parameter", func() { 200 | BeforeEach(func() { 201 | runner = grpc_server.NewGRPCServer(listenAddress, tlsConfig, 42, helloworld.RegisterGreeterServer) 202 | }) 203 | It("fails", func() { 204 | Expect(err.Error()).To(ContainSubstring("is not implemented by `handler`")) 205 | }) 206 | }) 207 | 208 | Context("when the registrar returns a value", func() { 209 | BeforeEach(func() { 210 | f := func(a *grpc.Server, b helloworld.GreeterServer) error { return nil } 211 | runner = grpc_server.NewGRPCServer(listenAddress, tlsConfig, &server{}, f) 212 | }) 213 | It("fails", func() { 214 | Expect(err.Error()).To(ContainSubstring("should return no value but it returns 1 value")) 215 | }) 216 | }) 217 | }) 218 | }) 219 | 220 | // server is used to implement helloworld.GreeterServer. 221 | type server struct{} 222 | 223 | // SayHello implements helloworld.GreeterServer 224 | func (s *server) SayHello(ctx context.Context, in *helloworld.HelloRequest) (*helloworld.HelloReply, error) { 225 | return &helloworld.HelloReply{Message: "Hello " + in.Name}, nil 226 | } 227 | 228 | // notServer doesn't implement anything 229 | type notServer struct{} 230 | 231 | type testInterface interface { 232 | something(a int) int 233 | } 234 | -------------------------------------------------------------------------------- /http_server/http_server_test.go: -------------------------------------------------------------------------------- 1 | package http_server_test 2 | 3 | import ( 4 | "crypto/tls" 5 | "fmt" 6 | "io/ioutil" 7 | "net/http" 8 | "os" 9 | "path" 10 | "syscall" 11 | 12 | . "github.com/onsi/ginkgo" 13 | . "github.com/onsi/gomega" 14 | "github.com/tedsuo/ifrit" 15 | "github.com/tedsuo/ifrit/http_server" 16 | "github.com/tedsuo/ifrit/http_server/unix_transport" 17 | ) 18 | 19 | var _ = Describe("HttpServer", func() { 20 | var ( 21 | address string 22 | server ifrit.Runner 23 | startedRequestChan chan struct{} 24 | finishRequestChan chan struct{} 25 | 26 | handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 27 | startedRequestChan <- struct{}{} 28 | <-finishRequestChan 29 | w.Write([]byte("yo")) 30 | }) 31 | ) 32 | 33 | BeforeEach(func() { 34 | startedRequestChan = make(chan struct{}, 1) 35 | finishRequestChan = make(chan struct{}, 1) 36 | port := 8000 + GinkgoParallelNode() 37 | address = fmt.Sprintf("127.0.0.1:%d", port) 38 | }) 39 | 40 | Describe("Invoke", func() { 41 | var process ifrit.Process 42 | var socketPath string 43 | 44 | Context("when the server is started with a different net Protocol.", func() { 45 | var tmpdir string 46 | 47 | BeforeEach(func() { 48 | unixHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 49 | w.Write([]byte("yo")) 50 | }) 51 | var err error 52 | tmpdir, err = ioutil.TempDir(os.TempDir(), "ifrit-server-test") 53 | Ω(err).ShouldNot(HaveOccurred()) 54 | 55 | socketPath = path.Join(tmpdir, "ifrit.sock") 56 | Ω(err).ShouldNot(HaveOccurred()) 57 | server = http_server.NewUnixServer(socketPath, unixHandler) 58 | process = ifrit.Invoke(server) 59 | }) 60 | 61 | AfterEach(func() { 62 | process.Signal(syscall.SIGINT) 63 | Eventually(process.Wait()).Should(Receive()) 64 | }) 65 | 66 | It("serves requests with the given handler", func() { 67 | resp, err := httpGetUnix("unix://"+socketPath+"/", socketPath) 68 | 69 | Ω(err).ShouldNot(HaveOccurred()) 70 | body, err := ioutil.ReadAll(resp.Body) 71 | Ω(err).ShouldNot(HaveOccurred()) 72 | Ω(string(body)).Should(Equal("yo")) 73 | }) 74 | }) 75 | 76 | Context("when the server starts successfully", func() { 77 | BeforeEach(func() { 78 | server = http_server.New(address, handler) 79 | process = ifrit.Invoke(server) 80 | }) 81 | 82 | AfterEach(func() { 83 | process.Signal(syscall.SIGINT) 84 | Eventually(process.Wait()).Should(Receive()) 85 | }) 86 | 87 | Context("and a request is in flight", func() { 88 | type httpResponse struct { 89 | response *http.Response 90 | err error 91 | } 92 | var responses chan httpResponse 93 | 94 | BeforeEach(func() { 95 | responses = make(chan httpResponse, 1) 96 | go func() { 97 | response, err := httpGet("http://" + address) 98 | responses <- httpResponse{response, err} 99 | close(responses) 100 | }() 101 | <-startedRequestChan 102 | }) 103 | 104 | AfterEach(func() { 105 | Eventually(responses).Should(BeClosed()) 106 | }) 107 | 108 | It("serves http requests with the given handler", func() { 109 | finishRequestChan <- struct{}{} 110 | 111 | var resp httpResponse 112 | Eventually(responses).Should(Receive(&resp)) 113 | 114 | Ω(resp.err).ShouldNot(HaveOccurred()) 115 | 116 | body, err := ioutil.ReadAll(resp.response.Body) 117 | Ω(err).ShouldNot(HaveOccurred()) 118 | Ω(string(body)).Should(Equal("yo")) 119 | }) 120 | 121 | Context("and it receives a signal", func() { 122 | BeforeEach(func() { 123 | process.Signal(syscall.SIGINT) 124 | }) 125 | 126 | It("stops serving new http requests", func() { 127 | _, err := httpGet("http://" + address) 128 | Ω(err).Should(HaveOccurred()) 129 | 130 | // make sure we exit 131 | finishRequestChan <- struct{}{} 132 | }) 133 | 134 | It("does not return an error", func() { 135 | finishRequestChan <- struct{}{} 136 | err := <-process.Wait() 137 | Ω(err).ShouldNot(HaveOccurred()) 138 | }) 139 | 140 | It("does not exit until all outstanding requests are complete", func() { 141 | Consistently(process.Wait()).ShouldNot(Receive()) 142 | finishRequestChan <- struct{}{} 143 | Eventually(process.Wait()).Should(Receive()) 144 | }) 145 | }) 146 | }) 147 | }) 148 | 149 | Context("when the server fails to start", func() { 150 | BeforeEach(func() { 151 | address = fmt.Sprintf("127.0.0.1:80") 152 | server = http_server.New(address, handler) 153 | }) 154 | 155 | It("returns an error", func() { 156 | process = ifrit.Invoke(server) 157 | err := <-process.Wait() 158 | Ω(err).Should(HaveOccurred()) 159 | }) 160 | }) 161 | 162 | Context("when the TLS server is started with a different net Protocol.", func() { 163 | var tlsConfig *tls.Config 164 | var tmpdir string 165 | var socketPath string 166 | 167 | BeforeEach(func() { 168 | basePath := path.Join(os.Getenv("GOPATH"), "src", "github.com", "tedsuo", "ifrit", "http_server", "test_certs") 169 | certFile := path.Join(basePath, "server.crt") 170 | keyFile := path.Join(basePath, "server.key") 171 | 172 | tlsCert, err := tls.LoadX509KeyPair(certFile, keyFile) 173 | Expect(err).NotTo(HaveOccurred()) 174 | 175 | tlsConfig = &tls.Config{ 176 | InsecureSkipVerify: true, 177 | } 178 | 179 | serverTlsConfig := &tls.Config{ 180 | Certificates: []tls.Certificate{tlsCert}, 181 | } 182 | 183 | unixHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 184 | w.Write([]byte("yo")) 185 | }) 186 | tmpdir, err = ioutil.TempDir(os.TempDir(), "ifrit-server-test") 187 | Ω(err).ShouldNot(HaveOccurred()) 188 | 189 | socketPath = path.Join(tmpdir, "ifrit.sock") 190 | Ω(err).ShouldNot(HaveOccurred()) 191 | server = http_server.NewUnixTLSServer(socketPath, unixHandler, serverTlsConfig) 192 | process = ifrit.Invoke(server) 193 | }) 194 | AfterEach(func() { 195 | process.Signal(syscall.SIGINT) 196 | Eventually(process.Wait()).Should(Receive()) 197 | }) 198 | 199 | It("serves tls-secured http requests with the given handler", func() { 200 | 201 | resp, err := httpTLSGetUnix("unix://"+socketPath+"/", socketPath, tlsConfig) 202 | Ω(err).ShouldNot(HaveOccurred()) 203 | body, err := ioutil.ReadAll(resp.Body) 204 | Ω(err).ShouldNot(HaveOccurred()) 205 | Ω(string(body)).Should(Equal("yo")) 206 | }) 207 | }) 208 | 209 | Context("and it starts a server with TLS", func() { 210 | var tlsConfig *tls.Config 211 | type httpResponse struct { 212 | response *http.Response 213 | err error 214 | } 215 | var responses chan httpResponse 216 | 217 | BeforeEach(func() { 218 | basePath := path.Join(os.Getenv("GOPATH"), "src", "github.com", "tedsuo", "ifrit", "http_server", "test_certs") 219 | certFile := path.Join(basePath, "server.crt") 220 | keyFile := path.Join(basePath, "server.key") 221 | 222 | tlsCert, err := tls.LoadX509KeyPair(certFile, keyFile) 223 | Expect(err).NotTo(HaveOccurred()) 224 | 225 | tlsConfig = &tls.Config{ 226 | InsecureSkipVerify: true, 227 | } 228 | 229 | serverTlsConfig := &tls.Config{ 230 | Certificates: []tls.Certificate{tlsCert}, 231 | } 232 | 233 | server = http_server.NewTLSServer(address, handler, serverTlsConfig) 234 | process = ifrit.Invoke(server) 235 | }) 236 | 237 | AfterEach(func() { 238 | process.Signal(syscall.SIGINT) 239 | Eventually(process.Wait()).Should(Receive()) 240 | }) 241 | 242 | Context("and a valid, secure request is in flight", func() { 243 | BeforeEach(func() { 244 | responses = make(chan httpResponse, 1) 245 | go func() { 246 | response, err := httpTLSGet("https://"+address, tlsConfig) 247 | responses <- httpResponse{response, err} 248 | close(responses) 249 | }() 250 | Eventually(startedRequestChan).Should(Receive()) 251 | }) 252 | 253 | AfterEach(func() { 254 | Eventually(responses).Should(BeClosed()) 255 | }) 256 | 257 | It("serves tls-secured http requests with the given handler", func() { 258 | finishRequestChan <- struct{}{} 259 | 260 | var resp httpResponse 261 | Eventually(responses).Should(Receive(&resp)) 262 | 263 | Ω(resp.err).ShouldNot(HaveOccurred()) 264 | 265 | body, err := ioutil.ReadAll(resp.response.Body) 266 | Ω(err).ShouldNot(HaveOccurred()) 267 | Ω(string(body)).Should(Equal("yo")) 268 | }) 269 | }) 270 | 271 | Context("and an insecure request is in flight", func() { 272 | BeforeEach(func() { 273 | responses = make(chan httpResponse, 1) 274 | go func() { 275 | response, err := httpGet("http://" + address) 276 | responses <- httpResponse{response, err} 277 | close(responses) 278 | }() 279 | Consistently(startedRequestChan).ShouldNot(Receive()) 280 | }) 281 | 282 | AfterEach(func() { 283 | Eventually(responses).Should(BeClosed()) 284 | }) 285 | 286 | It("rejects insecure http requests and receives an error", func() { 287 | finishRequestChan <- struct{}{} 288 | 289 | var resp httpResponse 290 | Eventually(responses).Should(Receive(&resp)) 291 | 292 | Ω(resp.err).ShouldNot(HaveOccurred()) 293 | Ω(resp.response.StatusCode).Should(Equal(http.StatusBadRequest)) 294 | }) 295 | }) 296 | }) 297 | }) 298 | }) 299 | 300 | func httpGetUnix(url, socketPath string) (*http.Response, error) { 301 | client := http.Client{ 302 | Transport: unix_transport.New(socketPath), 303 | } 304 | return client.Get(url) 305 | } 306 | 307 | func httpGet(url string) (*http.Response, error) { 308 | client := http.Client{ 309 | Transport: &http.Transport{ 310 | Proxy: http.ProxyFromEnvironment, 311 | }, 312 | } 313 | return client.Get(url) 314 | } 315 | 316 | func httpTLSGet(url string, tlsConfig *tls.Config) (*http.Response, error) { 317 | client := http.Client{ 318 | Transport: &http.Transport{ 319 | Proxy: http.ProxyFromEnvironment, 320 | TLSClientConfig: tlsConfig, 321 | }, 322 | } 323 | return client.Get(url) 324 | } 325 | 326 | func httpTLSGetUnix(url string, socketPath string, tlsConfig *tls.Config) (*http.Response, error) { 327 | client := http.Client{ 328 | Transport: unix_transport.NewWithTLS(socketPath, tlsConfig), 329 | } 330 | return client.Get(url) 331 | } 332 | -------------------------------------------------------------------------------- /grouper/ordered_test.go: -------------------------------------------------------------------------------- 1 | package grouper_test 2 | 3 | import ( 4 | "errors" 5 | "os" 6 | "sync/atomic" 7 | "syscall" 8 | "time" 9 | 10 | "github.com/tedsuo/ifrit" 11 | "github.com/tedsuo/ifrit/fake_runner" 12 | "github.com/tedsuo/ifrit/grouper" 13 | 14 | . "github.com/onsi/ginkgo" 15 | . "github.com/onsi/gomega" 16 | ) 17 | 18 | var _ = Describe("Ordered Group", func() { 19 | var ( 20 | started chan struct{} 21 | 22 | groupRunner ifrit.Runner 23 | groupProcess ifrit.Process 24 | members grouper.Members 25 | 26 | childRunner1 *fake_runner.TestRunner 27 | childRunner2 *fake_runner.TestRunner 28 | childRunner3 *fake_runner.TestRunner 29 | 30 | Δ time.Duration = 10 * time.Millisecond 31 | ) 32 | 33 | Describe("Start", func() { 34 | BeforeEach(func() { 35 | childRunner1 = fake_runner.NewTestRunner() 36 | childRunner2 = fake_runner.NewTestRunner() 37 | childRunner3 = fake_runner.NewTestRunner() 38 | 39 | members = grouper.Members{ 40 | {"child1", childRunner1}, 41 | {"child2", childRunner2}, 42 | {"child3", childRunner3}, 43 | } 44 | 45 | groupRunner = grouper.NewOrdered(os.Interrupt, members) 46 | }) 47 | 48 | AfterEach(func() { 49 | childRunner1.EnsureExit() 50 | childRunner2.EnsureExit() 51 | childRunner3.EnsureExit() 52 | 53 | Eventually(started).Should(BeClosed()) 54 | groupProcess.Signal(os.Kill) 55 | Eventually(groupProcess.Wait()).Should(Receive()) 56 | }) 57 | 58 | BeforeEach(func() { 59 | started = make(chan struct{}) 60 | groupProcess = ifrit.Background(groupRunner) 61 | go func() { 62 | select { 63 | case <-groupProcess.Ready(): 64 | case <-groupProcess.Wait(): 65 | } 66 | close(started) 67 | }() 68 | }) 69 | 70 | It("runs the first runner, then the second, then becomes ready", func() { 71 | Eventually(childRunner1.RunCallCount).Should(Equal(1)) 72 | Consistently(childRunner2.RunCallCount, Δ).Should(BeZero()) 73 | Consistently(started, Δ).ShouldNot(BeClosed()) 74 | 75 | childRunner1.TriggerReady() 76 | 77 | Eventually(childRunner2.RunCallCount).Should(Equal(1)) 78 | Consistently(childRunner3.RunCallCount, Δ).Should(BeZero()) 79 | Consistently(started, Δ).ShouldNot(BeClosed()) 80 | 81 | childRunner2.TriggerReady() 82 | 83 | Eventually(childRunner3.RunCallCount).Should(Equal(1)) 84 | Consistently(started, Δ).ShouldNot(BeClosed()) 85 | 86 | childRunner3.TriggerReady() 87 | 88 | Eventually(started).Should(BeClosed()) 89 | }) 90 | 91 | Describe("when all the runners are ready", func() { 92 | var ( 93 | signal2 <-chan os.Signal 94 | signal3 <-chan os.Signal 95 | ) 96 | 97 | BeforeEach(func() { 98 | childRunner1.WaitForCall() 99 | childRunner1.TriggerReady() 100 | signal2 = childRunner2.WaitForCall() 101 | childRunner2.TriggerReady() 102 | signal3 = childRunner3.WaitForCall() 103 | childRunner3.TriggerReady() 104 | 105 | Eventually(started).Should(BeClosed()) 106 | }) 107 | 108 | Describe("when it receives a signal", func() { 109 | BeforeEach(func() { 110 | groupProcess.Signal(syscall.SIGUSR2) 111 | }) 112 | 113 | It("doesn't send any more signals to remaining child processes", func() { 114 | Eventually(signal3).Should(Receive(Equal(syscall.SIGUSR2))) 115 | childRunner2.TriggerExit(nil) 116 | Consistently(signal3).ShouldNot(Receive()) 117 | }) 118 | }) 119 | 120 | Describe("when a process exits cleanly", func() { 121 | BeforeEach(func() { 122 | childRunner1.TriggerExit(nil) 123 | }) 124 | 125 | It("sends an interrupt signal to the other processes", func() { 126 | Eventually(signal3).Should(Receive(Equal(os.Interrupt))) 127 | childRunner3.TriggerExit(nil) 128 | Eventually(signal2).Should(Receive(Equal(os.Interrupt))) 129 | }) 130 | 131 | It("does not exit", func() { 132 | Consistently(groupProcess.Wait(), Δ).ShouldNot(Receive()) 133 | }) 134 | 135 | Describe("when another process exits", func() { 136 | BeforeEach(func() { 137 | childRunner2.TriggerExit(nil) 138 | }) 139 | 140 | It("doesn't send any more signals to remaining child processes", func() { 141 | Eventually(signal3).Should(Receive(Equal(os.Interrupt))) 142 | Consistently(signal3).ShouldNot(Receive()) 143 | }) 144 | }) 145 | 146 | Describe("when all of the processes have exited cleanly", func() { 147 | BeforeEach(func() { 148 | childRunner2.TriggerExit(nil) 149 | childRunner3.TriggerExit(nil) 150 | }) 151 | 152 | It("exits cleanly", func() { 153 | Eventually(groupProcess.Wait()).Should(Receive(BeNil())) 154 | }) 155 | }) 156 | 157 | Describe("when one of the processes exits with an error", func() { 158 | BeforeEach(func() { 159 | childRunner2.TriggerExit(errors.New("Fail")) 160 | childRunner3.TriggerExit(nil) 161 | }) 162 | 163 | It("returns an error indicating which child processes failed", func() { 164 | var err error 165 | Eventually(groupProcess.Wait()).Should(Receive(&err)) 166 | errTrace := err.(grouper.ErrorTrace) 167 | Ω(errTrace).Should(HaveLen(3)) 168 | 169 | Ω(errTrace).Should(ContainElement(grouper.ExitEvent{grouper.Member{"child1", childRunner1}, nil})) 170 | Ω(errTrace).Should(ContainElement(grouper.ExitEvent{grouper.Member{"child2", childRunner2}, errors.New("Fail")})) 171 | }) 172 | }) 173 | }) 174 | }) 175 | 176 | Describe("when the first member is started", func() { 177 | var signals <-chan os.Signal 178 | 179 | BeforeEach(func() { 180 | childRunner1.WaitForCall() 181 | childRunner1.TriggerReady() 182 | }) 183 | 184 | Describe("and the first member exits while second member is setting up", func() { 185 | BeforeEach(func() { 186 | signals = childRunner2.WaitForCall() 187 | childRunner1.TriggerExit(nil) 188 | }) 189 | 190 | It("should terminate", func() { 191 | var signal os.Signal 192 | Eventually(signals).Should(Receive(&signal)) 193 | Expect(signal).To(Equal(syscall.SIGINT)) 194 | }) 195 | }) 196 | 197 | Describe("and the second member exits before becoming ready", func() { 198 | BeforeEach(func() { 199 | signals = childRunner1.WaitForCall() 200 | childRunner2.TriggerExit(nil) 201 | }) 202 | 203 | It("should terminate the first runner", func() { 204 | var signal os.Signal 205 | Eventually(signals).Should(Receive(&signal)) 206 | Expect(signal).To(Equal(syscall.SIGINT)) 207 | childRunner1.TriggerExit(nil) 208 | var err error 209 | Eventually(groupProcess.Wait()).Should(Receive(&err)) 210 | Expect(err).NotTo(HaveOccurred()) 211 | }) 212 | }) 213 | }) 214 | 215 | Describe("Failed start", func() { 216 | BeforeEach(func() { 217 | signal1 := childRunner1.WaitForCall() 218 | childRunner1.TriggerReady() 219 | childRunner2.TriggerExit(errors.New("Fail")) 220 | Eventually(signal1).Should(Receive(Equal(os.Interrupt))) 221 | childRunner1.TriggerExit(nil) 222 | Eventually(started).Should(BeClosed()) 223 | }) 224 | 225 | It("exits without starting further processes", func() { 226 | var err error 227 | 228 | Eventually(groupProcess.Wait()).Should(Receive(&err)) 229 | errTrace := err.(grouper.ErrorTrace) 230 | Ω(errTrace).Should(ContainElement(grouper.ExitEvent{grouper.Member{"child1", childRunner1}, nil})) 231 | Ω(errTrace).Should(ContainElement(grouper.ExitEvent{grouper.Member{"child2", childRunner2}, errors.New("Fail")})) 232 | Ω(exitIndex("child1", errTrace)).Should(BeNumerically(">", exitIndex("child2", errTrace))) 233 | }) 234 | }) 235 | }) 236 | 237 | Describe("Stop", func() { 238 | 239 | var runnerIndex int64 240 | var startOrder chan int64 241 | var stopOrder chan int64 242 | var receivedSignals chan os.Signal 243 | 244 | makeRunner := func(waitTime time.Duration) (ifrit.Runner, chan struct{}) { 245 | quickExit := make(chan struct{}) 246 | return ifrit.RunFunc(func(signals <-chan os.Signal, ready chan<- struct{}) error { 247 | index := atomic.AddInt64(&runnerIndex, 1) 248 | startOrder <- index 249 | close(ready) 250 | 251 | select { 252 | case <-quickExit: 253 | case <-signals: 254 | } 255 | time.Sleep(waitTime) 256 | stopOrder <- index 257 | 258 | return nil 259 | }), quickExit 260 | } 261 | 262 | makeSignalEchoRunner := func(waitTime time.Duration, name string) ifrit.Runner { 263 | return ifrit.RunFunc(func(signals <-chan os.Signal, ready chan<- struct{}) error { 264 | close(ready) 265 | done := make(chan bool) 266 | go func() { 267 | time.Sleep(waitTime) 268 | done <- true 269 | }() 270 | L: 271 | for { 272 | select { 273 | case s := <-signals: 274 | receivedSignals <- s 275 | case _ = <-done: 276 | break L 277 | } 278 | } 279 | return nil 280 | }) 281 | } 282 | 283 | Context("when runner receives a single signal", func() { 284 | BeforeEach(func() { 285 | startOrder = make(chan int64, 3) 286 | stopOrder = make(chan int64, 3) 287 | 288 | r1, _ := makeRunner(0) 289 | r2, _ := makeRunner(30 * time.Millisecond) 290 | r3, _ := makeRunner(50 * time.Millisecond) 291 | members = grouper.Members{ 292 | {"child1", r1}, 293 | {"child2", r2}, 294 | {"child3", r3}, 295 | } 296 | }) 297 | 298 | AfterEach(func() { 299 | groupProcess.Signal(os.Kill) 300 | Eventually(groupProcess.Wait()).Should(Receive()) 301 | }) 302 | 303 | JustBeforeEach(func() { 304 | groupRunner = grouper.NewOrdered(os.Interrupt, members) 305 | 306 | started = make(chan struct{}) 307 | go func() { 308 | groupProcess = ifrit.Invoke(groupRunner) 309 | close(started) 310 | }() 311 | 312 | Eventually(started).Should(BeClosed()) 313 | }) 314 | 315 | It("stops in reverse order", func() { 316 | groupProcess.Signal(os.Kill) 317 | Eventually(groupProcess.Wait()).Should(Receive()) 318 | close(startOrder) 319 | close(stopOrder) 320 | 321 | Ω(startOrder).To(HaveLen(len(stopOrder))) 322 | 323 | order := []int64{} 324 | for r := range startOrder { 325 | order = append(order, r) 326 | } 327 | 328 | for i := len(stopOrder) - 1; i >= 0; i-- { 329 | Ω(order[i]).To(Equal(<-stopOrder)) 330 | } 331 | }) 332 | 333 | Context("when a runner stops", func() { 334 | var quickExit chan struct{} 335 | 336 | BeforeEach(func() { 337 | var r1 ifrit.Runner 338 | r1, quickExit = makeRunner(0) 339 | members[0].Runner = r1 340 | }) 341 | 342 | It("stops in reverse order", func() { 343 | close(quickExit) 344 | Eventually(groupProcess.Wait()).Should(Receive()) 345 | close(startOrder) 346 | close(stopOrder) 347 | 348 | Ω(startOrder).To(HaveLen(len(stopOrder))) 349 | 350 | order := []int64{} 351 | for r := range startOrder { 352 | order = append(order, r) 353 | } 354 | 355 | firstDeath := <-stopOrder 356 | for i := len(order) - 1; i >= 0; i-- { 357 | if order[i] == firstDeath { 358 | continue 359 | } 360 | Ω(order[i]).To(Equal(<-stopOrder)) 361 | } 362 | }) 363 | }) 364 | }) 365 | 366 | Context("when a runner receives multiple signals", func() { 367 | BeforeEach(func() { 368 | startOrder = make(chan int64, 2) 369 | stopOrder = make(chan int64, 2) 370 | 371 | r1 := makeSignalEchoRunner(200*time.Millisecond, "child1") 372 | r2 := makeSignalEchoRunner(100*time.Millisecond, "child2") 373 | members = grouper.Members{ 374 | {"child1", r1}, 375 | {"child2", r2}, 376 | } 377 | }) 378 | 379 | AfterEach(func() { 380 | groupProcess.Signal(os.Kill) 381 | Eventually(groupProcess.Wait()).Should(Receive()) 382 | }) 383 | 384 | JustBeforeEach(func() { 385 | groupRunner = grouper.NewOrdered(os.Interrupt, members) 386 | 387 | started = make(chan struct{}) 388 | go func() { 389 | groupProcess = ifrit.Invoke(groupRunner) 390 | close(started) 391 | }() 392 | 393 | Eventually(started).Should(BeClosed()) 394 | }) 395 | 396 | Context("of different types", func() { 397 | 398 | BeforeEach(func() { 399 | receivedSignals = make(chan os.Signal, 4) 400 | }) 401 | 402 | It("allows the process to finish gracefully", func() { 403 | groupProcess.Signal(syscall.SIGINT) 404 | Consistently(groupProcess.Wait(), 20*time.Millisecond, 10*time.Millisecond).ShouldNot(Receive()) 405 | groupProcess.Signal(syscall.SIGUSR1) 406 | Consistently(groupProcess.Wait(), 20*time.Millisecond, 10*time.Millisecond).ShouldNot(Receive()) 407 | groupProcess.Signal(syscall.SIGUSR2) 408 | Consistently(groupProcess.Wait(), 20*time.Millisecond, 10*time.Millisecond).ShouldNot(Receive()) 409 | 410 | Eventually(groupProcess.Wait()).Should(Receive()) 411 | 412 | signals := []os.Signal{syscall.SIGINT, syscall.SIGUSR1, syscall.SIGUSR2, syscall.SIGUSR2} 413 | for _, expectedSig := range signals { 414 | actualSig := <-receivedSignals 415 | Expect(actualSig).Should(Equal(expectedSig)) 416 | } 417 | }) 418 | }) 419 | 420 | Context("of same type", func() { 421 | 422 | BeforeEach(func() { 423 | receivedSignals = make(chan os.Signal, 2) 424 | }) 425 | 426 | It("allows the process to finish gracefully", func() { 427 | groupProcess.Signal(syscall.SIGUSR1) 428 | Consistently(groupProcess.Wait(), 20*time.Millisecond, 10*time.Millisecond).ShouldNot(Receive()) 429 | groupProcess.Signal(syscall.SIGUSR1) 430 | Consistently(groupProcess.Wait(), 20*time.Millisecond, 10*time.Millisecond).ShouldNot(Receive()) 431 | groupProcess.Signal(syscall.SIGUSR1) 432 | Consistently(groupProcess.Wait(), 20*time.Millisecond, 10*time.Millisecond).ShouldNot(Receive()) 433 | 434 | Eventually(groupProcess.Wait()).Should(Receive()) 435 | 436 | signals := []os.Signal{syscall.SIGUSR1, syscall.SIGUSR1} 437 | for _, expectedSig := range signals { 438 | actualSig := <-receivedSignals 439 | Expect(actualSig).Should(Equal(expectedSig)) 440 | } 441 | }) 442 | }) 443 | }) 444 | }) 445 | }) 446 | 447 | func exitIndex(name string, errTrace grouper.ErrorTrace) int { 448 | for i, exitTrace := range errTrace { 449 | if exitTrace.Member.Name == name { 450 | return i 451 | } 452 | } 453 | 454 | return -1 455 | } 456 | -------------------------------------------------------------------------------- /grouper/queue_ordered_test.go: -------------------------------------------------------------------------------- 1 | package grouper_test 2 | 3 | import ( 4 | "errors" 5 | "os" 6 | "sync/atomic" 7 | "syscall" 8 | "time" 9 | 10 | "github.com/tedsuo/ifrit" 11 | "github.com/tedsuo/ifrit/fake_runner" 12 | "github.com/tedsuo/ifrit/grouper" 13 | 14 | . "github.com/onsi/ginkgo" 15 | . "github.com/onsi/gomega" 16 | ) 17 | 18 | var _ = Describe("QueueQueued", func() { 19 | var ( 20 | started chan struct{} 21 | 22 | groupRunner ifrit.Runner 23 | groupProcess ifrit.Process 24 | members grouper.Members 25 | 26 | childRunner1 *fake_runner.TestRunner 27 | childRunner2 *fake_runner.TestRunner 28 | childRunner3 *fake_runner.TestRunner 29 | 30 | Δ time.Duration = 10 * time.Millisecond 31 | ) 32 | 33 | Describe("Start", func() { 34 | BeforeEach(func() { 35 | childRunner1 = fake_runner.NewTestRunner() 36 | childRunner2 = fake_runner.NewTestRunner() 37 | childRunner3 = fake_runner.NewTestRunner() 38 | 39 | members = grouper.Members{ 40 | {"child1", childRunner1}, 41 | {"child2", childRunner2}, 42 | {"child3", childRunner3}, 43 | } 44 | 45 | groupRunner = grouper.NewQueueOrdered(os.Interrupt, members) 46 | 47 | started = make(chan struct{}) 48 | groupProcess = ifrit.Background(groupRunner) 49 | go func() { 50 | select { 51 | case <-groupProcess.Ready(): 52 | case <-groupProcess.Wait(): 53 | } 54 | close(started) 55 | }() 56 | }) 57 | 58 | AfterEach(func() { 59 | childRunner1.EnsureExit() 60 | childRunner2.EnsureExit() 61 | childRunner3.EnsureExit() 62 | 63 | Eventually(started).Should(BeClosed()) 64 | groupProcess.Signal(os.Kill) 65 | Eventually(groupProcess.Wait()).Should(Receive()) 66 | }) 67 | 68 | It("runs the first runner, then the second, then the third, then becomes ready", func() { 69 | Eventually(childRunner1.RunCallCount).Should(Equal(1)) 70 | Consistently(childRunner2.RunCallCount, Δ).Should(BeZero()) 71 | Consistently(started, Δ).ShouldNot(BeClosed()) 72 | 73 | childRunner1.TriggerReady() 74 | 75 | Eventually(childRunner2.RunCallCount).Should(Equal(1)) 76 | Consistently(childRunner3.RunCallCount, Δ).Should(BeZero()) 77 | Consistently(started, Δ).ShouldNot(BeClosed()) 78 | 79 | childRunner2.TriggerReady() 80 | 81 | Eventually(childRunner3.RunCallCount).Should(Equal(1)) 82 | Consistently(started, Δ).ShouldNot(BeClosed()) 83 | 84 | childRunner3.TriggerReady() 85 | 86 | Eventually(started).Should(BeClosed()) 87 | }) 88 | 89 | Describe("when all the runners are ready", func() { 90 | var ( 91 | signal1 <-chan os.Signal 92 | signal2 <-chan os.Signal 93 | ) 94 | 95 | BeforeEach(func() { 96 | signal1 = childRunner1.WaitForCall() 97 | childRunner1.TriggerReady() 98 | signal2 = childRunner2.WaitForCall() 99 | childRunner2.TriggerReady() 100 | childRunner3.WaitForCall() 101 | childRunner3.TriggerReady() 102 | 103 | Eventually(started).Should(BeClosed()) 104 | }) 105 | 106 | Describe("when it receives a signal", func() { 107 | BeforeEach(func() { 108 | groupProcess.Signal(syscall.SIGUSR2) 109 | }) 110 | 111 | It("doesn't send any more signals to remaining child processes", func() { 112 | Eventually(signal1).Should(Receive(Equal(syscall.SIGUSR2))) 113 | childRunner1.TriggerExit(nil) 114 | Consistently(signal1).ShouldNot(Receive()) 115 | }) 116 | }) 117 | 118 | Describe("when a process exits cleanly", func() { 119 | BeforeEach(func() { 120 | // why start with 1?? in the original 121 | childRunner3.TriggerExit(nil) 122 | }) 123 | 124 | It("sends an interrupt signal to the other processes", func() { 125 | Eventually(signal1).Should(Receive(Equal(os.Interrupt))) 126 | childRunner1.TriggerExit(nil) 127 | Eventually(signal2).Should(Receive(Equal(os.Interrupt))) 128 | }) 129 | 130 | It("does not exit", func() { 131 | Consistently(groupProcess.Wait(), Δ).ShouldNot(Receive()) 132 | }) 133 | 134 | Describe("when another process exits", func() { 135 | BeforeEach(func() { 136 | childRunner2.TriggerExit(nil) 137 | }) 138 | 139 | It("doesn't send any more signals to remaining child processes", func() { 140 | Eventually(signal1).Should(Receive(Equal(os.Interrupt))) 141 | Consistently(signal1).ShouldNot(Receive()) 142 | }) 143 | }) 144 | 145 | Describe("when all of the processes have exited cleanly", func() { 146 | BeforeEach(func() { 147 | childRunner1.TriggerExit(nil) 148 | childRunner2.TriggerExit(nil) 149 | }) 150 | 151 | It("exits cleanly", func() { 152 | Eventually(groupProcess.Wait()).Should(Receive(BeNil())) 153 | }) 154 | }) 155 | 156 | Describe("when one of the processes exits with an error", func() { 157 | BeforeEach(func() { 158 | childRunner2.TriggerExit(errors.New("Fail")) 159 | childRunner1.TriggerExit(nil) 160 | }) 161 | 162 | It("returns an error indicating which child processes failed", func() { 163 | var err error 164 | Eventually(groupProcess.Wait()).Should(Receive(&err)) 165 | errTrace := err.(grouper.ErrorTrace) 166 | Ω(errTrace).Should(HaveLen(3)) 167 | 168 | Ω(errTrace).Should(ContainElement(grouper.ExitEvent{grouper.Member{"child1", childRunner1}, nil})) 169 | Ω(errTrace).Should(ContainElement(grouper.ExitEvent{grouper.Member{"child2", childRunner2}, errors.New("Fail")})) 170 | Ω(errTrace).Should(ContainElement(grouper.ExitEvent{grouper.Member{"child3", childRunner3}, nil})) 171 | }) 172 | }) 173 | }) 174 | }) 175 | 176 | Describe("when the first member is started", func() { 177 | var signals <-chan os.Signal 178 | 179 | BeforeEach(func() { 180 | childRunner1.WaitForCall() 181 | childRunner1.TriggerReady() 182 | }) 183 | 184 | Describe("and the first member exits while second member is setting up", func() { 185 | BeforeEach(func() { 186 | signals = childRunner2.WaitForCall() 187 | childRunner1.TriggerExit(nil) 188 | }) 189 | 190 | It("should terminate", func() { 191 | var signal os.Signal 192 | Eventually(signals).Should(Receive(&signal)) 193 | Expect(signal).To(Equal(syscall.SIGINT)) 194 | }) 195 | }) 196 | 197 | Describe("and the second member exits before becoming ready", func() { 198 | BeforeEach(func() { 199 | signals = childRunner1.WaitForCall() 200 | childRunner2.TriggerExit(nil) 201 | }) 202 | 203 | It("should terminate the first runner", func() { 204 | var signal os.Signal 205 | Eventually(signals).Should(Receive(&signal)) 206 | Expect(signal).To(Equal(syscall.SIGINT)) 207 | childRunner1.TriggerExit(nil) 208 | var err error 209 | Eventually(groupProcess.Wait()).Should(Receive(&err)) 210 | Expect(err).NotTo(HaveOccurred()) 211 | }) 212 | }) 213 | }) 214 | 215 | Describe("Failed start", func() { 216 | BeforeEach(func() { 217 | signal1 := childRunner1.WaitForCall() 218 | childRunner1.TriggerReady() 219 | childRunner2.TriggerExit(errors.New("Fail")) 220 | Eventually(signal1).Should(Receive(Equal(os.Interrupt))) 221 | childRunner1.TriggerExit(nil) 222 | Eventually(started).Should(BeClosed()) 223 | }) 224 | 225 | It("exits without starting further processes", func() { 226 | var err error 227 | 228 | Eventually(groupProcess.Wait()).Should(Receive(&err)) 229 | errTrace := err.(grouper.ErrorTrace) 230 | Ω(errTrace).Should(ContainElement(grouper.ExitEvent{grouper.Member{"child1", childRunner1}, nil})) 231 | Ω(errTrace).Should(ContainElement(grouper.ExitEvent{grouper.Member{"child2", childRunner2}, errors.New("Fail")})) 232 | Ω(exitIndex("child1", errTrace)).Should(BeNumerically(">", exitIndex("child2", errTrace))) 233 | }) 234 | }) 235 | }) 236 | 237 | Describe("Stop", func() { 238 | 239 | var runnerIndex int64 240 | var startOrder chan int64 241 | var stopOrder chan int64 242 | var receivedSignals chan os.Signal 243 | 244 | makeRunner := func(waitTime time.Duration) (ifrit.Runner, chan struct{}) { 245 | quickExit := make(chan struct{}) 246 | return ifrit.RunFunc(func(signals <-chan os.Signal, ready chan<- struct{}) error { 247 | index := atomic.AddInt64(&runnerIndex, 1) 248 | startOrder <- index 249 | close(ready) 250 | 251 | select { 252 | case <-quickExit: 253 | case <-signals: 254 | } 255 | time.Sleep(waitTime) 256 | stopOrder <- index 257 | 258 | return nil 259 | }), quickExit 260 | } 261 | 262 | makeSignalEchoRunner := func(waitTime time.Duration, name string) ifrit.Runner { 263 | return ifrit.RunFunc(func(signals <-chan os.Signal, ready chan<- struct{}) error { 264 | close(ready) 265 | done := make(chan bool) 266 | go func() { 267 | time.Sleep(waitTime) 268 | done <- true 269 | }() 270 | L: 271 | for { 272 | select { 273 | case s := <-signals: 274 | receivedSignals <- s 275 | case _ = <-done: 276 | break L 277 | } 278 | } 279 | return nil 280 | }) 281 | } 282 | 283 | Context("when runner receives a single signal", func() { 284 | BeforeEach(func() { 285 | startOrder = make(chan int64, 3) 286 | stopOrder = make(chan int64, 3) 287 | 288 | r1, _ := makeRunner(0) 289 | r2, _ := makeRunner(30 * time.Millisecond) 290 | r3, _ := makeRunner(50 * time.Millisecond) 291 | members = grouper.Members{ 292 | {"child1", r1}, 293 | {"child2", r2}, 294 | {"child3", r3}, 295 | } 296 | }) 297 | 298 | AfterEach(func() { 299 | groupProcess.Signal(os.Kill) 300 | Eventually(groupProcess.Wait()).Should(Receive()) 301 | }) 302 | 303 | JustBeforeEach(func() { 304 | groupRunner = grouper.NewQueueOrdered(os.Interrupt, members) 305 | 306 | started = make(chan struct{}) 307 | go func() { 308 | groupProcess = ifrit.Invoke(groupRunner) 309 | close(started) 310 | }() 311 | 312 | Eventually(started).Should(BeClosed()) 313 | }) 314 | 315 | It("stops in FIFO order", func() { 316 | groupProcess.Signal(os.Kill) 317 | Eventually(groupProcess.Wait()).Should(Receive()) 318 | close(startOrder) 319 | close(stopOrder) 320 | 321 | Ω(startOrder).To(HaveLen(len(stopOrder))) 322 | 323 | startOrderRunners := []int64{} 324 | for r := range startOrder { 325 | startOrderRunners = append(startOrderRunners, r) 326 | } 327 | stopOrderRunners := []int64{} 328 | for r := range stopOrder { 329 | stopOrderRunners = append(stopOrderRunners, r) 330 | } 331 | 332 | for i, runner := range stopOrderRunners { 333 | Ω(runner).To(Equal(startOrderRunners[i])) 334 | } 335 | }) 336 | 337 | Context("when a runner stops", func() { 338 | var quickExit chan struct{} 339 | 340 | BeforeEach(func() { 341 | var r1 ifrit.Runner 342 | r1, quickExit = makeRunner(0) 343 | members[0].Runner = r1 344 | }) 345 | 346 | It("stops in FIFO order", func() { 347 | close(quickExit) 348 | Eventually(groupProcess.Wait()).Should(Receive()) 349 | close(startOrder) 350 | close(stopOrder) 351 | 352 | Ω(startOrder).To(HaveLen(len(stopOrder))) 353 | 354 | firstDeath := <-startOrder 355 | 356 | startOrderRunners := []int64{firstDeath} 357 | for r := range startOrder { 358 | startOrderRunners = append(startOrderRunners, r) 359 | } 360 | stopOrderRunners := []int64{} 361 | for r := range stopOrder { 362 | stopOrderRunners = append(stopOrderRunners, r) 363 | } 364 | 365 | for i, runner := range stopOrderRunners { 366 | if runner == firstDeath { 367 | continue 368 | } 369 | Ω(runner).To(Equal(startOrderRunners[i])) 370 | } 371 | }) 372 | }) 373 | }) 374 | 375 | Context("when a runner receives multiple signals", func() { 376 | BeforeEach(func() { 377 | startOrder = make(chan int64, 2) 378 | stopOrder = make(chan int64, 2) 379 | 380 | r1 := makeSignalEchoRunner(100*time.Millisecond, "child1") 381 | r2 := makeSignalEchoRunner(200*time.Millisecond, "child2") 382 | members = grouper.Members{ 383 | {"child1", r1}, 384 | {"child2", r2}, 385 | } 386 | }) 387 | 388 | AfterEach(func() { 389 | groupProcess.Signal(os.Kill) 390 | Eventually(groupProcess.Wait()).Should(Receive()) 391 | }) 392 | 393 | JustBeforeEach(func() { 394 | groupRunner = grouper.NewQueueOrdered(os.Interrupt, members) 395 | 396 | started = make(chan struct{}) 397 | go func() { 398 | groupProcess = ifrit.Invoke(groupRunner) 399 | close(started) 400 | }() 401 | 402 | Eventually(started).Should(BeClosed()) 403 | }) 404 | 405 | Context("of different types", func() { 406 | 407 | BeforeEach(func() { 408 | receivedSignals = make(chan os.Signal, 4) // looks like it would redefine but actually resets the channel? 409 | }) 410 | 411 | It("allows the process to finish gracefully", func() { 412 | groupProcess.Signal(syscall.SIGINT) 413 | Consistently(groupProcess.Wait(), 20*time.Millisecond, 10*time.Millisecond).ShouldNot(Receive()) 414 | groupProcess.Signal(syscall.SIGUSR1) 415 | Consistently(groupProcess.Wait(), 20*time.Millisecond, 10*time.Millisecond).ShouldNot(Receive()) 416 | groupProcess.Signal(syscall.SIGUSR2) 417 | Consistently(groupProcess.Wait(), 20*time.Millisecond, 10*time.Millisecond).ShouldNot(Receive()) 418 | 419 | Eventually(groupProcess.Wait()).Should(Receive()) 420 | 421 | signals := []os.Signal{syscall.SIGINT, syscall.SIGUSR1, syscall.SIGUSR2, syscall.SIGUSR2} 422 | for _, expectedSig := range signals { 423 | Eventually(receivedSignals).Should(Receive(Equal(expectedSig))) 424 | } 425 | }) 426 | }) 427 | 428 | Context("of same type", func() { 429 | 430 | BeforeEach(func() { 431 | receivedSignals = make(chan os.Signal, 3) 432 | }) 433 | 434 | It("allows the process to finish gracefully", func() { 435 | groupProcess.Signal(syscall.SIGUSR1) 436 | Consistently(groupProcess.Wait(), 20*time.Millisecond, 10*time.Millisecond).ShouldNot(Receive()) 437 | groupProcess.Signal(syscall.SIGUSR1) 438 | Consistently(groupProcess.Wait(), 20*time.Millisecond, 10*time.Millisecond).ShouldNot(Receive()) 439 | groupProcess.Signal(syscall.SIGUSR1) 440 | Consistently(groupProcess.Wait(), 20*time.Millisecond, 10*time.Millisecond).ShouldNot(Receive()) 441 | 442 | Eventually(groupProcess.Wait()).Should(Receive()) 443 | 444 | signals := []os.Signal{syscall.SIGUSR1, syscall.SIGUSR1} 445 | for _, expectedSig := range signals { 446 | Eventually(receivedSignals).Should(Receive(Equal(expectedSig))) 447 | } 448 | }) 449 | }) 450 | }) 451 | }) 452 | }) 453 | --------------------------------------------------------------------------------