Files
notify/bulk_dataid_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

458 lines
16 KiB
Go

package notify
import (
"context"
"errors"
"math"
"sync"
"testing"
"time"
)
func TestBulkRuntimeSeparatesBidirectionalDataIDNamespaces(t *testing.T) {
clientRuntime := newBulkRuntime("cblk")
serverRuntime := newBulkRuntime("sblk")
clientID, err := clientRuntime.reserveDataID("peer", 0)
if err != nil {
t.Fatalf("reserve client data id: %v", err)
}
serverID, err := serverRuntime.reserveDataID("peer", 0)
if err != nil {
t.Fatalf("reserve server data id: %v", err)
}
if clientID == serverID || clientID%2 != 1 || serverID%2 != 0 {
t.Fatalf("client/server data ids = %d/%d, want disjoint odd/even namespaces", clientID, serverID)
}
clientBulk := newBulkHandle(context.Background(), clientRuntime, "peer", BulkOpenRequest{BulkID: "client", DataID: clientID}, 0, nil, nil, 0, nil, nil, nil, nil, nil)
serverBulk := newBulkHandle(context.Background(), serverRuntime, "peer", BulkOpenRequest{BulkID: "server", DataID: serverID}, 0, nil, nil, 0, nil, nil, nil, nil, nil)
if err := clientRuntime.registerReserved("peer", clientBulk); err != nil {
t.Fatalf("register client bulk: %v", err)
}
if err := serverRuntime.registerReserved("peer", serverBulk); err != nil {
t.Fatalf("register server bulk: %v", err)
}
if got, ok := clientRuntime.lookupInboundFrame("peer", clientID); !ok || got != clientBulk {
t.Fatalf("client local frame lookup = %p/%v, want client bulk", got, ok)
}
if got, ok := clientRuntime.lookupInboundFrame("peer", serverID); ok || got != nil {
t.Fatalf("client peer frame lookup = %p/%v, want missing inbound bulk", got, ok)
}
}
func TestBulkRuntimeRoutesLegacyZeroDataIDInboundFrames(t *testing.T) {
for _, role := range []string{"cblk", "sblk"} {
t.Run(role, func(t *testing.T) {
runtime := newBulkRuntime(role)
bulk := newBulkHandle(context.Background(), runtime, "peer", BulkOpenRequest{
BulkID: "legacy-inbound",
// A legacy initiator leaves DataID unset and uses the ID
// allocated by the receiver's open response.
}, 0, nil, nil, 0, nil, nil, nil, nil, nil)
if err := runtime.registerInbound("peer", bulk); err != nil {
t.Fatalf("register legacy inbound bulk: %v", err)
}
if got := bulk.dataIDSnapshot(); got == 0 {
t.Fatal("legacy inbound registration allocated zero data id")
}
got, ok := runtime.lookupInboundFrame("peer", bulk.dataIDSnapshot())
if !ok || got != bulk {
t.Fatalf("legacy inbound frame lookup = %p/%v, want %p/true", got, ok, bulk)
}
})
}
}
func TestBulkRuntimeAllocatorSkipsLegacyInboundCollision(t *testing.T) {
runtime := newBulkRuntime("cblk")
inbound := newBulkHandle(context.Background(), runtime, "peer", BulkOpenRequest{
BulkID: "legacy-inbound",
DataID: 1,
}, 0, nil, nil, 0, nil, nil, nil, nil, nil)
if err := runtime.registerInbound("peer", inbound); err != nil {
t.Fatalf("register legacy inbound bulk: %v", err)
}
reserved, err := runtime.reserveDataID("peer", 0)
if err != nil {
t.Fatalf("reserve outbound data id: %v", err)
}
if reserved == inbound.dataIDSnapshot() {
t.Fatalf("outbound allocator reused legacy inbound data id %d", reserved)
}
}
func TestBulkRuntimeInboundExplicitDataIDCannotExhaustOutboundAllocator(t *testing.T) {
tests := []struct {
role string
poisonID uint64
wantID uint64
}{
{role: "cblk", poisonID: math.MaxUint64, wantID: 1},
{role: "sblk", poisonID: math.MaxUint64 - 1, wantID: 2},
}
for _, test := range tests {
t.Run(test.role, func(t *testing.T) {
runtime := newBulkRuntime(test.role)
inbound := newBulkHandle(context.Background(), runtime, "peer", BulkOpenRequest{
BulkID: "peer-controlled",
DataID: test.poisonID,
}, 0, nil, nil, 0, nil, nil, nil, nil, nil)
if err := runtime.registerInbound("peer", inbound); err != nil {
t.Fatalf("register inbound bulk: %v", err)
}
got, err := runtime.reserveDataID("peer", 0)
if err != nil {
t.Fatalf("reserve outbound data id after peer-controlled id: %v", err)
}
if got != test.wantID {
t.Fatalf("reserved outbound data id = %d, want %d", got, test.wantID)
}
})
}
}
func TestBulkRuntimeStaleFinalizeDoesNotRemoveReplacement(t *testing.T) {
runtime := newBulkRuntime("cblk")
old := newBulkHandle(context.Background(), runtime, "peer", BulkOpenRequest{
BulkID: "reused",
DataID: 1,
}, 0, nil, nil, 0, nil, nil, nil, nil, nil)
if err := runtime.registerInbound("peer", old); err != nil {
t.Fatalf("register old bulk: %v", err)
}
old.markReset(errors.New("old failed"))
replacement := newBulkHandle(context.Background(), runtime, "peer", BulkOpenRequest{
BulkID: "reused",
DataID: 3,
}, 0, nil, nil, 0, nil, nil, nil, nil, nil)
if err := runtime.registerInbound("peer", replacement); err != nil {
t.Fatalf("register replacement bulk: %v", err)
}
old.markReset(errors.New("late duplicate reset"))
if got, ok := runtime.lookup("peer", "reused"); !ok || got != replacement {
t.Fatalf("replacement bulk after stale finalize = %p/%v, want %p/true", got, ok, replacement)
}
}
func TestBulkRuntimeControlLookupRejectsMismatchedIdentity(t *testing.T) {
runtime := newBulkRuntime("control")
inbound := newBulkHandle(context.Background(), runtime, "peer", BulkOpenRequest{BulkID: "inbound", DataID: 7}, 0, nil, nil, 0, nil, nil, nil, nil, nil)
outbound := newBulkHandle(context.Background(), runtime, "peer", BulkOpenRequest{BulkID: "outbound", DataID: 7}, 0, nil, nil, 0, nil, nil, nil, nil, nil)
if err := runtime.registerInbound("peer", inbound); err != nil {
t.Fatalf("register inbound bulk: %v", err)
}
if err := runtime.registerOutbound("peer", outbound); err != nil {
t.Fatalf("register outbound bulk: %v", err)
}
if got, ok := runtime.lookupControl("peer", "outbound", 7); !ok || got != outbound {
t.Fatalf("matching control lookup = %p/%v, want outbound", got, ok)
}
if got, ok := runtime.lookupControl("peer", "outbound", 0); !ok || got != outbound {
t.Fatalf("BulkID-only control lookup = %p/%v, want outbound", got, ok)
}
if got, ok := runtime.lookupControl("peer", "outbound", 8); ok || got != nil {
t.Fatalf("mismatched control lookup = %p/%v, want rejection", got, ok)
}
if got, ok := runtime.lookupControl("peer", "", 7); ok || got != nil {
t.Fatalf("ambiguous data-only control lookup = %p/%v, want rejection", got, ok)
}
}
func TestBulkCloseControlRejectsMismatchedDataID(t *testing.T) {
t.Run("client", func(t *testing.T) {
client := NewClient().(*ClientCommon)
runtime := client.getBulkRuntime()
bulk := newBulkHandle(context.Background(), runtime, clientFileScope(), BulkOpenRequest{
BulkID: "close-client",
DataID: 11,
}, 0, nil, nil, 0, nil, nil, nil, nil, nil)
if err := runtime.register(clientFileScope(), bulk); err != nil {
t.Fatalf("register client bulk: %v", err)
}
defer bulk.markReset(errors.New("test cleanup"))
payload, err := encode(BulkCloseRequest{BulkID: bulk.ID(), DataID: 13, Full: true})
if err != nil {
t.Fatalf("encode client close: %v", err)
}
client.handleInboundBulkClose(&Message{
NetType: NET_CLIENT,
ServerConn: client,
TransferMsg: TransferMsg{Key: BulkCloseSignalKey, Value: payload, Type: MSG_ASYNC},
})
bulk.mu.Lock()
defer bulk.mu.Unlock()
if bulk.remoteClosed || bulk.peerReadClosed || bulk.resetErr != nil {
t.Fatalf("mismatched client close mutated bulk: remote=%v peer=%v reset=%v", bulk.remoteClosed, bulk.peerReadClosed, bulk.resetErr)
}
})
t.Run("server", func(t *testing.T) {
server := NewServer().(*ServerCommon)
logical := server.bootstrapAcceptedLogical("close-server", nil, nil)
if logical == nil {
t.Fatal("bootstrap server logical connection failed")
}
runtime := server.getBulkRuntime()
scope := serverFileScope(logical)
bulk := newBulkHandle(context.Background(), runtime, scope, BulkOpenRequest{
BulkID: "close-server",
DataID: 17,
}, 0, logical, nil, 0, nil, nil, nil, nil, nil)
if err := runtime.register(scope, bulk); err != nil {
t.Fatalf("register server bulk: %v", err)
}
defer bulk.markReset(errors.New("test cleanup"))
payload, err := encode(BulkCloseRequest{BulkID: bulk.ID(), DataID: 19, Full: true})
if err != nil {
t.Fatalf("encode server close: %v", err)
}
server.handleInboundBulkClose(&Message{
NetType: NET_SERVER,
LogicalConn: logical,
TransportConn: nil,
TransferMsg: TransferMsg{Key: BulkCloseSignalKey, Value: payload, Type: MSG_ASYNC},
})
bulk.mu.Lock()
defer bulk.mu.Unlock()
if bulk.remoteClosed || bulk.peerReadClosed || bulk.resetErr != nil {
t.Fatalf("mismatched server close mutated bulk: remote=%v peer=%v reset=%v", bulk.remoteClosed, bulk.peerReadClosed, bulk.resetErr)
}
})
}
func TestBulkOpenDedicatedConcurrentlyFromBothPeers(t *testing.T) {
server := NewServer().(*ServerCommon)
if err := UseModernPSKServer(server, integrationSharedSecret, integrationModernPSKOptions()); err != nil {
t.Fatalf("UseModernPSKServer failed: %v", err)
}
serverAccepted := make(chan BulkAcceptInfo, 1)
server.SetBulkHandler(func(info BulkAcceptInfo) error {
serverAccepted <- info
return nil
})
if err := server.Listen("tcp", "127.0.0.1:0"); err != nil {
t.Fatalf("server Listen failed: %v", err)
}
defer func() { _ = server.Stop() }()
client := NewClient().(*ClientCommon)
if err := UseModernPSKClient(client, integrationSharedSecret, integrationModernPSKOptions()); err != nil {
t.Fatalf("UseModernPSKClient failed: %v", err)
}
clientAccepted := make(chan BulkAcceptInfo, 1)
client.SetBulkHandler(func(info BulkAcceptInfo) error {
clientAccepted <- info
return nil
})
if err := client.Connect("tcp", server.listener.Addr().String()); err != nil {
t.Fatalf("client Connect failed: %v", err)
}
defer func() { _ = client.Stop() }()
var logical *LogicalConn
deadline := time.Now().Add(2 * time.Second)
for time.Now().Before(deadline) {
peers := server.GetLogicalConnList()
if len(peers) > 0 {
logical = peers[0]
break
}
time.Sleep(time.Millisecond)
}
if logical == nil {
t.Fatal("timed out waiting for server logical connection")
}
type openResult struct {
bulk Bulk
err error
}
clientResult := make(chan openResult, 1)
serverResult := make(chan openResult, 1)
go func() {
bulk, err := client.OpenDedicatedBulk(context.Background(), BulkOpenOptions{Range: BulkRange{Length: 1}})
clientResult <- openResult{bulk: bulk, err: err}
}()
go func() {
bulk, err := server.OpenBulkLogical(context.Background(), logical, BulkOpenOptions{Range: BulkRange{Offset: 1, Length: 1}})
serverResult <- openResult{bulk: bulk, err: err}
}()
clientOpen := <-clientResult
serverOpen := <-serverResult
if clientOpen.err != nil || serverOpen.err != nil {
t.Fatalf("concurrent dedicated opens failed: client=%v server=%v", clientOpen.err, serverOpen.err)
}
clientInbound := waitAcceptedBulk(t, serverAccepted, 2*time.Second)
serverInbound := waitAcceptedBulk(t, clientAccepted, 2*time.Second)
if clientOpen.bulk.(*bulkHandle).dataIDSnapshot()%2 != 1 || serverOpen.bulk.(*bulkHandle).dataIDSnapshot()%2 != 0 {
t.Fatalf("local data ids = %d/%d, want odd/even", clientOpen.bulk.(*bulkHandle).dataIDSnapshot(), serverOpen.bulk.(*bulkHandle).dataIDSnapshot())
}
if clientInbound.Bulk.(*bulkHandle).dataIDSnapshot() != clientOpen.bulk.(*bulkHandle).dataIDSnapshot() {
t.Fatalf("client-open data id mismatch across peers")
}
if serverInbound.Bulk.(*bulkHandle).dataIDSnapshot() != serverOpen.bulk.(*bulkHandle).dataIDSnapshot() {
t.Fatalf("server-open data id mismatch across peers")
}
if _, err := clientOpen.bulk.Write([]byte("client")); err != nil {
t.Fatalf("client bulk write failed: %v", err)
}
readBulkExactly(t, clientInbound.Bulk, "client", 2*time.Second)
if _, err := serverOpen.bulk.Write([]byte("server")); err != nil {
t.Fatalf("server bulk write failed: %v", err)
}
readBulkExactly(t, serverInbound.Bulk, "server", 2*time.Second)
_ = clientOpen.bulk.Close()
_ = clientInbound.Bulk.Close()
_ = serverOpen.bulk.Close()
_ = serverInbound.Bulk.Close()
}
func TestBulkRuntimeDataIDAllocatorObservesExplicitIDs(t *testing.T) {
runtime := newBulkRuntime("dataid")
scope := "peer"
explicit := newBulkHandle(context.Background(), runtime, scope, BulkOpenRequest{
BulkID: "explicit",
DataID: 41,
}, 0, nil, nil, 0, nil, nil, nil, nil, nil)
if err := runtime.register(scope, explicit); err != nil {
t.Fatalf("register explicit bulk: %v", err)
}
if got := explicit.dataIDSnapshot(); got != 41 {
t.Fatalf("explicit data id = %d, want 41", got)
}
auto := newBulkHandle(context.Background(), runtime, scope, BulkOpenRequest{
BulkID: "auto",
}, 0, nil, nil, 0, nil, nil, nil, nil, nil)
if err := runtime.register(scope, auto); err != nil {
t.Fatalf("register auto bulk: %v", err)
}
if got := auto.dataIDSnapshot(); got != 42 {
t.Fatalf("auto data id = %d, want 42 after explicit 41", got)
}
reserved, err := runtime.reserveDataID(scope, 0)
if err != nil {
t.Fatalf("reserve data id: %v", err)
}
if reserved != 43 {
t.Fatalf("reserved data id = %d, want 43", reserved)
}
autoWhileReserved := newBulkHandle(context.Background(), runtime, scope, BulkOpenRequest{
BulkID: "auto-while-reserved",
}, 0, nil, nil, 0, nil, nil, nil, nil, nil)
if err := runtime.register(scope, autoWhileReserved); err != nil {
t.Fatalf("register auto bulk while id is reserved: %v", err)
}
if got := autoWhileReserved.dataIDSnapshot(); got != 44 {
t.Fatalf("auto data id while 43 is reserved = %d, want 44", got)
}
reservedBulk := newBulkHandle(context.Background(), runtime, scope, BulkOpenRequest{
BulkID: "reserved",
DataID: reserved,
}, 0, nil, nil, 0, nil, nil, nil, nil, nil)
if err := runtime.registerReserved(scope, reservedBulk); err != nil {
t.Fatalf("register reserved bulk: %v", err)
}
if _, err := runtime.reserveDataID(scope, reserved); err == nil {
t.Fatal("re-reserving an active data id should fail")
}
}
func TestBulkRuntimeDataIDAllocatorConcurrentReservationsAreUnique(t *testing.T) {
runtime := newBulkRuntime("dataid-concurrent")
const count = 128
ids := make(chan uint64, count)
errs := make(chan error, count)
var wg sync.WaitGroup
for i := 0; i < count; i++ {
wg.Add(1)
go func() {
defer wg.Done()
id, err := runtime.reserveDataID("peer", 0)
if err != nil {
errs <- err
return
}
ids <- id
}()
}
wg.Wait()
close(ids)
close(errs)
for err := range errs {
t.Fatalf("reserve data id: %v", err)
}
seen := make(map[uint64]struct{}, count)
for id := range ids {
if _, ok := seen[id]; ok {
t.Fatalf("duplicate reserved data id %d", id)
}
seen[id] = struct{}{}
}
if len(seen) != count {
t.Fatalf("reserved id count = %d, want %d", len(seen), count)
}
}
func TestBulkMixedDedicatedThenSharedUsesFreshDataID(t *testing.T) {
server := NewServer().(*ServerCommon)
if err := UseModernPSKServer(server, integrationSharedSecret, integrationModernPSKOptions()); err != nil {
t.Fatalf("UseModernPSKServer failed: %v", err)
}
acceptCh := make(chan BulkAcceptInfo, 2)
server.SetBulkHandler(func(info BulkAcceptInfo) error {
acceptCh <- info
return nil
})
if err := server.Listen("tcp", "127.0.0.1:0"); err != nil {
t.Fatalf("server Listen failed: %v", err)
}
defer func() { _ = server.Stop() }()
client := NewClient().(*ClientCommon)
if err := UseModernPSKClient(client, integrationSharedSecret, integrationModernPSKOptions()); err != nil {
t.Fatalf("UseModernPSKClient failed: %v", err)
}
if err := client.Connect("tcp", server.listener.Addr().String()); err != nil {
t.Fatalf("client Connect failed: %v", err)
}
defer func() { _ = client.Stop() }()
first, err := client.OpenDedicatedBulk(context.Background(), BulkOpenOptions{
Range: BulkRange{Offset: 0, Length: 1},
})
if err != nil {
t.Fatalf("open dedicated bulk: %v", err)
}
firstAccepted := waitAcceptedBulk(t, acceptCh, 2*time.Second)
firstID := first.(*bulkHandle).dataIDSnapshot()
if firstID == 0 || firstAccepted.Bulk.(*bulkHandle).dataIDSnapshot() != firstID {
t.Fatalf("dedicated data id mismatch: client=%d server=%d", firstID, firstAccepted.Bulk.(*bulkHandle).dataIDSnapshot())
}
_ = first.Close()
_ = firstAccepted.Bulk.Close()
second, err := client.OpenSharedBulk(context.Background(), BulkOpenOptions{
Range: BulkRange{Offset: 1, Length: 1},
})
if err != nil {
t.Fatalf("open shared bulk after dedicated: %v", err)
}
secondAccepted := waitAcceptedBulk(t, acceptCh, 2*time.Second)
secondID := second.(*bulkHandle).dataIDSnapshot()
if secondID <= firstID {
t.Fatalf("shared data id = %d, want greater than dedicated id %d", secondID, firstID)
}
if got := secondAccepted.Bulk.(*bulkHandle).dataIDSnapshot(); got != secondID {
t.Fatalf("shared data id mismatch: client=%d server=%d", secondID, got)
}
_ = second.Close()
_ = secondAccepted.Bulk.Close()
}