Skip to content

Commit ce77011

Browse files
committed
Retry writes that fail with EOF
Treat io.EOF as a transient network error so Writer retries writes interrupted by broker connection closures. Add focused error classification and Writer regression tests using a fake transport. Fixes #1352
1 parent 2e0b396 commit ce77011

3 files changed

Lines changed: 56 additions & 1 deletion

File tree

error.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -611,7 +611,8 @@ func isTemporary(err error) bool {
611611
}
612612

613613
func isTransientNetworkError(err error) bool {
614-
return errors.Is(err, io.ErrUnexpectedEOF) ||
614+
return errors.Is(err, io.EOF) ||
615+
errors.Is(err, io.ErrUnexpectedEOF) ||
615616
errors.Is(err, syscall.ECONNREFUSED) ||
616617
errors.Is(err, syscall.ECONNRESET) ||
617618
errors.Is(err, syscall.EPIPE)

error_test.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package kafka
22

33
import (
44
"fmt"
5+
"io"
56
"testing"
67

78
"github.com/stretchr/testify/assert"
@@ -125,3 +126,9 @@ func TestError(t *testing.T) {
125126
assert.ErrorIs(t, msgTooLarge, MessageSizeTooLarge)
126127
})
127128
}
129+
130+
func TestIsTransientNetworkErrorEOF(t *testing.T) {
131+
if !isTransientNetworkError(io.EOF) {
132+
t.Fatal("io.EOF should be treated as a transient network error")
133+
}
134+
}

writer_test.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,62 @@ import (
66
"fmt"
77
"io"
88
"math"
9+
"net"
910
"strconv"
1011
"strings"
1112
"sync"
1213
"testing"
1314
"time"
1415

16+
metadataAPI "github.com/segmentio/kafka-go/protocol/metadata"
17+
produceAPI "github.com/segmentio/kafka-go/protocol/produce"
18+
1519
"github.com/segmentio/kafka-go/sasl/plain"
1620
)
1721

22+
type roundTripperFunc func(context.Context, net.Addr, Request) (Response, error)
23+
24+
func (f roundTripperFunc) RoundTrip(ctx context.Context, addr net.Addr, req Request) (Response, error) {
25+
return f(ctx, addr, req)
26+
}
27+
28+
func TestWriterRetriesEOF(t *testing.T) {
29+
var produceCalls int
30+
transport := roundTripperFunc(func(ctx context.Context, addr net.Addr, req Request) (Response, error) {
31+
switch req.(type) {
32+
case *metadataAPI.Request:
33+
return &metadataAPI.Response{Topics: []metadataAPI.ResponseTopic{{Name: "topic", Partitions: []metadataAPI.ResponsePartition{{}}}}}, nil
34+
case *produceAPI.Request:
35+
produceCalls++
36+
if produceCalls == 1 {
37+
return nil, io.EOF
38+
}
39+
return &produceAPI.Response{Topics: []produceAPI.ResponseTopic{{Topic: "topic", Partitions: []produceAPI.ResponsePartition{{}}}}}, nil
40+
default:
41+
return nil, fmt.Errorf("unexpected request type %T", req)
42+
}
43+
})
44+
45+
w := &Writer{
46+
Addr: TCP("broker:9092"),
47+
Topic: "topic",
48+
Transport: transport,
49+
MaxAttempts: 2,
50+
BatchSize: 1,
51+
BatchTimeout: time.Hour,
52+
WriteBackoffMin: time.Nanosecond,
53+
WriteBackoffMax: time.Nanosecond,
54+
}
55+
defer w.Close()
56+
57+
if err := w.WriteMessages(context.Background(), Message{Value: []byte("value")}); err != nil {
58+
t.Fatalf("WriteMessages returned error: %v", err)
59+
}
60+
if produceCalls != 2 {
61+
t.Fatalf("Produce calls = %d, want 2", produceCalls)
62+
}
63+
}
64+
1865
func TestBatchQueue(t *testing.T) {
1966
tests := []struct {
2067
scenario string

0 commit comments

Comments
 (0)