diff --git a/sink.go b/sink.go index bd57c51..7dbdb59 100644 --- a/sink.go +++ b/sink.go @@ -1,6 +1,7 @@ package keepcurrent import ( + "fmt" "io" "io/ioutil" "os" @@ -72,7 +73,22 @@ func ToChannel(ch chan []byte) Sink { return &byteChannel{ch} } -func (s *byteChannel) UpdateFrom(r io.Reader) error { +func (s *byteChannel) UpdateFrom(r io.Reader) (err error) { + // The channel is owned by the caller (see ToChannel) and may be closed + // concurrently — e.g. on shutdown or config reload — while a Runner is + // mid-sync. A send on a closed channel panics, which would crash the whole + // process; recover *only that specific panic* and surface it as a sink + // error instead (Runner reports sink errors via OnSinkError rather than + // dying). Any other panic is re-raised so genuine bugs still surface. + defer func() { + if rec := recover(); rec != nil { + if e, ok := rec.(error); ok && e.Error() == "send on closed channel" { + err = fmt.Errorf("byte channel sink: %w", e) + return + } + panic(rec) + } + }() b, err := ioutil.ReadAll(r) if err != nil { return err diff --git a/sink_test.go b/sink_test.go new file mode 100644 index 0000000..52b24a1 --- /dev/null +++ b/sink_test.go @@ -0,0 +1,32 @@ +package keepcurrent + +import ( + "bytes" + "testing" +) + +// Regression test for the "send on closed channel" crash: the channel is owned +// by the caller and may be closed (shutdown / config reload) while a Runner is +// mid-sync. UpdateFrom must surface that as an error rather than panicking and +// crashing the whole process. +func TestByteChannelClosedDoesNotPanic(t *testing.T) { + ch := make(chan []byte) + close(ch) + s := ToChannel(ch) + err := s.UpdateFrom(bytes.NewReader([]byte("hello"))) + if err == nil { + t.Fatal("expected an error sending to a closed channel, got nil") + } +} + +// A healthy send still delivers the data unchanged. +func TestByteChannelDelivers(t *testing.T) { + ch := make(chan []byte, 1) + s := ToChannel(ch) + if err := s.UpdateFrom(bytes.NewReader([]byte("hello"))); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got := <-ch; string(got) != "hello" { + t.Fatalf("got %q, want %q", got, "hello") + } +}