Files
notify/review_fix_regression_test.go
b612 1f2e74acca fix(notify): 修复传输生命周期竞态,完善背压与协议边界
- 完善 stream/bulk DataID 分配、预留和双向命名空间,修复并发打开及 dedicated/shared 回退时的 ID 冲突
- 将收发、回复、恢复任务和 sidecar 绑定原始会话与物理连接,防止重连后的旧消息误操作新连接
- 加强 close/reset 身份校验及实例移除检查,修复 dedicated attach 失败、通道引用和资源回收竞态
- 收紧批量发送器停止准入,确保在途入队完成后统一清理请求、缓冲区和等待者
- 修复 record 满队列死锁、取消时序号消耗及关闭竞态,确保关闭有界并返回真实错误
- 增加协商式 record 逻辑半关闭,保留反向 ACK;通过 reset 传递 RecordFailure,避免背压掩盖原始失败原因
- 补齐帧长度、批次数量、序号溢出和未确认窗口校验,提前拒绝超限数据并按字节预算拆批
- 为入站分发增加全局及单连接的条数、字节预算和阻塞背压,关闭时唤醒等待者,消除正常断连日志噪音
- 完善 bulk 窗口释放失败处理与传输诊断,补充并发、重连、背压、协议边界及真实 TCP 回归覆盖
2026-09-23 15:33:17 +08:00

198 lines
5.7 KiB
Go

package notify
import (
"b612.me/stario"
"context"
"errors"
netpkg "net"
"sync/atomic"
"testing"
"time"
)
type gatedDedicatedReadConn struct {
netpkg.Conn
total int
read int
reached chan struct{}
release chan struct{}
closed bool
}
func (c *gatedDedicatedReadConn) Read(p []byte) (int, error) {
n, err := c.Conn.Read(p)
c.read += n
if !c.closed && c.read >= c.total {
c.closed = true
close(c.reached)
<-c.release
}
return n, err
}
func TestControlMessageErrorPreservesTransportDetachedSentinel(t *testing.T) {
for name, decode := range map[string]func(string) error{
"bulk": bulkControlMessageError,
"stream": streamControlMessageError,
} {
t.Run(name, func(t *testing.T) {
err := decode("transport detached: stale transport generation=7")
if !errors.Is(err, errTransportDetached) {
t.Fatalf("decoded error = %v, want transport detached sentinel", err)
}
})
}
}
func TestSendDedicatedBulkAttachRequestRejectsNilDialResult(t *testing.T) {
client := &ClientCommon{}
bulk := newBulkHandle(context.Background(), newBulkRuntime("cblk"), clientFileScope(), BulkOpenRequest{
BulkID: "nil-dial",
}, 0, nil, nil, 0, nil, nil, nil, nil, nil)
if _, err := client.sendDedicatedBulkAttachRequest(context.Background(), nil, bulk); !errors.Is(err, errTransportDetached) {
t.Fatalf("nil dial result error = %v, want transport detached", err)
}
}
func TestDedicatedSidecarCloseWaitsForAttachment(t *testing.T) {
left, right := netpkg.Pipe()
defer left.Close()
defer right.Close()
sidecar := newBulkDedicatedSidecar(left, 1)
entered := make(chan struct{})
release := make(chan struct{})
attachDone := make(chan error, 1)
go func() {
attachDone <- sidecar.withConn(func(conn netpkg.Conn) error {
close(entered)
<-release
return nil
})
}()
select {
case <-entered:
case <-time.After(time.Second):
t.Fatal("sidecar attachment did not start")
}
closeDone := make(chan struct{})
go func() {
sidecar.close()
close(closeDone)
}()
select {
case <-closeDone:
t.Fatal("sidecar closed while attachment still held")
case <-time.After(25 * time.Millisecond):
}
close(release)
if err := <-attachDone; err != nil {
t.Fatalf("sidecar attachment error = %v", err)
}
select {
case <-closeDone:
case <-time.After(time.Second):
t.Fatal("sidecar close did not finish after attachment release")
}
}
func TestBulkAcceptDispatchRejectsResetBeforeHandler(t *testing.T) {
bulk := newBulkHandle(context.Background(), newBulkRuntime("sblk"), serverFileDomain+":test", BulkOpenRequest{
BulkID: "stale-dispatch",
}, 0, nil, nil, 0, nil, nil, nil, nil, nil)
bulk.markReset(errTransportDetached)
var calls atomic.Int32
err := dispatchBulkAccept(func(BulkAcceptInfo) error {
calls.Add(1)
return nil
}, bulk, BulkAcceptInfo{Bulk: bulk})
if !errors.Is(err, errTransportDetached) {
t.Fatalf("stale dispatch error = %v, want transport detached", err)
}
if got := calls.Load(); got != 0 {
t.Fatalf("stale dispatch handler calls = %d, want 0", got)
}
}
func TestClientDedicatedSidecarDropsFrameAfterTransportReattach(t *testing.T) {
client := NewClient().(*ClientCommon)
UseLegacySecurityClient(client)
stopCtx, stopFn := context.WithCancel(context.Background())
defer stopFn()
queue := stario.NewQueueCtx(stopCtx, 4, ^uint32(0))
oldLeft, oldRight := netpkg.Pipe()
defer oldRight.Close()
epoch := client.beginClientSessionEpoch()
client.setClientSessionRuntime(newClientSessionRuntime(oldLeft, stopCtx, stopFn, queue, epoch))
client.markSessionStarted()
defer client.markSessionStopped("test done", nil)
oldRoute := client.clientSessionRouteSnapshot()
sidecarLeft, sidecarRight := netpkg.Pipe()
defer sidecarRight.Close()
payload, err := client.encodeDedicatedBulkBatchPayload(101, []bulkDedicatedSendRequest{{
Type: bulkFastPayloadTypeData,
Seq: 1,
Payload: []byte("stale"),
}})
if err != nil {
t.Fatalf("encode stale sidecar payload: %v", err)
}
frameRead := make(chan struct{})
releaseFrame := make(chan struct{})
readConn := &gatedDedicatedReadConn{
Conn: sidecarLeft,
total: bulkDedicatedRecordHeaderLen + len(payload),
reached: frameRead,
release: releaseFrame,
}
sidecar := newBulkDedicatedSidecar(readConn, 1)
loopDone := make(chan struct{})
go func() {
client.readDedicatedSidecarLoopAtRoute(sidecar, oldRoute)
close(loopDone)
}()
writeDone := make(chan error, 1)
go func() { writeDone <- writeBulkDedicatedRecord(sidecarRight, payload) }()
select {
case <-frameRead:
case <-time.After(time.Second):
t.Fatal("sidecar loop did not read the complete frame")
}
newLeft, newRight := netpkg.Pipe()
defer newRight.Close()
if err := client.attachClientSessionTransport(newLeft); err != nil {
t.Fatalf("attach replacement client transport: %v", err)
}
runtime := client.getBulkRuntime()
bulk := newBulkHandle(stopCtx, runtime, clientFileScope(), BulkOpenRequest{
BulkID: "stale-sidecar-frame",
DataID: 101,
Range: BulkRange{Length: 32},
}, oldRoute.epoch, nil, nil, 0, nil, nil, nil, nil, nil)
bulk.setClientSnapshotOwner(client)
bulk.setClientSessionRoute(oldRoute)
if err := runtime.registerInbound(clientFileScope(), bulk); err != nil {
t.Fatalf("register stale-route bulk: %v", err)
}
defer bulk.markReset(errors.New("test cleanup"))
close(releaseFrame)
if err := <-writeDone; err != nil {
t.Fatalf("write stale sidecar payload: %v", err)
}
select {
case <-loopDone:
case <-time.After(time.Second):
t.Fatal("stale-route sidecar loop did not stop after reattach")
}
bulk.mu.Lock()
defer bulk.mu.Unlock()
if len(bulk.readQueue) != 0 || len(bulk.readBuf.data) != 0 || bulk.resetErr != nil {
t.Fatalf("stale sidecar frame mutated bulk: queued=%d buffered=%d reset=%v", len(bulk.readQueue), len(bulk.readBuf.data), bulk.resetErr)
}
}