Skip to content

Commit de688c0

Browse files
igor-dvlostluck
andauthored
Close harness data/state channels and don't Recv on Send EOF (#40262)
* [Go SDK] Close data and state channels on harness exit * [Go SDK] Don't Recv on write after Send EOF * [Go SDK] Treat canceled StateChannel reads as closed * Document bugfix for Go harness issues Fixed harness leaking Data/State gRPC streams and a deadlock issue. * Fix empty critical section in tests --------- Co-authored-by: Robert Burke <lostluck@users.noreply.github.com>
1 parent 43bd833 commit de688c0

6 files changed

Lines changed: 238 additions & 30 deletions

File tree

‎CHANGES.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@
8282
## Bugfixes
8383

8484
* (Go) Fixed a data race on the Prism runner's artifact cache map in JobServices ([#32656](https://github.com/apache/beam/issues/32656)).
85+
* (Go) Fixed the harness leaking Data/State gRPC streams after the worker stops, and a deadlock when Send returns EOF ([#40260](https://github.com/apache/beam/issues/40260)).
8586
* (Java) Fixed the declared schema of the error output of the Kafka write SchemaTransform, which wrapped the error schema a second time and did not match the rows it emits ([#39760](https://github.com/apache/beam/issues/39760)).
8687
* (Go) Fixed pubsubio importing a `google.golang.org/genproto` package removed in recent releases, which broke builds of Go modules depending on a current `genproto` version ([#40018](https://github.com/apache/beam/issues/40018)).
8788
* (Java) BigQueryIO now treats a 404 when deleting a temporary table or dataset as success, so a replayed work item whose earlier attempt already deleted it no longer retries forever ([#24997](https://github.com/apache/beam/issues/24997)).

‎sdks/go/pkg/beam/core/runtime/harness/datamgr.go‎

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,20 @@ func (m *DataChannelManager) Open(ctx context.Context, port exec.Port) (*DataCha
144144
return ch, nil
145145
}
146146

147+
// Close closes all cached DataChannels.
148+
func (m *DataChannelManager) Close() {
149+
m.mu.Lock()
150+
chans := m.ports
151+
m.ports = nil
152+
m.mu.Unlock()
153+
for _, ch := range chans {
154+
ch.mu.Lock()
155+
ch.forceRecreate = nil
156+
ch.mu.Unlock()
157+
ch.cancelFn()
158+
}
159+
}
160+
147161
func (m *DataChannelManager) closeInstruction(instID instructionID, ports []exec.Port) error {
148162
m.mu.Lock()
149163
defer m.mu.Unlock()
@@ -562,13 +576,9 @@ type dataWriter struct {
562576
func (w *dataWriter) send(msg *fnpb.Elements) error {
563577
if err := w.ch.client.Send(msg); err != nil {
564578
if err == io.EOF {
565-
log.Warnf(context.TODO(), "dataWriter[%v;%v] EOF on send; fetching real error", w.id, w.ch.id)
566-
err = nil
567-
for err == nil {
568-
// Per GRPC stream documentation, if there's an EOF, we must call Recv
569-
// until a non-nil error is returned, to ensure resources are cleaned up.
570-
// https://pkg.go.dev/google.golang.org/grpc#ClientConn.NewStream
571-
_, err = w.ch.client.Recv()
579+
// Don't Recv here; the read loop owns the stream.
580+
if w.ch.readErr != nil {
581+
err = w.ch.readErr
572582
}
573583
}
574584
log.Warnf(context.TODO(), "dataWriter[%v;%v] error on send: %v", w.id, w.ch.id, err)
@@ -687,13 +697,9 @@ type timerWriter struct {
687697
func (w *timerWriter) send(msg *fnpb.Elements) error {
688698
if err := w.ch.client.Send(msg); err != nil {
689699
if err == io.EOF {
690-
log.Warnf(context.TODO(), "timerWriter[%v;%v] EOF on send; fetching real error", w.id, w.ch.id)
691-
err = nil
692-
for err == nil {
693-
// Per GRPC stream documentation, if there's an EOF, we must call Recv
694-
// until a non-nil error is returned, to ensure resources are cleaned up.
695-
// https://pkg.go.dev/google.golang.org/grpc#ClientConn.NewStream
696-
_, err = w.ch.client.Recv()
700+
// Don't Recv here; the read loop owns the stream.
701+
if w.ch.readErr != nil {
702+
err = w.ch.readErr
697703
}
698704
}
699705
log.Warnf(context.TODO(), "timerWriter[%v;%v] error on send: %v", w.id, w.ch.id, err)

‎sdks/go/pkg/beam/core/runtime/harness/datamgr_test.go‎

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424
"runtime"
2525
"strings"
2626
"sync"
27+
"sync/atomic"
2728
"testing"
2829
"time"
2930

@@ -543,6 +544,106 @@ func TestDataChannelTerminate_Writes(t *testing.T) {
543544
}
544545
}
545546

547+
type teardownDataClient struct{ block chan struct{} }
548+
549+
func (f *teardownDataClient) Recv() (*fnpb.Elements, error) { <-f.block; return nil, io.EOF }
550+
func (f *teardownDataClient) Send(*fnpb.Elements) error { return nil }
551+
552+
func TestDataChannelManagerClose(t *testing.T) {
553+
m := &DataChannelManager{ports: map[string]*DataChannel{}}
554+
var cancelled [2]atomic.Bool
555+
var unblock sync.Once
556+
block := make(chan struct{})
557+
for i := 0; i < 2; i++ {
558+
i := i
559+
ch := makeDataChannel(context.Background(), "port", &teardownDataClient{block: block}, func() {
560+
cancelled[i].Store(true)
561+
unblock.Do(func() { close(block) })
562+
})
563+
ch.forceRecreate = func(string, error) {
564+
m.mu.Lock()
565+
_ = m.ports
566+
m.mu.Unlock()
567+
}
568+
m.ports["p"+string(rune('0'+i))] = ch
569+
}
570+
571+
m.Close()
572+
573+
for i := range cancelled {
574+
if !cancelled[i].Load() {
575+
t.Errorf("channel %d not cancelled", i)
576+
}
577+
}
578+
m.mu.Lock()
579+
left := m.ports
580+
m.mu.Unlock()
581+
if left != nil {
582+
t.Error("ports not cleared")
583+
}
584+
m.Close()
585+
}
586+
587+
type eofOnSendClient struct {
588+
recvForever chan struct{}
589+
}
590+
591+
func (c *eofOnSendClient) Send(*fnpb.Elements) error { return io.EOF }
592+
func (c *eofOnSendClient) Recv() (*fnpb.Elements, error) {
593+
<-c.recvForever
594+
return nil, io.EOF
595+
}
596+
597+
func TestDataWriterSendEOF(t *testing.T) {
598+
ch := &DataChannel{
599+
id: "id",
600+
client: &eofOnSendClient{recvForever: make(chan struct{})},
601+
cancelFn: func() {},
602+
}
603+
w := &dataWriter{ch: ch, id: clientID{ptransformID: "pt", instID: "inst"}}
604+
605+
done := make(chan error, 1)
606+
go func() {
607+
ch.mu.Lock()
608+
defer ch.mu.Unlock()
609+
done <- w.send(&fnpb.Elements{})
610+
}()
611+
612+
select {
613+
case err := <-done:
614+
if err == nil {
615+
t.Fatal("send succeeded")
616+
}
617+
case <-time.After(5 * time.Second):
618+
t.Fatal("send blocked")
619+
}
620+
}
621+
622+
func TestTimerWriterSendEOF(t *testing.T) {
623+
ch := &DataChannel{
624+
id: "id",
625+
client: &eofOnSendClient{recvForever: make(chan struct{})},
626+
cancelFn: func() {},
627+
}
628+
w := &timerWriter{ch: ch, id: clientID{ptransformID: "pt", instID: "inst"}, timerFamilyID: "fam"}
629+
630+
done := make(chan error, 1)
631+
go func() {
632+
ch.mu.Lock()
633+
defer ch.mu.Unlock()
634+
done <- w.send(&fnpb.Elements{})
635+
}()
636+
637+
select {
638+
case err := <-done:
639+
if err == nil {
640+
t.Fatal("send succeeded")
641+
}
642+
case <-time.After(5 * time.Second):
643+
t.Fatal("send blocked")
644+
}
645+
}
646+
546647
type noopDataClient struct {
547648
}
548649

‎sdks/go/pkg/beam/core/runtime/harness/harness.go‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,8 @@ func MainWithOptions(ctx context.Context, loggingEndpoint, controlEndpoint strin
164164
runnerCapabilities: rcMap,
165165
elmTimeout: elmTimeout,
166166
}
167+
defer ctrl.data.Close()
168+
defer ctrl.state.Close()
167169

168170
if enabled, ok := rcMap[graphx.URNDataSampling]; ok && enabled {
169171
ctrl.dataSampler = exec.NewDataSampler(ctx)

‎sdks/go/pkg/beam/core/runtime/harness/statemgr.go‎

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -627,6 +627,20 @@ func (m *StateChannelManager) Open(ctx context.Context, port exec.Port) (*StateC
627627
return ch, nil
628628
}
629629

630+
// Close closes all cached StateChannels.
631+
func (m *StateChannelManager) Close() {
632+
m.mu.Lock()
633+
chans := m.ports
634+
m.ports = nil
635+
m.mu.Unlock()
636+
for _, ch := range chans {
637+
ch.mu.Lock()
638+
ch.forceRecreate = nil
639+
ch.mu.Unlock()
640+
ch.cancelFn()
641+
}
642+
}
643+
630644
type stateClient interface {
631645
Send(*fnpb.StateRequest) error
632646
Recv() (*fnpb.StateResponse, error)
@@ -704,7 +718,7 @@ func (c *StateChannel) read(ctx context.Context) {
704718
msg, err := c.client.Recv()
705719
if err != nil {
706720
c.terminateStreamOnError(err)
707-
if err == io.EOF {
721+
if err == io.EOF || status.Code(err) == codes.Canceled {
708722
log.Warnf(ctx, "StateChannel[%v].read: closed", c.id)
709723
return
710724
}
@@ -750,14 +764,13 @@ func (c *StateChannel) write(ctx context.Context) {
750764
}
751765

752766
if err == io.EOF {
753-
log.Warnf(ctx, "StateChannel[%v].write EOF on send; fetching real error", c.id)
754-
err = nil
755-
for err == nil {
756-
// Per GRPC stream documentation, if there's an EOF, we must call Recv
757-
// until a non-nil error is returned, to ensure resources are cleaned up.
758-
// https://pkg.go.dev/google.golang.org/grpc#ClientConn.NewStream
759-
_, err = c.client.Recv()
767+
// Don't Recv here; the read loop owns the stream.
768+
log.Warnf(ctx, "StateChannel[%v].write EOF on send", c.id)
769+
c.mu.Lock()
770+
if c.closedErr != nil {
771+
err = c.closedErr
760772
}
773+
c.mu.Unlock()
761774
}
762775
log.Errorf(ctx, "StateChannel[%v].write error on send: %v", c.id, err)
763776

‎sdks/go/pkg/beam/core/runtime/harness/statemgr_test.go‎

Lines changed: 93 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,14 @@ import (
2323
"log"
2424
"strings"
2525
"sync"
26+
"sync/atomic"
2627
"testing"
2728
"time"
2829

2930
"github.com/apache/beam/sdks/v2/go/pkg/beam/internal/errors"
3031
fnpb "github.com/apache/beam/sdks/v2/go/pkg/beam/model/fnexecution_v1"
32+
"google.golang.org/grpc/codes"
33+
"google.golang.org/grpc/status"
3134
)
3235

3336
// fakeStateClient replicates the call and response protocol
@@ -137,6 +140,21 @@ func TestStateChannel(t *testing.T) {
137140
},
138141
expectedErr: io.EOF,
139142
validateCancelled: true,
143+
}, {
144+
name: "readCanceled",
145+
caseFn: func(t *testing.T, c *StateChannel, client *fakeStateClient) error {
146+
go func() {
147+
req := <-client.send
148+
client.setRecvErr(status.Error(codes.Canceled, "context canceled"))
149+
client.recv <- &fnpb.StateResponse{
150+
Id: req.Id,
151+
}
152+
}()
153+
_, err := c.Send(&fnpb.StateRequest{})
154+
return err
155+
},
156+
expectedErr: status.Error(codes.Canceled, "context canceled"),
157+
validateCancelled: true,
140158
}, {
141159
name: "readOtherErr",
142160
caseFn: func(t *testing.T, c *StateChannel, client *fakeStateClient) error {
@@ -179,18 +197,12 @@ func TestStateChannel(t *testing.T) {
179197
caseFn: func(t *testing.T, c *StateChannel, client *fakeStateClient) error {
180198
go func() {
181199
client.setSendErr(io.EOF)
182-
req := <-client.send
183-
// This can be plumbed through on either side, write or read,
184-
// the important part is that we get it.
185-
client.setRecvErr(expectedError)
186-
client.recv <- &fnpb.StateResponse{
187-
Id: req.Id,
188-
}
200+
<-client.send
189201
}()
190202
_, err := c.Send(&fnpb.StateRequest{})
191203
return err
192204
},
193-
expectedErr: expectedError,
205+
expectedErr: io.EOF,
194206
validateCancelled: true,
195207
}, {
196208
name: "writeOtherError",
@@ -500,6 +512,79 @@ func TestStateKeyWriter(t *testing.T) {
500512
}
501513
}
502514

515+
type teardownStateClient struct{ block chan struct{} }
516+
517+
func (f *teardownStateClient) Recv() (*fnpb.StateResponse, error) {
518+
<-f.block
519+
return nil, status.Error(codes.Canceled, "context canceled")
520+
}
521+
func (f *teardownStateClient) Send(*fnpb.StateRequest) error { return nil }
522+
523+
func TestStateChannelManagerClose(t *testing.T) {
524+
m := &StateChannelManager{}
525+
block := make(chan struct{})
526+
var cancelled atomic.Bool
527+
var unblock sync.Once
528+
ch := makeStateChannel(context.Background(), "port", &teardownStateClient{block: block}, func() {
529+
cancelled.Store(true)
530+
unblock.Do(func() { close(block) })
531+
})
532+
ch.forceRecreate = func(string, error) {
533+
m.mu.Lock()
534+
_ = m.ports
535+
m.mu.Unlock()
536+
}
537+
m.ports = map[string]*StateChannel{"p": ch}
538+
539+
m.Close()
540+
541+
if !cancelled.Load() {
542+
t.Error("channel not cancelled")
543+
}
544+
m.mu.Lock()
545+
left := m.ports
546+
m.mu.Unlock()
547+
if left != nil {
548+
t.Error("ports not cleared")
549+
}
550+
m.Close()
551+
}
552+
553+
type eofOnSendStateClient struct {
554+
recvForever chan struct{}
555+
}
556+
557+
func (c *eofOnSendStateClient) Send(*fnpb.StateRequest) error { return io.EOF }
558+
func (c *eofOnSendStateClient) Recv() (*fnpb.StateResponse, error) {
559+
<-c.recvForever
560+
return nil, io.EOF
561+
}
562+
563+
func TestStateChannelWriteEOF(t *testing.T) {
564+
c := &StateChannel{
565+
id: "id",
566+
client: &eofOnSendStateClient{recvForever: make(chan struct{})},
567+
requests: make(chan *fnpb.StateRequest, 1),
568+
responses: make(map[string]chan<- *fnpb.StateResponse),
569+
cancelFn: func() {},
570+
DoneCh: make(chan struct{}),
571+
}
572+
c.responses["r1"] = make(chan *fnpb.StateResponse, 1)
573+
c.requests <- &fnpb.StateRequest{Id: "r1"}
574+
575+
done := make(chan struct{})
576+
go func() {
577+
c.write(context.Background())
578+
close(done)
579+
}()
580+
581+
select {
582+
case <-done:
583+
case <-time.After(5 * time.Second):
584+
t.Fatal("write blocked")
585+
}
586+
}
587+
503588
// This likely can't be replaced by the "errors" package helpers,
504589
// since we serialize errors in some cases.
505590
func contains(got, want error) bool {

0 commit comments

Comments
 (0)