diff --git a/.github/workflows/push.yml b/.github/workflows/build.yml similarity index 55% rename from .github/workflows/push.yml rename to .github/workflows/build.yml index c03f5d5..6f643fa 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/build.yml @@ -1,23 +1,28 @@ name: build -on: [push, pull_request] +on: + push: + pull_request: jobs: - lint: + golangci: + name: lint runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: docker://golangci/golangci-lint:v1.31.0 - with: - args: golangci-lint run - env: - CGO_ENABLED: 0 + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version: stable + - name: golangci-lint + uses: golangci/golangci-lint-action@v6 + with: + version: v1.62.2 test: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: actions/setup-go@v1 + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 with: - go-version: 1.15 + go-version: 1.23.4 - name: Run Unit tests run: go test -covermode atomic -coverprofile=profile.cov $(go list -m)/... - name: Send coverage @@ -26,4 +31,3 @@ jobs: run: | GO111MODULE=off go get github.com/mattn/goveralls $(go env GOPATH)/bin/goveralls -coverprofile=profile.cov -service=github - diff --git a/.golangci.yaml b/.golangci.yaml new file mode 100644 index 0000000..0ffd4f4 --- /dev/null +++ b/.golangci.yaml @@ -0,0 +1,90 @@ +--- +run: + tests: false +linters-settings: + dupl: + threshold: 100 + funlen: + lines: 100 + statements: 50 + goconst: + min-len: 2 + min-occurrences: 2 + gocritic: + enabled-tags: + - diagnostic + - experimental + - opinionated + - performance + - style + disabled-checks: + - hugeParam + gocyclo: + min-complexity: 15 + revive: + confidence: 0.8 + lll: + line-length: 140 + misspell: + locale: US + nolintlint: + require-explanation: true + allow-no-explanation: + - gocognit + - funlen + - gocyclo + +linters: + disable-all: true + enable: + - bodyclose + - dogsled + - dupl + - errcheck + - funlen + - nolintlint + - gochecknoglobals + - gochecknoinits + - gocognit + - goconst + - gocritic + - gocyclo + - gofmt + - goimports + - revive + - goprintffuncname + - gosec + - gosimple + - govet + - ineffassign + - misspell + - nakedret + - prealloc + - protogetter + - rowserrcheck + - copyloopvar + - staticcheck + - stylecheck + - sqlclosecheck + - typecheck + - unconvert + - unparam + - unused + - whitespace + - wsl + +issues: + exclude: + # Very commonly not checked. + - 'Error return value of .((os\.)?std(out|err)\..*|.*Close|.*Flush|os\.Remove(All)?|.*print(f|ln)?|os\.(Un)?Setenv). is not checked' + - 'G104:.*' + - 'exported method (.*\.MarshalJSON|.*\.UnmarshalJSON|.*\.MarshalText|.*\.UnmarshalText|.*\.LogValue|.*\.MarshalLogObject) should have comment or be unexported' + - 'shadow: declaration of "err" shadows declaration.*' + max-same-issues: 0 + exclude-use-default: false + exclude-dirs: + - .github + - build + - web + - .go + - vendor \ No newline at end of file diff --git a/README.md b/README.md index 83b3bb0..49835a2 100644 --- a/README.md +++ b/README.md @@ -1,30 +1,14 @@ [![Go Report Card](https://goreportcard.com/badge/github.com/postfinance/profiler)](https://goreportcard.com/report/github.com/postfinance/profiler) [![GoDoc](https://godoc.org/github.com/postfinance/profiler?status.svg)](https://godoc.org/github.com/postfinance/profiler) -[![Build Status](https://github.com/postfinance/profiler/workflows/build/badge.svg)](https://github.com/postfinance/profiler/actions) -[![Coverage Status](https://coveralls.io/repos/github/postfinance/profiler/badge.svg?branch=master)](https://coveralls.io/github/postfinance/profiler?branch=master) - - - - -**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* - -- [profiler](#profiler) - - [Usage](#usage) - - [Start the pprof endpoint](#start-the-pprof-endpoint) - - [Collect pprof data](#collect-pprof-data) - - [Usage with kubernetes services](#usage-with-kubernetes-services) - - [Start the pprof endpoint](#start-the-pprof-endpoint-1) - - [Check log](#check-log) - - [Port-forward](#port-forward) - - [Collect pprof data](#collect-pprof-data-1) - - +[![Build](https://github.com/postfinance/profiler/actions/workflows/build.yml/badge.svg)](https://github.com/postfinance/profiler/actions/workflows/build.yml) +[![Coverage](https://coveralls.io/repos/github/postfinance/profiler/badge.svg?branch=master)](https://coveralls.io/github/postfinance/profiler?branch=master) # profiler ## Usage Add the following line to your Go code: + ```go // create and start the profiler handler profiler.New().Start() @@ -37,56 +21,73 @@ profiler.New( ) ``` -Defaults: -- Signal *HUP* -- Listen *:6666* -- Timeout *10m* +## Defaults + +| Parameter | Default | +|-----------|---------| +| Signal | *HUP* | +| Listen | *:6666* | +| Timeout | *10m* | ### Start the pprof endpoint -```bash + +```shell pkill -HUP ``` -After *timeout* the endpoint will shutdown. + +> After *timeout* the endpoint will shutdown. ### Collect pprof data -```bash + +```shell go tool pprof -http $(hostname):8080 http://localhost:6666/debug/pprof/profile ``` -## Usage with kubernetes services +... or ... + +```shell +go tool pprof -http localhost:7007 http://localhost:8080/debug/pprof/profile +``` + +## Kubernetes ### Start the pprof endpoint -```bash -$ k get pods + +```shell +kubectl get pods NAME READY STATUS RESTARTS AGE ... -$ k exec -ti sh +kubectl exec -ti sh / # pkill -HUP / # ``` -After *timeout* the endpoint will shutdown. +> After *timeout* the endpoint will shutdown. ### Check log -```bash -$ k logs -f -... -2020/02/10 16:37:09 start pprof endpoint on ":6666" -... + +```shell +kubectl logs -f | grep 'start debug endpoint' ``` ### Port-forward -```bash -$ k port-forward 8080:6666 + +```shell +kubectl port-forward 8080:6666 Forwarding from 127.0.0.1:8080 -> 6666 Forwarding from [::1]:8080 -> 6666 Handling connection for 8080 ``` ### Collect pprof data -```bash -$ go tool pprof -http $(hostname):8888 http://localhost:8080/debug/pprof/profile + +```shell +go tool pprof -http $(hostname):8888 http://localhost:8080/debug/pprof/profile ``` +... or ... +```shell +go tool pprof -http localhost:7007 http://localhost:8080/debug/pprof/profile +``` diff --git a/go.mod b/go.mod index 07bc154..5050014 100644 --- a/go.mod +++ b/go.mod @@ -1,5 +1,11 @@ module github.com/postfinance/profiler -go 1.15 +go 1.23 require github.com/stretchr/testify v1.6.1 + +require ( + github.com/davecgh/go-spew v1.1.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect +) diff --git a/go.sum b/go.sum index 1f1e7af..afe7890 100644 --- a/go.sum +++ b/go.sum @@ -2,7 +2,6 @@ github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= diff --git a/options.go b/options.go new file mode 100644 index 0000000..dc9f543 --- /dev/null +++ b/options.go @@ -0,0 +1,44 @@ +package profiler + +import ( + "os" + "time" +) + +// Option is a Profiler functional option +type Option func(*Profiler) + +// WithSignal sets the signal to activate the pprof handler +func WithSignal(s os.Signal) Option { + return func(p *Profiler) { + p.signal = s + } +} + +// WithAddress sets the listen address of the pprof handler +func WithAddress(address string) Option { + return func(p *Profiler) { + p.address = address + } +} + +// WithTimeout sets the timeout after the pprof handler will be shutdown +func WithTimeout(timeout time.Duration) Option { + return func(p *Profiler) { + p.timeout = timeout + } +} + +// WithEventHandler registers a custom event handler +func WithEventHandler(evt EventHandler) Option { + return func(p *Profiler) { + p.evt = evt + } +} + +// WithHooks registers the Profiler hooks +func WithHooks(hooks ...Hooker) Option { + return func(p *Profiler) { + p.hooks = append(p.hooks, hooks...) + } +} diff --git a/profiler.go b/profiler.go index 6c7d52c..4449e51 100644 --- a/profiler.go +++ b/profiler.go @@ -3,79 +3,44 @@ package profiler import ( "context" + "errors" + "expvar" "log" "net/http" "os" "os/signal" "sync" + "sync/atomic" "syscall" "time" - // nolint: gosec // G108: Profiling endpoint is automatically exposed on /debug/pprof - _ "net/http/pprof" // normally pprof will be imported in the main package + "net/http/pprof" ) -// nolint: gochecknoglobals -var ( - pprofmux *http.ServeMux -) - -// nolint: gochecknoinits -func init() { - pprofmux = http.DefaultServeMux - http.DefaultServeMux = http.NewServeMux() -} +// EventHandler function to handle events +type EventHandler func(v string, args ...any) // Hooker represents the interface for Profiler hooks type Hooker interface { - // PreStart will be executed after the signal was received but before the pprof endpoint starts + // PreStart will be executed after the signal was received but before the debug endpoint starts PreStart() - // PostShutdown will be executed after the pprof endpoint is shutdown + // PostShutdown will be executed after the debug endpoint is shutdown or the start has failed PostShutdown() } -// Profiler represents profiling +// ============================================================================= + +// Profiler represents the Profiler type Profiler struct { - sync.Mutex signal os.Signal address string timeout time.Duration hooks []Hooker - stop chan struct{} - done chan struct{} - once *sync.Once -} - -// Opt are Profiler functional options -type Opt func(*Profiler) - -// WithSignal sets the signal to aktivate the pprof handler -func WithSignal(s os.Signal) Opt { - return func(p *Profiler) { - p.signal = s - } -} - -// WithAddress sets the listen address of the pprof handler -func WithAddress(address string) Opt { - return func(p *Profiler) { - p.address = address - } -} - -// WithTimeout sets the timeout after the pprof handler will be shutdown -func WithTimeout(timeout time.Duration) Opt { - return func(p *Profiler) { - p.timeout = timeout - } -} - -// WithHooks registers the Profiler hooks -func WithHooks(hooks ...Hooker) Opt { - return func(p *Profiler) { - p.hooks = append(p.hooks, hooks...) - } + started *atomic.Int32 + running sync.Mutex + stopC chan struct{} + evt EventHandler } // New returns a new profiler @@ -83,136 +48,168 @@ func WithHooks(hooks ...Hooker) Opt { // - Signal : syscall.SIGHUP // - Address: ":6666" // - Timeout: 10m -func New(opts ...Opt) *Profiler { +func New(options ...Option) *Profiler { p := &Profiler{ signal: syscall.SIGHUP, address: ":6666", timeout: 10 * time.Minute, - stop: make(chan struct{}), - done: make(chan struct{}), - once: new(sync.Once), + + started: new(atomic.Int32), + stopC: make(chan struct{}), + evt: func(msg string, args ...any) { + log.Println(append([]any{msg}, args...)...) + }, } - for _, opt := range opts { - opt(p) + for _, option := range options { + option(p) } return p } -// Address returns the listen address for the pprof endpoint +// Address returns the listen address for the debug endpoint func (p *Profiler) Address() string { return p.address } -// Start the pprof signal handler +// Start the profiler signal handler +// After the first call, subsequent calls +// to Start do nothing until Stop is called. func (p *Profiler) Start() { - go func() { - p.once.Do(p.handler) - }() + if p.started.CompareAndSwap(0, 1) { + go p.start() + } } -// Stop the pprof signal handler +// Stop the profiler signal handler +// After the first call, subsequent calls +// to Stop do nothing until Start is called. func (p *Profiler) Stop() { - p.stop <- struct{}{} - <-p.done - p.reset() + if p.started.CompareAndSwap(1, 0) { + p.stopC <- struct{}{} + } } -func (p *Profiler) reset() { - p.Lock() - p.once = new(sync.Once) // reset sync.Once for a subsequent call to Start - p.Unlock() -} +// ============================================================================= -func (p *Profiler) handler() { - log.Printf("start profiler handler - pprof endpoint will be started on signal: %v", p.signal) +func (p *Profiler) start() { + p.running.Lock() + defer p.running.Unlock() - defer log.Println("profiler handler stopped") + p.evt("start profiler signal handler", "signal", p.signal) + defer p.evt("profiler signal handler stopped") - sig := make(chan os.Signal, 1) + sigC := make(chan os.Signal, 1) + wg := new(sync.WaitGroup) + ctx, cancel := context.WithCancel(context.Background()) for { - // signal handling - signal.Notify(sig, p.signal) + signal.Notify(sigC, p.signal) + select { - case <-sig: - disableSignals(sig) - case <-p.stop: - disableSignals(sig) - p.done <- struct{}{} + case <-sigC: // receive signal to start the debug endpoint + disableSignals(sigC) - return - } - // start the pprof endpoint - shutdown := make(chan struct{}) - srv := &http.Server{ - Addr: p.address, - Handler: pprofmux, - } + wg.Add(1) + p.startEndpoint(ctx) + wg.Done() + case <-p.stopC: // stop the signal handler + p.evt("stop profiler signal handler", "signal", p.signal) - go func() { - log.Printf("start pprof endpoint on %q\n", p.address) - // execute the PreStart hooks - for _, h := range p.hooks { - h.PreStart() - } - - if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { - log.Println("failed to start pprof endpoint:", err) - } else { - log.Println("pprof endpoint stopped") - } - // execute the PostShutdown hooks ... even after a failed startup - for _, h := range p.hooks { - h.PostShutdown() - } - - close(shutdown) - }() - // - timer := time.NewTimer(p.timeout) - select { - case <-timer.C: // timer expired - shutdownEndpoint(srv, p.timeout) - <-shutdown - case <-shutdown: // start of endpoint failed - if !timer.Stop() { - <-timer.C - } - case <-p.stop: // stop requested - if !timer.Stop() { - <-timer.C - } - - shutdownEndpoint(srv, p.timeout) - <-shutdown - p.done <- struct{}{} + disableSignals(sigC) + + // stop the endpoint (if running) and + // wait until the endpoint is stopped + cancel() + wg.Wait() return } } } -// disableSignals stop receiving of signals and drain the signal channel -func disableSignals(c chan os.Signal) { - signal.Stop(c) - // drain signal channel - select { - case <-c: - default: +// startEndpoint starts the debug http endpoint +func (p *Profiler) startEndpoint(ctx context.Context) { + shutdown := make(chan struct{}) + + srv := &http.Server{ + Addr: p.address, + Handler: standardLibraryMux(), + ReadTimeout: 5 * time.Second, + WriteTimeout: 10 * time.Second, } -} -// shutdownEndpoint shutdown the http server graceful -func shutdownEndpoint(srv *http.Server, timeout time.Duration) { - log.Printf("shutdown pprof endpoint on %q\n", srv.Addr) + go func() { + p.evt("start debug endpoint", "address", p.address) + defer p.evt("debug endpoint stopped") + // execute the PreStart hooks + for _, h := range p.hooks { + h.PreStart() + } - ctx, cancel := context.WithTimeout(context.Background(), timeout) + if err := srv.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) { + p.evt("ERROR: start debug endpoint", "err", err) + } else { + p.evt("debug endpoint stopped") + } + + // execute the PostShutdown hooks ... even after a failed startup + for _, h := range p.hooks { + h.PostShutdown() + } + close(shutdown) + }() + + timer := time.NewTimer(p.timeout) + + select { + case <-timer.C: // timer expired + case <-ctx.Done(): // context canceled + timer.Stop() + } + + p.evt("shutdown debug endpoint", "address", p.address, "timeout", p.timeout) + + sCtx, cancel := context.WithTimeout(context.Background(), p.timeout) defer cancel() - if err := srv.Shutdown(ctx); err != nil { - log.Println("failed to shutdown pprof endpoint:", err) + if err := srv.Shutdown(sCtx); err != nil { + p.evt("ERROR: shutdown debug endpoint", "err", err) + } + + <-shutdown +} + +// ============================================================================= + +// standardLibraryMux registers all the debug routes from the standard library +// into a new mux bypassing the use of the DefaultServerMux. Using the +// DefaultServerMux would be a security risk since a dependency could inject a +// handler into our service without us knowing it. +// +// Source: https://github.com/ardanlabs/service4.1-video/blob/main/business/web/v1/debug/debug.go +func standardLibraryMux() *http.ServeMux { + mux := http.NewServeMux() + + mux.HandleFunc("/debug/pprof/", pprof.Index) + mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline) + mux.HandleFunc("/debug/pprof/profile", pprof.Profile) + mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol) + mux.HandleFunc("/debug/pprof/trace", pprof.Trace) + mux.Handle("/debug/vars", expvar.Handler()) + + return mux +} + +// disableSignals stop receiving of signals and drain the signal channel +func disableSignals(sigC chan os.Signal) { + signal.Stop(sigC) + + // drain signal channel + select { + case <-sigC: + default: } } diff --git a/profiler_test.go b/profiler_test.go index a92125d..b1e0045 100644 --- a/profiler_test.go +++ b/profiler_test.go @@ -1,11 +1,15 @@ package profiler_test import ( + "bytes" + "encoding/json" + "expvar" "fmt" "log" "net" "net/http" "os" + "strings" "sync" "syscall" "testing" @@ -26,34 +30,58 @@ func TestMain(m *testing.M) { os.Exit(m.Run()) } -func testProfiler(t *testing.T, p *profiler.Profiler, success bool) { +func testAddress(t *testing.T) string { + // get a free port + l, _ := net.Listen("tcp", "") + _, port, err := net.SplitHostPort(l.Addr().String()) + require.NoError(t, err) + require.NoError(t, l.Close()) + + return fmt.Sprintf("localhost:%s", port) +} + +func testProfiler(t *testing.T, + p *profiler.Profiler, + ep string, + success bool, + checkBody func(t *testing.T, body []byte), +) { p.Start() - time.Sleep(1 * time.Second) // wait until the setup is done + time.Sleep(100 * time.Millisecond) // switch goroutine assert.NoError(t, syscall.Kill(syscall.Getpid(), signal)) - time.Sleep(1 * time.Second) // wait until the signal is processed + time.Sleep(100 * time.Millisecond) // switch goroutine client := http.Client{ Timeout: 10 * time.Millisecond, } - resp, err := client.Get(fmt.Sprintf("http://%s", p.Address())) + if ep == "" { + ep = "/debug/pprof" + + if checkBody == nil { + checkBody = func(t *testing.T, b []byte) { + require.Contains(t, string(b), "/debug/pprof/") + } + } + } + + resp, err := client.Get(fmt.Sprintf("http://%s%s", p.Address(), ep)) assert.Equal(t, err == nil, success) - if resp != nil { + if resp != nil && resp.Body != nil { + var buf bytes.Buffer + + buf.ReadFrom(resp.Body) _ = resp.Body.Close() + + checkBody(t, buf.Bytes()) } p.Stop() } func TestStart(t *testing.T) { - // get a free port - l, _ := net.Listen("tcp", "") - _, port, err := net.SplitHostPort(l.Addr().String()) - assert.NoError(t, err) - assert.NoError(t, l.Close()) - - address := fmt.Sprintf("localhost:%s", port) + address := testAddress(t) p := profiler.New( profiler.WithSignal(signal), @@ -62,29 +90,127 @@ func TestStart(t *testing.T) { ) require.NotNil(t, p) - testProfiler(t, p, true) + testProfiler(t, p, "", true, nil) } func TestRestart(t *testing.T) { - // get a free port - l, _ := net.Listen("tcp", "") - _, port, err := net.SplitHostPort(l.Addr().String()) - assert.NoError(t, err) - assert.NoError(t, l.Close()) + address := testAddress(t) - address := fmt.Sprintf("localhost:%s", port) + p := profiler.New( + profiler.WithSignal(signal), + profiler.WithAddress(address), + profiler.WithTimeout(timeout), + ) + require.NotNil(t, p) + + testProfiler(t, p, "", true, nil) + testProfiler(t, p, "", true, nil) +} + +func TestFastRestart(t *testing.T) { + address := testAddress(t) + + startEvent := 0 + stopEvent := 0 p := profiler.New( profiler.WithSignal(signal), profiler.WithAddress(address), profiler.WithTimeout(timeout), + profiler.WithEventHandler(func(msg string, args ...any) { + if strings.Contains(msg, "start profiler signal handler") { + startEvent++ + } + if strings.Contains(msg, "stop profiler signal handler") { + stopEvent++ + } + }), ) require.NotNil(t, p) - testProfiler(t, p, true) - testProfiler(t, p, true) + p.Start() + p.Stop() + p.Start() + p.Stop() + time.Sleep(100 * time.Millisecond) // switch goroutine + + require.Equal(t, 2, startEvent) + require.Equal(t, 2, stopEvent) } +func TestMultipleStartStop(t *testing.T) { + address := testAddress(t) + + startEvent := 0 + stopEvent := 0 + + p := profiler.New( + profiler.WithSignal(signal), + profiler.WithAddress(address), + profiler.WithTimeout(timeout), + profiler.WithEventHandler(func(msg string, args ...any) { + if strings.Contains(msg, "start profiler signal handler") { + startEvent++ + } + if strings.Contains(msg, "stop profiler signal handler") { + stopEvent++ + } + }), + ) + require.NotNil(t, p) + + p.Start() + p.Start() + time.Sleep(100 * time.Millisecond) // switch goroutine + + require.Equal(t, 1, startEvent) + require.Equal(t, 0, stopEvent) + + p.Stop() + p.Stop() + time.Sleep(100 * time.Millisecond) // switch goroutine + + require.Equal(t, 1, startEvent) + require.Equal(t, 1, stopEvent) + + p.Start() + p.Start() + time.Sleep(100 * time.Millisecond) // switch goroutine + + require.Equal(t, 2, startEvent) + require.Equal(t, 1, stopEvent) + + p.Stop() + p.Stop() + time.Sleep(100 * time.Millisecond) // switch goroutine + + require.Equal(t, 2, startEvent) + require.Equal(t, 2, stopEvent) +} + +func TestExpvars(t *testing.T) { + hello := expvar.NewString("hello") + hello.Set("world") + + address := testAddress(t) + + p := profiler.New( + profiler.WithSignal(signal), + profiler.WithAddress(address), + profiler.WithTimeout(timeout), + ) + require.NotNil(t, p) + + testProfiler(t, p, "/debug/vars", true, func(t *testing.T, body []byte) { + m := make(map[string]any) + require.NoError(t, json.Unmarshal(body, &m)) + require.Equal(t, "world", m["hello"].(string)) + t.Log("hello", m["hello"].(string)) + }) +} + +// ============================================================================= + type TestHookOne struct { sync.Mutex PreStartupTriggered bool @@ -152,14 +278,9 @@ func (tht *TestHookTwo) HasPostShutdownTriggered() bool { return tht.PostShutdownTriggered } -func TestWithHooks(t *testing.T) { - // get a free port - l, _ := net.Listen("tcp", "") - _, port, err := net.SplitHostPort(l.Addr().String()) - assert.NoError(t, err) - assert.NoError(t, l.Close()) - address := fmt.Sprintf("localhost:%s", port) +func TestWithHooks(t *testing.T) { + address := testAddress(t) one := &TestHookOne{} two := &TestHookTwo{} @@ -173,9 +294,9 @@ func TestWithHooks(t *testing.T) { require.NotNil(t, p) p.Start() - time.Sleep(1 * time.Second) // wait until the setup is done + time.Sleep(100 * time.Millisecond) // switch goroutine assert.NoError(t, syscall.Kill(syscall.Getpid(), signal)) - time.Sleep(1 * time.Second) // wait until the signal is processed + time.Sleep(100 * time.Millisecond) // switch goroutine assert.True(t, one.HasPreStartupTriggered()) assert.True(t, two.HasPreStartupTriggered()) @@ -191,6 +312,8 @@ func TestWithHooks(t *testing.T) { assert.True(t, two.HasPostShutdownTriggered()) } +// ============================================================================= + type HookFailedStart struct { sync.Mutex Shutdown bool @@ -213,7 +336,6 @@ func (hfs *HookFailedStart) IsShutdown() bool { return hfs.Shutdown } - func TestFailedStart(t *testing.T) { // get a free port l, _ := net.Listen("tcp", "") @@ -235,6 +357,6 @@ func TestFailedStart(t *testing.T) { ) require.NotNil(t, p) - testProfiler(t, p, false) + testProfiler(t, p, "", false, nil) assert.True(t, fh.IsShutdown()) }