Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
89e607b
fix(command): return tx queued error
feiguoL Jul 28, 2026
4b4e26e
fix(command): preserve tx execabort
feiguoL Jul 28, 2026
c3533c7
fix(command): drain tx exec replies
feiguoL Jul 28, 2026
f2e20c6
fix(command): drain tx exec errors
feiguoL Jul 30, 2026
1ea289e
fix(command): drain tx push replies
feiguoL Jul 30, 2026
bbf87f9
fix(command): keep tx failed reply
feiguoL Jul 30, 2026
d888096
fix(command): keep tx queued cause
feiguoL Jul 30, 2026
3014231
fix(command): keep tx exec context
feiguoL Jul 31, 2026
207932b
fix(command): expose tx queued read
feiguoL Jul 31, 2026
2027100
fix(command): keep tx drain cause
feiguoL Jul 31, 2026
feae6b8
fix(command): keep tx queue cause
feiguoL Jul 31, 2026
0cd6e5c
fix(command): clear tx watch on exec
feiguoL Jul 31, 2026
192a7ae
fix(conn): drain push on release
feiguoL Jul 31, 2026
1cbbde6
fix(conn): remove drain failures
feiguoL Jul 31, 2026
01f6967
fix(command): preserve tx array state
feiguoL Jul 31, 2026
335226c
fix(command): keep tx himport state
feiguoL Jul 31, 2026
7dffb58
fix(command): keep cluster himport
feiguoL Jul 31, 2026
d949387
fix(command): preserve tx queued state
feiguoL Jul 31, 2026
e30296c
fix(command): preserve tx drain state
feiguoL Jul 31, 2026
921301a
fix(command): preserve tx drain state
feiguoL Jul 31, 2026
c03f723
fix(command): keep tx read replies
feiguoL Jul 31, 2026
64b5cbd
fix(command): preserve tx handler state
feiguoL Jul 31, 2026
83716e1
fix(command): discard tx exec attrs
feiguoL Jul 31, 2026
78da465
fix(command): discard cluster exec attrs
feiguoL Jul 31, 2026
c419adb
fix(command): keep tx himport replies
feiguoL Jul 31, 2026
b227dc9
fix(command): discard tx mixed replies
feiguoL Jul 31, 2026
684026f
fix(command): reject negative exec array in cluster tx
feiguoL Aug 6, 2026
8e959ff
refactor(cluster): consolidate exec error into classifyExecError
feiguoL Aug 11, 2026
174b4d8
fix(command): stamp wrapped error on queued-reply read failure
feiguoL Aug 11, 2026
f61e9a1
fix(command): preserve queued error on non-array EXEC reply
feiguoL Aug 11, 2026
3d0619e
fix(command): preserve himport side effects on mid-drain failure
feiguoL Aug 11, 2026
e1d7b8c
fix(cluster): preserve queued root cause on queue-reply read failure
feiguoL Aug 11, 2026
4476568
fix(command): force bad conn on push drain and wrap malformed EXEC er…
feiguoL Aug 11, 2026
8aa040c
fix(cluster): preserve redirect and HIMPORT on malformed EXEC errors
feiguoL Aug 11, 2026
4b9e2e9
fix(cluster): don't retry after executed EXEC elements; carry HIMPORT…
feiguoL Aug 11, 2026
cde306f
fix(cluster): stop retry after EXEC array header
feiguoL Aug 12, 2026
a7b9713
fix(conn): remove desynced conn after push drain errors
feiguoL Aug 14, 2026
b62cc4b
ci: pin govulncheck to Go 1.26.6
feiguoL Aug 14, 2026
2f048b2
fix(conn): fail closed on remaining push-drain errors
feiguoL Aug 14, 2026
9e2f8db
fix(push): ignore empty buffered peek timeouts for void drain
feiguoL Aug 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/govulncheck.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v7
with:
go-version: "1.26.x"
go-version: "1.26.6"
cache: true

- name: Install govulncheck
Expand Down Expand Up @@ -66,4 +66,4 @@ jobs:
exit 1
fi

exit "$status"
exit "$status"
124 changes: 124 additions & 0 deletions csc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2367,6 +2367,101 @@ func TestReleaseConnRemovesConnectionAfterPartialPushRead(t *testing.T) {
}
}

func TestReleaseConnRemovesConnectionAfterReaderBufferedPartialPushRead(t *testing.T) {
cp := &releaseRecordingPool{}
c := &baseClient{
opt: &Options{Addr: "127.0.0.1:6379", Protocol: 3},
connPool: cp,
pushProcessor: push.NewProcessor(),
}

cn, cleanup := newReaderBufferedPushConn(t, []byte(">2\r\n$10\r\ninvalidate\r\n*1\r\n$3\r\nfo"))
defer cleanup()

c.releaseConn(context.Background(), cn, nil)

if cp.removes != 1 || cp.puts != 0 {
t.Fatalf("reader-buffered partial push read must remove, not re-pool, the connection: removes=%d puts=%d",
cp.removes, cp.puts)
}
}

func TestReleaseConnRemovesConnectionAfterCustomProcessorError(t *testing.T) {
cp := &releaseRecordingPool{}
proc := erroringProcessor{push.NewProcessor(), proto.NewOOMError("OOM custom processor failure")}
c := &baseClient{
opt: &Options{Addr: "127.0.0.1:6379", Protocol: 3},
connPool: cp,
pushProcessor: proc,
}

cn, cleanup := newReaderBufferedPushConn(t, invalidateFrame("foo"))
defer cleanup()

c.releaseConn(context.Background(), cn, nil)

if cp.removes != 1 || cp.puts != 0 {
t.Fatalf("custom-processor drain error must remove, not re-pool, the connection: removes=%d puts=%d",
cp.removes, cp.puts)
}
}

func TestReleaseConnRemovesConnectionAfterVoidProcessorBufferedPartialPushRead(t *testing.T) {
cp := &releaseRecordingPool{}
c := &baseClient{
opt: &Options{Addr: "127.0.0.1:6379", Protocol: 3},
connPool: cp,
pushProcessor: push.NewVoidProcessor(),
}

cn, cleanup := newReaderBufferedPushConn(t, []byte(">2\r\n$10\r\ninvalidate\r\n*1\r\n$3\r\nfo"))
defer cleanup()

c.releaseConn(context.Background(), cn, nil)

if cp.removes != 1 || cp.puts != 0 {
t.Fatalf("void-processor buffered partial push read must remove, not re-pool, the connection: removes=%d puts=%d",
cp.removes, cp.puts)
}
}

func TestReleaseConnRemovesConnectionAfterVoidProcessorPeekError(t *testing.T) {
cp := &releaseRecordingPool{}
c := &baseClient{
opt: &Options{Addr: "127.0.0.1:6379", Protocol: 3},
connPool: cp,
pushProcessor: push.NewVoidProcessor(),
}

// Leading RESP3 attribute can be consumed by PeekReplyType before the
// following partial push frame fails; release-time drain must still treat
// that as fatal and remove the connection.
server, client := newIdleTCPConnPair(t)
defer server.Close()
defer client.Close()

cn := pool.NewConn(client)
frame := []byte("|1\r\n+meta\r\n>2\r\n$10\r\ninvalidate\r\n*1\r\n$3\r\nfo")
if _, err := server.Write(frame); err != nil {
t.Fatalf("write attr+partial push: %v", err)
}
_ = server.Close()
deadline := time.Now().Add(time.Second)
for !cn.MaybeHasData() && time.Now().Before(deadline) {
time.Sleep(time.Millisecond)
}
if !cn.MaybeHasData() {
t.Fatal("attr+partial push never became readable")
}

c.releaseConn(context.Background(), cn, nil)

if cp.removes != 1 || cp.puts != 0 {
t.Fatalf("void-processor peek failure must remove, not re-pool, the connection: removes=%d puts=%d",
cp.removes, cp.puts)
}
}

// bufferedNetConn models a wrapper such as tls.Conn: bytes may already be
// buffered inside the wrapper while NetConn's raw socket is empty.
type bufferedNetConn struct {
Expand Down Expand Up @@ -2452,6 +2547,35 @@ func TestDrainPushNotifications_EmptyWrappedProbeStaysShort(t *testing.T) {
}
}

func TestDrainPushNotifications_EmptyWrappedProbeStaysShortWithVoidProcessor(t *testing.T) {
oldHardReadCap := cscDrainHardReadCap
cscDrainHardReadCap = 200 * time.Millisecond
defer func() { cscDrainHardReadCap = oldHardReadCap }()

server, client := net.Pipe()
defer server.Close()
defer client.Close()

c := &baseClient{opt: &Options{Protocol: 3}, pushProcessor: push.NewVoidProcessor()}
cn := pool.NewConn(&bufferedNetConn{
Conn: client,
buffered: bytes.NewReader(nil),
})
cn.MarkCscReadPending()

start := time.Now()
processed, err := c.drainPushNotifications(cn)
if err != nil {
t.Fatalf("empty wrapped probe with void processor: %v", err)
}
if processed {
t.Fatal("empty wrapped probe with void processor must not report processor success")
}
if elapsed := time.Since(start); elapsed >= 50*time.Millisecond {
t.Fatalf("empty wrapped probe with void processor held the connection for %v", elapsed)
}
}

// TestDrainPushNotifications_ConsumesReaderBufferedPush is a regression guard: a
// push buffered in proto.Reader (no socket data) must still drain — the gate
// checks HasBufferedData(), not only MaybeHasData().
Expand Down
18 changes: 18 additions & 0 deletions error.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,11 @@ func shouldRetry(err error, retryTimeout bool) bool {
return false
}

var txReadErr *txQueuedReadError
if errors.As(err, &txReadErr) {
return false
}

// Check for EOF errors (works with wrapped errors)
if errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) {
return true
Expand Down Expand Up @@ -190,6 +195,19 @@ func isBadConn(err error, allowTimeout bool, addr string) bool {
return false
}

var txReadErr *txQueuedReadError
if errors.As(err, &txReadErr) {
if txReadErr.forceBad {
return true
}
return isBadConn(txReadErr.readErr, allowTimeout, addr)
}
Comment thread
feiguoL marked this conversation as resolved.

var forceBadErr *forceBadConnError
if errors.As(err, &forceBadErr) {
return true
}

// Check for context errors (works with wrapped errors)
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
return true
Expand Down
Loading
Loading