notify/starnotify/runtime_snapshot_test.go
starainrt 09d972c7b7
feat(notify): 重构通信内核并补齐 stream/bulk/record/transfer 能力
- 引入 LogicalConn/TransportConn 分层,ClientConn 保留兼容适配层
  - 新增 Stream、Bulk、RecordStream 三条数据面能力及对应控制路径
  - 完成 transfer/file 传输内核与状态快照、诊断能力
  - 补齐 reconnect、inbound dispatcher、modern psk 等基础模块
  - 增加大规模回归、并发与基准测试覆盖
  - 更新依赖库
2026-04-15 15:24:36 +08:00

563 lines
18 KiB
Go

package starnotify
import (
"context"
"errors"
"net"
"sync"
"testing"
"time"
"b612.me/notify"
)
var errSingleConnListenerClosed = errors.New("single conn listener closed")
type singleConnListener struct {
conn net.Conn
used bool
mu sync.Mutex
closed chan struct{}
once sync.Once
}
func newSingleConnListener(conn net.Conn) *singleConnListener {
return &singleConnListener{
conn: conn,
closed: make(chan struct{}),
}
}
func (l *singleConnListener) Accept() (net.Conn, error) {
l.mu.Lock()
if !l.used && l.conn != nil {
conn := l.conn
l.used = true
l.mu.Unlock()
return conn, nil
}
l.mu.Unlock()
<-l.closed
return nil, errSingleConnListenerClosed
}
func (l *singleConnListener) Close() error {
l.once.Do(func() {
close(l.closed)
})
return nil
}
func (l *singleConnListener) Addr() net.Addr {
return singleConnAddr("starnotify-single-listener")
}
type singleConnAddr string
func (a singleConnAddr) Network() string { return "single-conn" }
func (a singleConnAddr) String() string { return string(a) }
func TestGetRuntimeSnapshotByKeyDefaults(t *testing.T) {
const clientKey = "runtime-snapshot-client"
const serverKey = "runtime-snapshot-server"
_ = DeleteClient(clientKey)
_ = DeleteServer(serverKey)
defer DeleteClient(clientKey)
defer DeleteServer(serverKey)
NewClient(clientKey)
NewServer(serverKey)
clientSnapshot, err := GetClientRuntimeSnapshot(clientKey)
if err != nil {
t.Fatalf("GetClientRuntimeSnapshot failed: %v", err)
}
if got, want := clientSnapshot.OwnerState, "idle"; got != want {
t.Fatalf("client OwnerState mismatch: got %q want %q", got, want)
}
if clientSnapshot.Alive {
t.Fatalf("client Alive mismatch: got %v want false", clientSnapshot.Alive)
}
if !clientSnapshot.HasRuntimeStopCtx {
t.Fatalf("client HasRuntimeStopCtx mismatch: got %v want true", clientSnapshot.HasRuntimeStopCtx)
}
if clientSnapshot.Retry != (notify.ConnectionRetrySnapshot{}) {
t.Fatalf("client Retry snapshot mismatch: %+v", clientSnapshot.Retry)
}
serverSnapshot, err := GetServerRuntimeSnapshot(serverKey)
if err != nil {
t.Fatalf("GetServerRuntimeSnapshot failed: %v", err)
}
if got, want := serverSnapshot.OwnerState, "idle"; got != want {
t.Fatalf("server OwnerState mismatch: got %q want %q", got, want)
}
if serverSnapshot.Alive {
t.Fatalf("server Alive mismatch: got %v want false", serverSnapshot.Alive)
}
if !serverSnapshot.HasRuntimeStopCtx {
t.Fatalf("server HasRuntimeStopCtx mismatch: got %v want true", serverSnapshot.HasRuntimeStopCtx)
}
if serverSnapshot.Retry != (notify.ConnectionRetrySnapshot{}) {
t.Fatalf("server Retry snapshot mismatch: %+v", serverSnapshot.Retry)
}
}
func TestGetRuntimeSnapshotMissingKey(t *testing.T) {
if _, err := GetClientRuntimeSnapshot("missing-client"); err == nil {
t.Fatal("GetClientRuntimeSnapshot should fail for missing key")
}
if _, err := GetServerRuntimeSnapshot("missing-server"); err == nil {
t.Fatal("GetServerRuntimeSnapshot should fail for missing key")
}
if _, err := GetServerClientRuntimeSnapshot("missing-server", "peer"); err == nil {
t.Fatal("GetServerClientRuntimeSnapshot should fail for missing server key")
}
if _, err := GetServerLogicalConn("missing-server", "peer"); err == nil {
t.Fatal("GetServerLogicalConn should fail for missing server key")
}
if _, ok, err := GetServerCurrentTransportConn("missing-server", "peer"); err == nil || ok {
t.Fatal("GetServerCurrentTransportConn should fail for missing server key")
}
if _, ok, err := GetServerClientTransportRuntimeSnapshot("missing-server", "peer"); err == nil || ok {
t.Fatal("GetServerClientTransportRuntimeSnapshot should fail for missing server key")
}
if _, err := GetServerDetachedClientRuntimeSnapshots("missing-server"); err == nil {
t.Fatal("GetServerDetachedClientRuntimeSnapshots should fail for missing server key")
}
}
func TestGetRuntimeSnapshotExposesRetryState(t *testing.T) {
const clientKey = "runtime-retry-client"
const serverKey = "runtime-retry-server"
_ = DeleteClient(clientKey)
_ = DeleteServer(serverKey)
defer DeleteClient(clientKey)
defer DeleteServer(serverKey)
NewLegacySecurityClient(clientKey)
NewServer(serverKey)
clientRetryErr := errors.New("dial failed")
err := ConnectClientFactoryWithRetry(clientKey, func(context.Context) (net.Conn, error) {
return nil, clientRetryErr
}, &notify.ConnectRetryOptions{
MaxAttempts: 2,
BaseDelay: time.Millisecond,
MaxDelay: time.Millisecond,
})
if !errors.Is(err, clientRetryErr) {
t.Fatalf("ConnectClientFactoryWithRetry error = %v, want %v", err, clientRetryErr)
}
clientSnapshot, err := GetClientRuntimeSnapshot(clientKey)
if err != nil {
t.Fatalf("GetClientRuntimeSnapshot failed: %v", err)
}
if got, want := clientSnapshot.Retry.RetryEventTotal, uint64(1); got != want {
t.Fatalf("client RetryEventTotal mismatch: got %d want %d", got, want)
}
if got, want := clientSnapshot.Retry.LastRetryAttempt, 1; got != want {
t.Fatalf("client LastRetryAttempt mismatch: got %d want %d", got, want)
}
if got, want := clientSnapshot.Retry.LastRetryError, clientRetryErr.Error(); got != want {
t.Fatalf("client LastRetryError mismatch: got %q want %q", got, want)
}
if got, want := clientSnapshot.Retry.LastResultError, clientRetryErr.Error(); got != want {
t.Fatalf("client LastResultError mismatch: got %q want %q", got, want)
}
if clientSnapshot.Retry.LastRetryAt.IsZero() {
t.Fatal("client LastRetryAt should be recorded")
}
if clientSnapshot.Retry.LastResultAt.IsZero() {
t.Fatal("client LastResultAt should be recorded")
}
serverErr := ListenServerWithRetry(serverKey, "tcp", "127.0.0.1:0", &notify.ConnectRetryOptions{
MaxAttempts: 2,
BaseDelay: time.Millisecond,
MaxDelay: time.Millisecond,
})
if serverErr == nil {
t.Fatal("ListenServerWithRetry should fail without security configuration")
}
serverSnapshot, err := GetServerRuntimeSnapshot(serverKey)
if err != nil {
t.Fatalf("GetServerRuntimeSnapshot failed: %v", err)
}
if got, want := serverSnapshot.Retry.RetryEventTotal, uint64(1); got != want {
t.Fatalf("server RetryEventTotal mismatch: got %d want %d", got, want)
}
if got, want := serverSnapshot.Retry.LastRetryAttempt, 1; got != want {
t.Fatalf("server LastRetryAttempt mismatch: got %d want %d", got, want)
}
if got, want := serverSnapshot.Retry.LastRetryError, serverErr.Error(); got != want {
t.Fatalf("server LastRetryError mismatch: got %q want %q", got, want)
}
if got, want := serverSnapshot.Retry.LastResultError, serverErr.Error(); got != want {
t.Fatalf("server LastResultError mismatch: got %q want %q", got, want)
}
if serverSnapshot.Retry.LastRetryAt.IsZero() {
t.Fatal("server LastRetryAt should be recorded")
}
if serverSnapshot.Retry.LastResultAt.IsZero() {
t.Fatal("server LastResultAt should be recorded")
}
}
func TestGetServerClientRuntimeSnapshotByKey(t *testing.T) {
const serverKey = "runtime-peer-server"
_ = DeleteServer(serverKey)
defer DeleteServer(serverKey)
server := NewServer(serverKey)
client := notify.NewClient()
secret := []byte("0123456789abcdef0123456789abcdef")
server.SetSecretKey(secret)
client.SetSecretKey(secret)
serverConn, clientConn := net.Pipe()
defer clientConn.Close()
listener := newSingleConnListener(serverConn)
defer listener.Close()
if err := server.ListenByListener(listener); err != nil {
t.Fatalf("ListenByListener failed: %v", err)
}
defer server.Stop()
if err := client.ConnectByConn(clientConn); err != nil {
t.Fatalf("ConnectByConn failed: %v", err)
}
defer client.Stop()
srv, err := Server(serverKey)
if err != nil {
t.Fatalf("Server lookup failed: %v", err)
}
var clientID string
deadline := time.Now().Add(time.Second)
for time.Now().Before(deadline) {
list := srv.GetLogicalConnList()
if len(list) == 1 && list[0] != nil {
clientID = list[0].ClientID
break
}
time.Sleep(10 * time.Millisecond)
}
if clientID == "" {
t.Fatal("server did not expose accepted client in time")
}
snapshot, err := GetServerClientRuntimeSnapshot(serverKey, clientID)
if err != nil {
t.Fatalf("GetServerClientRuntimeSnapshot failed: %v", err)
}
if got, want := snapshot.ClientID, clientID; got != want {
t.Fatalf("ClientID mismatch: got %q want %q", got, want)
}
if !snapshot.Alive {
t.Fatalf("Alive mismatch: got %v want true", snapshot.Alive)
}
if !snapshot.IdentityBound {
t.Fatal("IdentityBound mismatch: got false want true")
}
if !snapshot.TransportAttached {
t.Fatalf("TransportAttached mismatch: got %v want true", snapshot.TransportAttached)
}
logicalSnapshot, err := GetServerLogicalRuntimeSnapshot(serverKey, clientID)
if err != nil {
t.Fatalf("GetServerLogicalRuntimeSnapshot failed: %v", err)
}
if logicalSnapshot.ClientID != snapshot.ClientID || logicalSnapshot.TransportGeneration != snapshot.TransportGeneration {
t.Fatalf("logical runtime snapshot mismatch: got %+v want %+v", logicalSnapshot, snapshot)
}
}
func TestGetServerClientTransportRuntimeSnapshotByKey(t *testing.T) {
const serverKey = "runtime-transport-server"
_ = DeleteServer(serverKey)
defer DeleteServer(serverKey)
server := NewServer(serverKey)
client := notify.NewClient()
secret := []byte("0123456789abcdef0123456789abcdef")
server.SetSecretKey(secret)
client.SetSecretKey(secret)
serverConn, clientConn := net.Pipe()
defer clientConn.Close()
listener := newSingleConnListener(serverConn)
defer listener.Close()
if err := server.ListenByListener(listener); err != nil {
t.Fatalf("ListenByListener failed: %v", err)
}
defer server.Stop()
if err := client.ConnectByConn(clientConn); err != nil {
t.Fatalf("ConnectByConn failed: %v", err)
}
defer client.Stop()
srv, err := Server(serverKey)
if err != nil {
t.Fatalf("Server lookup failed: %v", err)
}
var clientID string
deadline := time.Now().Add(time.Second)
for time.Now().Before(deadline) {
list := srv.GetLogicalConnList()
if len(list) == 1 && list[0] != nil {
clientID = list[0].ClientID
if clientID != "" {
break
}
}
time.Sleep(10 * time.Millisecond)
}
if clientID == "" {
t.Fatal("server did not expose accepted client in time")
}
snapshot, ok, err := GetServerClientTransportRuntimeSnapshot(serverKey, clientID)
if err != nil {
t.Fatalf("GetServerClientTransportRuntimeSnapshot failed: %v", err)
}
if !ok {
t.Fatal("GetServerClientTransportRuntimeSnapshot should report current transport")
}
if got, want := snapshot.ClientID, clientID; got != want {
t.Fatalf("ClientID mismatch: got %q want %q", got, want)
}
if !snapshot.Attached {
t.Fatal("Attached mismatch: got false want true")
}
if !snapshot.HasRuntimeConn {
t.Fatal("HasRuntimeConn mismatch: got false want true")
}
if !snapshot.Current {
t.Fatal("Current mismatch: got false want true")
}
transportSnapshot, ok, err := GetServerTransportRuntimeSnapshot(serverKey, clientID)
if err != nil {
t.Fatalf("GetServerTransportRuntimeSnapshot failed: %v", err)
}
if !ok {
t.Fatal("GetServerTransportRuntimeSnapshot should report current transport")
}
if transportSnapshot.ClientID != snapshot.ClientID || transportSnapshot.TransportGeneration != snapshot.TransportGeneration {
t.Fatalf("transport runtime snapshot mismatch: got %+v want %+v", transportSnapshot, snapshot)
}
}
func TestGetServerLogicalAndTransportConnByKey(t *testing.T) {
const serverKey = "runtime-conn-object-server"
_ = DeleteServer(serverKey)
defer DeleteServer(serverKey)
server := NewServer(serverKey)
client := notify.NewClient()
secret := []byte("0123456789abcdef0123456789abcdef")
server.SetSecretKey(secret)
client.SetSecretKey(secret)
serverConn, clientConn := net.Pipe()
defer clientConn.Close()
listener := newSingleConnListener(serverConn)
defer listener.Close()
if err := server.ListenByListener(listener); err != nil {
t.Fatalf("ListenByListener failed: %v", err)
}
defer server.Stop()
if err := client.ConnectByConn(clientConn); err != nil {
t.Fatalf("ConnectByConn failed: %v", err)
}
defer client.Stop()
srv, err := Server(serverKey)
if err != nil {
t.Fatalf("Server lookup failed: %v", err)
}
var clientID string
deadline := time.Now().Add(time.Second)
for time.Now().Before(deadline) {
list := srv.GetLogicalConnList()
if len(list) == 1 && list[0] != nil && list[0].ClientID != "" {
clientID = list[0].ClientID
break
}
time.Sleep(10 * time.Millisecond)
}
if clientID == "" {
t.Fatal("server did not expose accepted logical conn in time")
}
logical, err := GetServerLogicalConn(serverKey, clientID)
if err != nil {
t.Fatalf("GetServerLogicalConn failed: %v", err)
}
if logical == nil || logical.ClientID != clientID {
t.Fatalf("logical conn mismatch: %+v", logical)
}
transport, ok, err := GetServerCurrentTransportConn(serverKey, clientID)
if err != nil {
t.Fatalf("GetServerCurrentTransportConn failed: %v", err)
}
if !ok {
t.Fatal("GetServerCurrentTransportConn should report current transport")
}
if transport == nil || transport.ClientID() != clientID || !transport.IsCurrent() {
t.Fatalf("transport conn mismatch: %+v", transport)
}
}
func TestGetServerDetachedClientRuntimeSnapshotsByKey(t *testing.T) {
const serverKey = "runtime-detached-server"
const clientKey = "runtime-detached-client"
_ = DeleteClient(clientKey)
_ = DeleteServer(serverKey)
defer DeleteClient(clientKey)
defer DeleteServer(serverKey)
server, err := NewModernPSKServer(serverKey, []byte("shared-secret"), testModernPSKOptions())
if err != nil {
t.Fatalf("NewModernPSKServer failed: %v", err)
}
server.SetDetachedClientKeepSec(30)
client, err := NewModernPSKClient(clientKey, []byte("shared-secret"), testModernPSKOptions())
if err != nil {
t.Fatalf("NewModernPSKClient failed: %v", err)
}
serverConn, clientConn := net.Pipe()
listener := newSingleConnListener(serverConn)
defer listener.Close()
defer clientConn.Close()
if err := server.ListenByListener(listener); err != nil {
t.Fatalf("ListenByListener failed: %v", err)
}
defer server.Stop()
if err := client.ConnectByConn(clientConn); err != nil {
t.Fatalf("ConnectByConn failed: %v", err)
}
defer client.Stop()
srv, err := Server(serverKey)
if err != nil {
t.Fatalf("Server lookup failed: %v", err)
}
deadline := time.Now().Add(time.Second)
var boundClientID string
for time.Now().Before(deadline) {
list := srv.GetClientLists()
if len(list) == 1 && list[0] != nil {
snapshot, snapErr := notify.GetClientConnRuntimeSnapshot(list[0])
if snapErr == nil && snapshot.IdentityBound {
boundClientID = snapshot.ClientID
break
}
}
time.Sleep(10 * time.Millisecond)
}
if boundClientID == "" {
t.Fatal("server did not bind accepted client identity in time")
}
if err := clientConn.Close(); err != nil {
t.Fatalf("close client conn failed: %v", err)
}
var snapshots []notify.ClientConnRuntimeSnapshot
deadline = time.Now().Add(time.Second)
for time.Now().Before(deadline) {
snapshots, err = GetServerDetachedClientRuntimeSnapshots(serverKey)
if err != nil {
t.Fatalf("GetServerDetachedClientRuntimeSnapshots failed: %v", err)
}
if len(snapshots) == 1 {
break
}
time.Sleep(10 * time.Millisecond)
}
if len(snapshots) != 1 {
t.Fatalf("detached snapshot count mismatch: got %d want 1", len(snapshots))
}
snapshot := snapshots[0]
if got, want := snapshot.ClientID, boundClientID; got != want {
t.Fatalf("detached snapshot ClientID mismatch: got %q want %q", got, want)
}
if snapshot.TransportAttached {
t.Fatalf("detached snapshot TransportAttached mismatch: got %v want false", snapshot.TransportAttached)
}
if !snapshot.IdentityBound {
t.Fatal("detached snapshot should remain identity bound")
}
if got, want := snapshot.DetachedClientKeepSec, int64(30); got != want {
t.Fatalf("detached snapshot keep seconds mismatch: got %d want %d", got, want)
}
if snapshot.TransportDetachedAt.IsZero() {
t.Fatal("detached snapshot should expose detach time")
}
}
func TestGetTransferSnapshotsByKeyDefaults(t *testing.T) {
const clientKey = "transfer-snapshot-client"
const serverKey = "transfer-snapshot-server"
_ = DeleteClient(clientKey)
_ = DeleteServer(serverKey)
defer DeleteClient(clientKey)
defer DeleteServer(serverKey)
NewClient(clientKey)
NewServer(serverKey)
clientSnapshots, err := GetClientTransferSnapshots(clientKey)
if err != nil {
t.Fatalf("GetClientTransferSnapshots failed: %v", err)
}
if len(clientSnapshots) != 0 {
t.Fatalf("client transfer snapshots count = %d, want 0", len(clientSnapshots))
}
serverSnapshots, err := GetServerTransferSnapshots(serverKey)
if err != nil {
t.Fatalf("GetServerTransferSnapshots failed: %v", err)
}
if len(serverSnapshots) != 0 {
t.Fatalf("server transfer snapshots count = %d, want 0", len(serverSnapshots))
}
if _, ok, err := GetClientTransferSnapshotByID(clientKey, "missing"); err != nil || ok {
t.Fatalf("GetClientTransferSnapshotByID = (%v, %v), want (nil, false)", err, ok)
}
if _, ok, err := GetClientTransferSnapshotByIDScope(clientKey, "missing", "scope-a"); err != nil || ok {
t.Fatalf("GetClientTransferSnapshotByIDScope = (%v, %v), want (nil, false)", err, ok)
}
if _, ok, err := GetClientTransferSnapshotByIDQuery(clientKey, "missing", notify.TransferSnapshotQuery{Scope: "scope-a"}); err != nil || ok {
t.Fatalf("GetClientTransferSnapshotByIDQuery = (%v, %v), want (nil, false)", err, ok)
}
if _, ok, err := GetServerTransferSnapshotByID(serverKey, "missing"); err != nil || ok {
t.Fatalf("GetServerTransferSnapshotByID = (%v, %v), want (nil, false)", err, ok)
}
if _, ok, err := GetServerTransferSnapshotByIDScope(serverKey, "missing", "scope-a"); err != nil || ok {
t.Fatalf("GetServerTransferSnapshotByIDScope = (%v, %v), want (nil, false)", err, ok)
}
if _, ok, err := GetServerTransferSnapshotByIDQuery(serverKey, "missing", notify.TransferSnapshotQuery{Scope: "scope-a"}); err != nil || ok {
t.Fatalf("GetServerTransferSnapshotByIDQuery = (%v, %v), want (nil, false)", err, ok)
}
}