Files
notify/transport_conn_test.go
T
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

256 lines
8.5 KiB
Go

package notify
import (
"context"
"errors"
"math"
"net"
"testing"
"time"
"b612.me/stario"
)
func TestClientConnCurrentTransportConnStreamSnapshot(t *testing.T) {
server := NewServer().(*ServerCommon)
left, right := net.Pipe()
defer left.Close()
defer right.Close()
logical := server.bootstrapAcceptedLogical("transport-stream", nil, left)
if logical == nil {
t.Fatal("bootstrapAcceptedLogical should return logical")
}
transport := logical.CurrentTransportConn()
if transport == nil {
t.Fatal("CurrentTransportConn should expose active stream transport")
}
if transport.LogicalConn() != logical {
t.Fatal("LogicalConn mismatch")
}
if !transport.Attached() {
t.Fatal("Attached mismatch: got false want true")
}
if !transport.HasRuntimeConn() {
t.Fatal("HasRuntimeConn mismatch: got false want true")
}
if !transport.UsesStreamTransport() {
t.Fatal("UsesStreamTransport mismatch: got false want true")
}
if !transport.IsCurrent() {
t.Fatal("IsCurrent mismatch: got false want true")
}
snapshot, err := GetTransportConnRuntimeSnapshot(transport)
if err != nil {
t.Fatalf("GetTransportConnRuntimeSnapshot failed: %v", err)
}
if got, want := snapshot.ClientID, logical.ClientID; got != want {
t.Fatalf("ClientID mismatch: got %q want %q", got, want)
}
if !snapshot.Attached || !snapshot.HasRuntimeConn || !snapshot.Current {
t.Fatalf("unexpected transport snapshot: %+v", snapshot)
}
if got, want := snapshot.BindingOwner, "server-transport"; got != want {
t.Fatalf("BindingOwner mismatch: got %q want %q", got, want)
}
if !snapshot.BindingCurrent || !snapshot.LogicalAlive {
t.Fatalf("binding state mismatch: %+v", snapshot)
}
}
func TestClientConnCurrentTransportConnPacketSnapshot(t *testing.T) {
server := NewServer().(*ServerCommon)
stopCtx, stopFn := context.WithCancel(context.Background())
defer stopFn()
udpListener, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: 0})
if err != nil {
t.Fatalf("ListenUDP failed: %v", err)
}
defer udpListener.Close()
server.setServerSessionRuntime(&serverSessionRuntime{
stopCtx: stopCtx,
stopFn: stopFn,
udpListener: udpListener,
})
addr, err := net.ResolveUDPAddr("udp", "127.0.0.1:34567")
if err != nil {
t.Fatalf("ResolveUDPAddr failed: %v", err)
}
logical := server.bootstrapAcceptedLogical("transport-packet", addr, nil)
if logical == nil {
t.Fatal("bootstrapAcceptedLogical should return packet logical")
}
transport := logical.CurrentTransportConn()
if transport == nil {
t.Fatal("CurrentTransportConn should expose packet transport route")
}
if got, want := transport.RemoteAddr().String(), addr.String(); got != want {
t.Fatalf("RemoteAddr mismatch: got %q want %q", got, want)
}
if !transport.Attached() {
t.Fatal("Attached mismatch: got false want true")
}
if transport.HasRuntimeConn() {
t.Fatal("packet transport should not expose runtime conn")
}
if transport.TransportGeneration() != 0 {
t.Fatalf("packet transport generation mismatch: got %d want 0", transport.TransportGeneration())
}
}
func TestTransportConnSendRejectsStaleGenerationAfterReattach(t *testing.T) {
server := NewServer().(*ServerCommon)
UseLegacySecurityServer(server)
runtimeCtx, runtimeCancel := context.WithCancel(context.Background())
defer runtimeCancel()
queue := stario.NewQueueCtx(runtimeCtx, 4, math.MaxUint32)
server.setServerSessionRuntime(&serverSessionRuntime{
stopCtx: runtimeCtx,
stopFn: runtimeCancel,
queue: queue,
})
server.markSessionStarted()
defer server.markSessionStopped("test done", nil)
firstLeft, firstRight := net.Pipe()
defer firstRight.Close()
logical, _, _ := newRegisteredServerLogicalForTest(t, server, "transport-send-stale", firstLeft, runtimeCtx, runtimeCancel)
logical.applyClientConnAttachmentProfile(0, 100*time.Millisecond, server.defaultMsgEn, server.defaultMsgDe, server.handshakeRsaKey, server.SecretKey)
firstTransport := logical.CurrentTransportConn()
if firstTransport == nil {
t.Fatal("first transport snapshot should exist")
}
secondLeft, secondRight := net.Pipe()
defer secondRight.Close()
if err := logical.attachClientConnSessionTransport(secondLeft); err != nil {
t.Fatalf("attachClientConnSessionTransport failed: %v", err)
}
secondTransport := logical.CurrentTransportConn()
if secondTransport == nil {
t.Fatal("second transport snapshot should exist after reattach")
}
if firstTransport.IsCurrent() {
t.Fatal("first transport should become stale after reattach")
}
if err := firstTransport.Send("stale", MsgVal("payload")); !errors.Is(err, errTransportDetached) {
t.Fatalf("stale transport send error = %v, want errors.Is(..., %v)", err, errTransportDetached)
}
recvCh := make(chan []byte, 1)
errCh := make(chan error, 1)
go func() {
_ = secondRight.SetReadDeadline(time.Now().Add(time.Second))
reader := stario.NewFrameReader(secondRight, nil)
payload, err := reader.Next()
if err != nil {
errCh <- err
return
}
recvCh <- payload
}()
if err := secondTransport.Send("fresh", MsgVal("payload")); err != nil {
t.Fatalf("fresh transport send failed: %v", err)
}
select {
case err := <-errCh:
t.Fatalf("fresh transport read failed: %v", err)
case got := <-recvCh:
if len(got) == 0 {
t.Fatal("fresh transport should produce framed payload")
}
case <-time.After(time.Second):
t.Fatal("fresh transport send timed out")
}
}
func TestTransportConnPinnedBindingDoesNotWriteReplacementTransport(t *testing.T) {
server := NewServer().(*ServerCommon)
UseLegacySecurityServer(server)
runtimeCtx, runtimeCancel := context.WithCancel(context.Background())
defer runtimeCancel()
queue := stario.NewQueueCtx(runtimeCtx, 4, math.MaxUint32)
server.setServerSessionRuntime(&serverSessionRuntime{stopCtx: runtimeCtx, stopFn: runtimeCancel, queue: queue})
server.markSessionStarted()
defer server.markSessionStopped("test done", nil)
firstLeft, firstRight := net.Pipe()
defer firstRight.Close()
logical, _, _ := newRegisteredServerLogicalForTest(t, server, "transport-pinned-binding", firstLeft, runtimeCtx, runtimeCancel)
firstTransport := logical.CurrentTransportConn()
if firstTransport == nil || firstTransport.binding == nil {
t.Fatal("first transport should pin its physical binding")
}
secondLeft, secondRight := net.Pipe()
defer secondRight.Close()
if err := logical.attachClientConnSessionTransport(secondLeft); err != nil {
t.Fatalf("attach replacement transport: %v", err)
}
if got := serverTransportBindingSnapshot(logical, firstTransport); got != firstTransport.binding {
t.Fatal("stale explicit transport resolved to a different physical binding")
}
result := make(chan error, 1)
go func() {
result <- server.writeEnvelopePayloadContext(context.Background(), logical, firstTransport, nil, []byte("stale"))
}()
assertNoPipeWrite(t, secondRight, "pinned transport write crossed onto replacement transport")
select {
case err := <-result:
if err == nil {
t.Fatal("write to retired pinned transport unexpectedly succeeded")
}
case <-time.After(time.Second):
t.Fatal("write to retired pinned transport did not terminate")
}
}
func TestTransportConnRuntimeSnapshotIncludesDetachDiagnostics(t *testing.T) {
server := NewServer().(*ServerCommon)
left, right := net.Pipe()
defer right.Close()
logical := server.bootstrapAcceptedLogical("transport-detach", nil, left)
if logical == nil {
t.Fatal("bootstrapAcceptedLogical should return logical")
}
transport := logical.CurrentTransportConn()
if transport == nil {
t.Fatal("CurrentTransportConn should return active transport")
}
server.detachLogicalSessionTransport(logical, "read error", errors.New("boom"))
snapshot, err := GetTransportConnRuntimeSnapshot(transport)
if err != nil {
t.Fatalf("GetTransportConnRuntimeSnapshot failed: %v", err)
}
if snapshot.Current {
t.Fatalf("snapshot Current should be false after detach: %+v", snapshot)
}
if snapshot.BindingCurrent {
t.Fatalf("snapshot BindingCurrent should be false after detach: %+v", snapshot)
}
if got, want := snapshot.LogicalReason, ""; got != want {
t.Fatalf("snapshot LogicalReason = %q, want %q", got, want)
}
if got, want := snapshot.TransportDetachReason, "read error"; got != want {
t.Fatalf("snapshot TransportDetachReason = %q, want %q", got, want)
}
if got, want := snapshot.TransportDetachKind, clientConnTransportDetachKindReadError; got != want {
t.Fatalf("snapshot TransportDetachKind = %q, want %q", got, want)
}
if got, want := snapshot.TransportDetachError, "boom"; got != want {
t.Fatalf("snapshot TransportDetachError = %q, want %q", got, want)
}
}