126 lines
3.1 KiB
Go
126 lines
3.1 KiB
Go
package e2e
|
|
|
|
// 并发残留压测:多观看端并发穿透后全部离场(混合断开方式), 目标端必须
|
|
// 零连接残留, 且隧道立即恢复可用——断开同步的正确性验收。
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"sync"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// countedEcho 计数回显目标: 跟踪存活连接数与峰值, 连接保持到对端关闭。
|
|
type countedEcho struct {
|
|
ln net.Listener
|
|
live atomic.Int64
|
|
peak atomic.Int64
|
|
}
|
|
|
|
func startCountedEcho(t *testing.T) *countedEcho {
|
|
t.Helper()
|
|
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() { ln.Close() })
|
|
ce := &countedEcho{ln: ln}
|
|
go func() {
|
|
for {
|
|
c, err := ln.Accept()
|
|
if err != nil {
|
|
return
|
|
}
|
|
go func(c net.Conn) {
|
|
defer c.Close()
|
|
cur := ce.live.Add(1)
|
|
defer ce.live.Add(-1)
|
|
for { // 峰值 CAS 刷高
|
|
p := ce.peak.Load()
|
|
if cur <= p || ce.peak.CompareAndSwap(p, cur) {
|
|
break
|
|
}
|
|
}
|
|
io.Copy(c, c)
|
|
}(c)
|
|
}
|
|
}()
|
|
return ce
|
|
}
|
|
|
|
// TestConcurrentViewersNoTargetLeak 50 观看端并发穿透、三种离场方式
|
|
// (干净 FIN / RST 崩溃 / 迟走), 全部离场后目标端零残留, 隧道立即可复用。
|
|
func TestConcurrentViewersNoTargetLeak(t *testing.T) {
|
|
n, a := startEnv(t)
|
|
ce := startCountedEcho(t)
|
|
_, port := mustRegister(t, n, a, ce.ln.Addr().String())
|
|
|
|
const viewers = 50
|
|
var wg sync.WaitGroup
|
|
var mu sync.Mutex
|
|
var late []net.Conn // 迟走者: 并发段结束后统一关闭
|
|
|
|
for i := 0; i < viewers; i++ {
|
|
wg.Add(1)
|
|
go func(i int) {
|
|
defer wg.Done()
|
|
conn, err := net.DialTimeout("tcp", fmt.Sprintf("127.0.0.1:%d", port), 3*time.Second)
|
|
if err != nil {
|
|
t.Errorf("观看端%d 拨号失败: %v", i, err)
|
|
return
|
|
}
|
|
_ = conn.SetDeadline(time.Now().Add(5 * time.Second))
|
|
payload := []byte(fmt.Sprintf("viewer-%02d", i))
|
|
if _, err := conn.Write(payload); err != nil {
|
|
t.Errorf("观看端%d 写失败: %v", i, err)
|
|
conn.Close()
|
|
return
|
|
}
|
|
got := make([]byte, len(payload))
|
|
if _, err := io.ReadFull(conn, got); err != nil {
|
|
t.Errorf("观看端%d 读回失败: %v", i, err)
|
|
conn.Close()
|
|
return
|
|
}
|
|
switch i % 3 {
|
|
case 0: // 干净 FIN(页面正常关闭)
|
|
conn.Close()
|
|
case 1: // RST 硬断(观看端进程崩溃)
|
|
if tc, ok := conn.(*net.TCPConn); ok {
|
|
tc.SetLinger(0)
|
|
}
|
|
conn.Close()
|
|
default: // 迟走: 压测段结束后统一关
|
|
mu.Lock()
|
|
late = append(late, conn)
|
|
mu.Unlock()
|
|
}
|
|
}(i)
|
|
}
|
|
wg.Wait()
|
|
t.Logf("并发 %d 观看端完成往返: 目标端存活=%d 峰值=%d", viewers, ce.live.Load(), ce.peak.Load())
|
|
|
|
for _, c := range late {
|
|
c.Close()
|
|
}
|
|
|
|
deadline := time.Now().Add(10 * time.Second)
|
|
for time.Now().Before(deadline) {
|
|
if ce.live.Load() == 0 {
|
|
break
|
|
}
|
|
time.Sleep(100 * time.Millisecond)
|
|
}
|
|
if left := ce.live.Load(); left != 0 {
|
|
t.Fatalf("全部观看端离场后目标端残留 %d 条连接(应为 0)", left)
|
|
}
|
|
|
|
// 隧道压测后立即可复用: 新观看端一轮完整往返
|
|
if got := roundTrip(t, port, []byte("after-storm")); string(got) != "after-storm" {
|
|
t.Fatalf("压测后隧道不可用, 回读=%q", got)
|
|
}
|
|
}
|