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