Node: RunWithScissors should not hang on error if no listener (#2649)
* Node: RunwithScissors should not hang * Tests should check error text * Fix data race in tests * Add comments to explain the tests.
This commit is contained in:
parent
8a866c3c1d
commit
c8e18ba72c
|
@ -10,38 +10,56 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ScissorsErrors = promauto.NewCounterVec(
|
ScissorsErrorsCaught = promauto.NewCounterVec(
|
||||||
prometheus.CounterOpts{
|
prometheus.CounterOpts{
|
||||||
Name: "scissor_errors_caught",
|
Name: "scissor_errors_caught",
|
||||||
Help: "Total number of unhandled errors caught",
|
Help: "Total number of unhandled errors caught",
|
||||||
}, []string{"scissors", "name"})
|
}, []string{"name"})
|
||||||
|
ScissorsPanicsCaught = promauto.NewCounterVec(
|
||||||
|
prometheus.CounterOpts{
|
||||||
|
Name: "scissor_panics_caught",
|
||||||
|
Help: "Total number of panics caught",
|
||||||
|
}, []string{"name"})
|
||||||
)
|
)
|
||||||
|
|
||||||
// Start a go routine with recovering from any panic by sending an error to a error channel
|
// Start a go routine with recovering from any panic by sending an error to a error channel
|
||||||
func RunWithScissors(ctx context.Context, errC chan error, name string, runnable supervisor.Runnable) {
|
func RunWithScissors(ctx context.Context, errC chan error, name string, runnable supervisor.Runnable) {
|
||||||
ScissorsErrors.WithLabelValues("scissors", name).Add(0)
|
ScissorsErrorsCaught.WithLabelValues(name).Add(0)
|
||||||
|
ScissorsPanicsCaught.WithLabelValues(name).Add(0)
|
||||||
go func() {
|
go func() {
|
||||||
defer func() {
|
defer func() {
|
||||||
if r := recover(); r != nil {
|
if r := recover(); r != nil {
|
||||||
|
var err error
|
||||||
switch x := r.(type) {
|
switch x := r.(type) {
|
||||||
case error:
|
case error:
|
||||||
errC <- fmt.Errorf("%s: %w", name, x)
|
err = fmt.Errorf("%s: %w", name, x)
|
||||||
default:
|
default:
|
||||||
errC <- fmt.Errorf("%s: %v", name, x)
|
err = fmt.Errorf("%s: %v", name, x)
|
||||||
}
|
}
|
||||||
ScissorsErrors.WithLabelValues("scissors", name).Inc()
|
// We don't want this to hang if the listener has already gone away.
|
||||||
|
select {
|
||||||
|
case errC <- err:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
ScissorsPanicsCaught.WithLabelValues(name).Inc()
|
||||||
|
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
err := runnable(ctx)
|
err := runnable(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errC <- err
|
// We don't want this to hang if the listener has already gone away.
|
||||||
|
select {
|
||||||
|
case errC <- err:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
ScissorsErrorsCaught.WithLabelValues(name).Inc()
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
func WrapWithScissors(runnable supervisor.Runnable, name string) supervisor.Runnable {
|
func WrapWithScissors(runnable supervisor.Runnable, name string) supervisor.Runnable {
|
||||||
ScissorsErrors.WithLabelValues("scissors", name).Add(0)
|
ScissorsErrorsCaught.WithLabelValues(name).Add(0)
|
||||||
|
ScissorsPanicsCaught.WithLabelValues(name).Add(0)
|
||||||
return func(ctx context.Context) (result error) {
|
return func(ctx context.Context) (result error) {
|
||||||
defer func() {
|
defer func() {
|
||||||
if r := recover(); r != nil {
|
if r := recover(); r != nil {
|
||||||
|
@ -51,7 +69,7 @@ func WrapWithScissors(runnable supervisor.Runnable, name string) supervisor.Runn
|
||||||
default:
|
default:
|
||||||
result = fmt.Errorf("%s: %v", name, x)
|
result = fmt.Errorf("%s: %v", name, x)
|
||||||
}
|
}
|
||||||
ScissorsErrors.WithLabelValues("scissors", name).Inc()
|
ScissorsPanicsCaught.WithLabelValues(name).Inc()
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
|
|
@ -5,10 +5,23 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/test-go/testify/require"
|
||||||
|
|
||||||
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
dto "github.com/prometheus/client_model/go"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func getCounterValue(metric *prometheus.CounterVec, runnableName string) float64 {
|
||||||
|
var m = &dto.Metric{}
|
||||||
|
if err := metric.WithLabelValues(runnableName).Write(m); err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return m.Counter.GetValue()
|
||||||
|
}
|
||||||
|
|
||||||
func throwNil(ctx context.Context) error {
|
func throwNil(ctx context.Context) error {
|
||||||
var x *int = nil
|
var x *int = nil
|
||||||
*x = 5
|
*x = 5
|
||||||
|
@ -78,3 +91,115 @@ func TestSupervisor(t *testing.T) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRunWithScissorsCleanExit(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
errC := make(chan error)
|
||||||
|
|
||||||
|
itRan := make(chan bool, 1)
|
||||||
|
RunWithScissors(ctx, errC, "TestRunWithScissorsCleanExit", func(ctx context.Context) error {
|
||||||
|
itRan <- true
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
shouldHaveRun := <-itRan
|
||||||
|
require.Equal(t, true, shouldHaveRun)
|
||||||
|
|
||||||
|
// Need to wait a bit to make sure the scissors code completes without hanging.
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
|
||||||
|
assert.Equal(t, 0.0, getCounterValue(ScissorsErrorsCaught, "TestRunWithScissorsCleanExit"))
|
||||||
|
assert.Equal(t, 0.0, getCounterValue(ScissorsPanicsCaught, "TestRunWithScissorsCleanExit"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunWithScissorsPanicReturned(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
errC := make(chan error)
|
||||||
|
|
||||||
|
itRan := make(chan bool, 1)
|
||||||
|
RunWithScissors(ctx, errC, "TestRunWithScissorsPanicReturned", func(ctx context.Context) error {
|
||||||
|
itRan <- true
|
||||||
|
panic("Some random panic")
|
||||||
|
})
|
||||||
|
|
||||||
|
var err error
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
break
|
||||||
|
case err = <-errC:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
shouldHaveRun := <-itRan
|
||||||
|
require.Equal(t, true, shouldHaveRun)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Equal(t, "TestRunWithScissorsPanicReturned: Some random panic", err.Error())
|
||||||
|
assert.Equal(t, 0.0, getCounterValue(ScissorsErrorsCaught, "TestRunWithScissorsPanicReturned"))
|
||||||
|
assert.Equal(t, 1.0, getCounterValue(ScissorsPanicsCaught, "TestRunWithScissorsPanicReturned"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunWithScissorsPanicDoesNotBlockWhenNoListener(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
errC := make(chan error)
|
||||||
|
|
||||||
|
itRan := make(chan bool, 1)
|
||||||
|
RunWithScissors(ctx, errC, "TestRunWithScissorsPanicDoesNotBlockWhenNoListener", func(ctx context.Context) error {
|
||||||
|
itRan <- true
|
||||||
|
panic("Some random panic")
|
||||||
|
})
|
||||||
|
|
||||||
|
shouldHaveRun := <-itRan
|
||||||
|
require.Equal(t, true, shouldHaveRun)
|
||||||
|
|
||||||
|
// Need to wait a bit to make sure the scissors code completes without hanging.
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
|
||||||
|
assert.Equal(t, 0.0, getCounterValue(ScissorsErrorsCaught, "TestRunWithScissorsPanicDoesNotBlockWhenNoListener"))
|
||||||
|
assert.Equal(t, 1.0, getCounterValue(ScissorsPanicsCaught, "TestRunWithScissorsPanicDoesNotBlockWhenNoListener"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunWithScissorsErrorReturned(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
errC := make(chan error)
|
||||||
|
|
||||||
|
itRan := make(chan bool, 1)
|
||||||
|
RunWithScissors(ctx, errC, "TestRunWithScissorsErrorReturned", func(ctx context.Context) error {
|
||||||
|
itRan <- true
|
||||||
|
return fmt.Errorf("Some random error")
|
||||||
|
})
|
||||||
|
|
||||||
|
var err error
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
break
|
||||||
|
case err = <-errC:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
shouldHaveRun := <-itRan
|
||||||
|
require.Equal(t, true, shouldHaveRun)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Equal(t, "Some random error", err.Error())
|
||||||
|
assert.Equal(t, 1.0, getCounterValue(ScissorsErrorsCaught, "TestRunWithScissorsErrorReturned"))
|
||||||
|
assert.Equal(t, 0.0, getCounterValue(ScissorsPanicsCaught, "TestRunWithScissorsErrorReturned"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunWithScissorsErrorDoesNotBlockWhenNoListener(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
errC := make(chan error)
|
||||||
|
|
||||||
|
itRan := make(chan bool, 1)
|
||||||
|
RunWithScissors(ctx, errC, "TestRunWithScissorsErrorDoesNotBlockWhenNoListener", func(ctx context.Context) error {
|
||||||
|
itRan <- true
|
||||||
|
return fmt.Errorf("Some random error")
|
||||||
|
})
|
||||||
|
|
||||||
|
shouldHaveRun := <-itRan
|
||||||
|
require.Equal(t, true, shouldHaveRun)
|
||||||
|
|
||||||
|
// Need to wait a bit to make sure the scissors code completes without hanging.
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
|
||||||
|
assert.Equal(t, 1.0, getCounterValue(ScissorsErrorsCaught, "TestRunWithScissorsErrorDoesNotBlockWhenNoListener"))
|
||||||
|
assert.Equal(t, 0.0, getCounterValue(ScissorsPanicsCaught, "TestRunWithScissorsErrorDoesNotBlockWhenNoListener"))
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue