fix(notify): 根治低带宽控制面阻塞与传输写入卡死

- 为控制消息增加优先级、公平调度、队列字节预算和自适应批处理
- 支持可取消的写门等待,收紧 shared/dedicated bulk、stream 和 Reply 写入边界
- 修复 bulk reset/close、连接 handoff 和安全 profile 切换时序
- 保留旧取消与超时错误契约,新增阶段化 TransportSendError
- 增加 ReplyCtx、ReplyObjCtx、写超时配置及黑洞连接和竞态回归测试
This commit is contained in:
2026-08-14 10:16:37 +08:00
parent 98ef9e7fcc
commit 0826e17063
45 changed files with 4231 additions and 293 deletions
+279
View File
@@ -0,0 +1,279 @@
package notify
import (
"context"
"errors"
"math"
"net"
"os"
"testing"
"time"
"b612.me/stario"
)
func newServerBlackholeTransport(t *testing.T, id string) (*ServerCommon, *LogicalConn, *TransportConn, net.Conn, net.Conn) {
t.Helper()
server := NewServer().(*ServerCommon)
UseLegacySecurityServer(server)
stopCtx, stopFn := context.WithCancel(context.Background())
server.setServerSessionRuntime(&serverSessionRuntime{
stopCtx: stopCtx,
stopFn: stopFn,
queue: stario.NewQueueCtx(stopCtx, 4, math.MaxUint32),
})
server.markSessionStarted()
left, right := net.Pipe()
logical, _, _ := newRegisteredServerLogicalForTest(t, server, id, left, stopCtx, stopFn)
logical.applyAttachmentProfile(0, 0, server.defaultMsgEn, server.defaultMsgDe, server.defaultFastStreamEncode, server.defaultFastBulkEncode, server.defaultFastPlainEncode, server.handshakeRsaKey, server.SecretKey)
transport := logical.CurrentTransportConn()
if transport == nil {
t.Fatal("server transport is nil")
}
t.Cleanup(func() {
_ = left.Close()
_ = right.Close()
server.markSessionStopped("test done", nil)
})
return server, logical, transport, left, right
}
func requireBoundedServerWrite(t *testing.T, timeout time.Duration, write func(context.Context) error) {
t.Helper()
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
done := make(chan error, 1)
started := time.Now()
go func() { done <- write(ctx) }()
select {
case err := <-done:
if err == nil {
t.Fatal("blackhole server write returned nil")
}
if !errors.Is(err, context.DeadlineExceeded) && !errors.Is(err, os.ErrDeadlineExceeded) {
var netErr net.Error
if !errors.As(err, &netErr) || !netErr.Timeout() {
t.Fatalf("blackhole server write error=%v, want deadline error", err)
}
}
if elapsed := time.Since(started); elapsed > 5*timeout {
t.Fatalf("blackhole server write returned after %v, want bounded by %v", elapsed, timeout)
}
case <-time.After(10 * timeout):
t.Fatalf("blackhole server write ignored its %v context deadline", timeout)
}
}
func TestServerSharedBulkWriteHonorsContextDeadline(t *testing.T) {
server, logical, transport, _, _ := newServerBlackholeTransport(t, "shared-bulk-write-context")
requireBoundedServerWrite(t, 30*time.Millisecond, func(ctx context.Context) error {
return server.sendFastBulkDataTransport(ctx, logical, transport, 1, 0, []byte("bulk"), bulkFastPathVersionV1)
})
}
func TestServerSharedStreamWriteHonorsContextDeadline(t *testing.T) {
server, logical, transport, _, _ := newServerBlackholeTransport(t, "shared-stream-write-context")
stream := newStreamHandle(context.Background(), nil, serverFileScope(logical), StreamOpenRequest{
StreamID: "shared-stream-write-context",
DataID: 1,
Channel: StreamDataChannel,
FastPathVersion: streamFastPathVersionV1,
}, 0, logical, transport, transport.TransportGeneration(), nil, nil, nil, defaultStreamConfig())
requireBoundedServerWrite(t, 30*time.Millisecond, func(ctx context.Context) error {
return server.sendFastStreamDataTransport(ctx, logical, transport, stream, []byte("stream"))
})
}
func TestMessageReplyUsesConfiguredDefaultWriteTimeout(t *testing.T) {
server, logical, transport, left, _ := newServerBlackholeTransport(t, "reply-default-write-timeout")
server.SetReplyWriteTimeout(35 * time.Millisecond)
message := Message{
NetType: NET_SERVER,
LogicalConn: logical,
TransportConn: transport,
TransferMsg: TransferMsg{
ID: 1,
Key: "reply-default-write-timeout",
Type: MSG_SYNC_ASK,
},
inboundConn: left,
}
started := time.Now()
err := message.Reply([]byte("reply"))
if err == nil {
t.Fatal("blackhole Message.Reply returned nil")
}
if elapsed := time.Since(started); elapsed > 250*time.Millisecond {
t.Fatalf("Message.Reply returned after %v, want configured default write bound", elapsed)
}
}
func TestMessageReplyCtxUsesEarlierCallerDeadline(t *testing.T) {
server, logical, transport, left, _ := newServerBlackholeTransport(t, "reply-caller-write-timeout")
server.SetReplyWriteTimeout(time.Second)
message := Message{
NetType: NET_SERVER,
LogicalConn: logical,
TransportConn: transport,
TransferMsg: TransferMsg{
ID: 2,
Key: "reply-caller-write-timeout",
Type: MSG_SYNC_ASK,
},
inboundConn: left,
}
ctx, cancel := context.WithTimeout(context.Background(), 25*time.Millisecond)
defer cancel()
started := time.Now()
err := message.ReplyCtx(ctx, []byte("reply"))
if err == nil {
t.Fatal("blackhole Message.ReplyCtx returned nil")
}
if elapsed := time.Since(started); elapsed > 200*time.Millisecond {
t.Fatalf("Message.ReplyCtx returned after %v, want caller deadline", elapsed)
}
canceled, cancelNow := context.WithCancel(context.Background())
cancelNow()
if err := message.ReplyObjCtx(canceled, "ok"); !errors.Is(err, context.Canceled) {
t.Fatalf("Message.ReplyObjCtx canceled error=%v, want context canceled", err)
}
}
type closeTrackingWriteConn struct {
closed bool
writes int
}
func (c *closeTrackingWriteConn) Read([]byte) (int, error) { return 0, net.ErrClosed }
func (c *closeTrackingWriteConn) Close() error { c.closed = true; return nil }
func (c *closeTrackingWriteConn) LocalAddr() net.Addr { return nil }
func (c *closeTrackingWriteConn) RemoteAddr() net.Addr { return nil }
func (c *closeTrackingWriteConn) SetDeadline(time.Time) error { return nil }
func (c *closeTrackingWriteConn) SetReadDeadline(time.Time) error { return nil }
func (c *closeTrackingWriteConn) SetWriteDeadline(time.Time) error { return nil }
func (c *closeTrackingWriteConn) Write(data []byte) (int, error) {
c.writes++
return len(data), nil
}
func TestServerRawWriteGateWaitTimeoutDoesNotCloseConnection(t *testing.T) {
server, logical, transport, _, _ := newServerBlackholeTransport(t, "raw-write-gate-timeout")
conn := &closeTrackingWriteConn{}
gateRef := retainRawConnWriteGate(conn)
<-gateRef.gate
t.Cleanup(func() {
gateRef.gate <- struct{}{}
releaseRawConnWriteGate(conn, gateRef)
})
ctx, cancel := context.WithTimeout(context.Background(), 25*time.Millisecond)
defer cancel()
err := server.writeEnvelopePayloadContextTimeout(ctx, logical, transport, conn, []byte("reply"), time.Second)
if !errors.Is(err, context.DeadlineExceeded) {
t.Fatalf("raw write gate wait error=%v, want context deadline exceeded", err)
}
if conn.closed {
t.Fatal("raw write gate wait timeout closed a connection before physical write started")
}
if conn.writes != 0 {
t.Fatalf("raw write gate wait performed %d physical writes", conn.writes)
}
}
func TestServerUDPWriteGateWaitHonorsWriteTimeout(t *testing.T) {
server := NewServer().(*ServerCommon)
stopCtx, stopFn := context.WithCancel(context.Background())
t.Cleanup(stopFn)
sender, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: 0})
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = sender.Close() })
receiver, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: 0})
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = receiver.Close() })
server.setServerSessionRuntime(&serverSessionRuntime{
stopCtx: stopCtx,
stopFn: stopFn,
queue: stario.NewQueueCtx(stopCtx, 4, math.MaxUint32),
udpListener: sender,
})
logical := newServerLogicalConn(server, "udp-write-gate-timeout", receiver.LocalAddr())
transport := &TransportConn{logical: logical, remoteAddr: receiver.LocalAddr(), attached: true}
gateHeld := make(chan struct{})
releaseGate := make(chan struct{})
firstDone := make(chan error, 1)
go func() {
firstDone <- server.withUDPWriteLock(context.Background(), func() error {
close(gateHeld)
<-releaseGate
return nil
})
}()
select {
case <-gateHeld:
case <-time.After(time.Second):
t.Fatal("first UDP writer did not acquire the write gate")
}
started := time.Now()
writeDone := make(chan error, 1)
go func() {
writeDone <- server.writeEnvelopePayloadContextTimeout(context.Background(), logical, transport, nil, []byte("reply"), 30*time.Millisecond)
}()
select {
case err = <-writeDone:
case <-time.After(200 * time.Millisecond):
close(releaseGate)
<-firstDone
<-writeDone
t.Fatal("UDP write gate wait ignored the configured write timeout")
}
if !errors.Is(err, context.DeadlineExceeded) {
close(releaseGate)
<-firstDone
t.Fatalf("UDP write gate wait error=%v, want context deadline exceeded", err)
}
if elapsed := time.Since(started); elapsed > 200*time.Millisecond {
close(releaseGate)
<-firstDone
t.Fatalf("UDP write gate wait returned after %v, want configured write bound", elapsed)
}
close(releaseGate)
if err := <-firstDone; err != nil {
t.Fatalf("first UDP writer failed: %v", err)
}
}
func TestTransportWriteGateRegistryReleasesBindingsAndRawWrites(t *testing.T) {
conn := &serializedWriteTestConn{}
first := newTransportBinding(conn, stario.NewQueue())
second := newTransportBinding(conn, stario.NewQueue())
if first.writeGateSnapshot() != second.writeGateSnapshot() {
t.Fatal("same physical connection did not share one write gate")
}
entry, ok := transportConnWriteGates.Load(conn)
if !ok {
t.Fatal("shared write gate was not registered")
}
first.stopBackgroundWorkers()
if current, ok := transportConnWriteGates.Load(conn); !ok || current != entry {
t.Fatal("stopping one shared binding removed the live write gate")
}
second.stopBackgroundWorkers()
if _, ok := transportConnWriteGates.Load(conn); ok {
t.Fatal("last binding release retained the historical connection write gate")
}
rawConn := &serializedWriteTestConn{}
if err := writeFullToConn(rawConn, []byte("raw")); err != nil {
t.Fatalf("raw write failed: %v", err)
}
if _, ok := transportConnWriteGates.Load(rawConn); ok {
t.Fatal("completed raw write retained a temporary write gate reference")
}
}