feat(notify): 重构通信内核并补齐 stream/bulk/record/transfer 能力

- 引入 LogicalConn/TransportConn 分层,ClientConn 保留兼容适配层
  - 新增 Stream、Bulk、RecordStream 三条数据面能力及对应控制路径
  - 完成 transfer/file 传输内核与状态快照、诊断能力
  - 补齐 reconnect、inbound dispatcher、modern psk 等基础模块
  - 增加大规模回归、并发与基准测试覆盖
  - 更新依赖库
This commit is contained in:
2026-04-15 15:24:36 +08:00
parent d14d13c393
commit 09d972c7b7
216 changed files with 51374 additions and 1715 deletions
+429 -6
View File
@@ -2,7 +2,10 @@ package starnotify
import (
"b612.me/notify"
"context"
"errors"
"io"
"net"
"sync"
)
@@ -20,9 +23,23 @@ func init() {
func NewClient(key string) notify.Client {
client := notify.NewClient()
cmu.Lock()
starClient[key] = client
cmu.Unlock()
storeClient(key, client)
return client
}
func NewModernPSKClient(key string, sharedSecret []byte, opts *notify.ModernPSKOptions) (notify.Client, error) {
client := notify.NewClient()
if err := notify.UseModernPSKClient(client, sharedSecret, opts); err != nil {
return nil, err
}
storeClient(key, client)
return client, nil
}
func NewLegacySecurityClient(key string) notify.Client {
client := notify.NewClient()
notify.UseLegacySecurityClient(client)
storeClient(key, client)
return client
}
@@ -45,9 +62,23 @@ func DeleteClient(key string) (err error) {
func NewServer(key string) notify.Server {
server := notify.NewServer()
smu.Lock()
starServer[key] = server
smu.Unlock()
storeServer(key, server)
return server
}
func NewModernPSKServer(key string, sharedSecret []byte, opts *notify.ModernPSKOptions) (notify.Server, error) {
server := notify.NewServer()
if err := notify.UseModernPSKServer(server, sharedSecret, opts); err != nil {
return nil, err
}
storeServer(key, server)
return server, nil
}
func NewLegacySecurityServer(key string) notify.Server {
server := notify.NewServer()
notify.UseLegacySecurityServer(server)
storeServer(key, server)
return server
}
@@ -107,3 +138,395 @@ func Client(key string) (notify.Client, error) {
}
return client, nil
}
func OpenClientStreamFromReader(ctx context.Context, key string, src io.Reader, opt notify.StreamOpenCopyOptions) (notify.Stream, int64, error) {
client, err := Client(key)
if err != nil {
return nil, 0, err
}
return notify.OpenClientStreamFromReader(ctx, client, src, opt)
}
func OpenServerLogicalStreamFromReader(ctx context.Context, key string, logical *notify.LogicalConn, src io.Reader, opt notify.StreamOpenCopyOptions) (notify.Stream, int64, error) {
server, err := Server(key)
if err != nil {
return nil, 0, err
}
return notify.OpenServerLogicalStreamFromReader(ctx, server, logical, src, opt)
}
func OpenServerTransportStreamFromReader(ctx context.Context, key string, transport *notify.TransportConn, src io.Reader, opt notify.StreamOpenCopyOptions) (notify.Stream, int64, error) {
server, err := Server(key)
if err != nil {
return nil, 0, err
}
return notify.OpenServerTransportStreamFromReader(ctx, server, transport, src, opt)
}
func CopyStreamToWriter(ctx context.Context, stream notify.Stream, dst io.Writer, opt notify.StreamCopyOptions) (int64, error) {
return notify.CopyStreamToWriter(ctx, stream, dst, opt)
}
func NewTransferSourceFromReader(src io.Reader, size int64) (notify.TransferReaderAt, error) {
return notify.NewTransferSourceFromReader(src, size)
}
func NewTransferSinkFromWriter(dst io.Writer) (notify.TransferWriterAt, error) {
return notify.NewTransferSinkFromWriter(dst)
}
func NewTransferReaderFromSource(source notify.TransferReaderAt, offset int64) (io.Reader, error) {
return notify.NewTransferReaderFromSource(source, offset)
}
func NewTransferWriterFromSink(sink notify.TransferWriterAt, offset int64) (io.Writer, error) {
return notify.NewTransferWriterFromSink(sink, offset)
}
func UseModernPSKClient(key string, sharedSecret []byte, opts *notify.ModernPSKOptions) error {
client, err := Client(key)
if err != nil {
return err
}
return notify.UseModernPSKClient(client, sharedSecret, opts)
}
func UseModernPSKServer(key string, sharedSecret []byte, opts *notify.ModernPSKOptions) error {
server, err := Server(key)
if err != nil {
return err
}
return notify.UseModernPSKServer(server, sharedSecret, opts)
}
func UseLegacySecurityClient(key string) error {
client, err := Client(key)
if err != nil {
return err
}
notify.UseLegacySecurityClient(client)
return nil
}
func UseLegacySecurityServer(key string) error {
server, err := Server(key)
if err != nil {
return err
}
notify.UseLegacySecurityServer(server)
return nil
}
func UseSignalReliabilityClient(key string, opts *notify.SignalReliabilityOptions) error {
client, err := Client(key)
if err != nil {
return err
}
return notify.UseSignalReliabilityClient(client, opts)
}
func UseSignalReliabilityServer(key string, opts *notify.SignalReliabilityOptions) error {
server, err := Server(key)
if err != nil {
return err
}
return notify.UseSignalReliabilityServer(server, opts)
}
func ConnectClientWithRetry(key string, network string, addr string, opts *notify.ConnectRetryOptions) error {
return ConnectClientWithRetryCtx(context.Background(), key, network, addr, opts)
}
func ConnectClientWithRetryCtx(ctx context.Context, key string, network string, addr string, opts *notify.ConnectRetryOptions) error {
client, err := Client(key)
if err != nil {
return err
}
return notify.ConnectClientWithRetry(ctx, client, network, addr, opts)
}
func ConnectClientFactoryWithRetry(key string, dialFn func(context.Context) (net.Conn, error), opts *notify.ConnectRetryOptions) error {
return ConnectClientFactoryWithRetryCtx(context.Background(), key, dialFn, opts)
}
func ConnectClientFactoryWithRetryCtx(ctx context.Context, key string, dialFn func(context.Context) (net.Conn, error), opts *notify.ConnectRetryOptions) error {
client, err := Client(key)
if err != nil {
return err
}
return notify.ConnectClientFactoryWithRetry(ctx, client, dialFn, opts)
}
func ListenServerWithRetry(key string, network string, addr string, opts *notify.ConnectRetryOptions) error {
return ListenServerWithRetryCtx(context.Background(), key, network, addr, opts)
}
func ListenServerWithRetryCtx(ctx context.Context, key string, network string, addr string, opts *notify.ConnectRetryOptions) error {
server, err := Server(key)
if err != nil {
return err
}
return notify.ListenServerWithRetry(ctx, server, network, addr, opts)
}
func GetSignalReliabilityStatsClient(key string) (notify.SignalReliabilityStats, error) {
client, err := Client(key)
if err != nil {
return notify.SignalReliabilityStats{}, err
}
return notify.GetSignalReliabilityStatsClient(client)
}
func GetSignalReliabilityStatsServer(key string) (notify.SignalReliabilityStats, error) {
server, err := Server(key)
if err != nil {
return notify.SignalReliabilityStats{}, err
}
return notify.GetSignalReliabilityStatsServer(server)
}
func GetClientRuntimeSnapshot(key string) (notify.ClientRuntimeSnapshot, error) {
client, err := Client(key)
if err != nil {
return notify.ClientRuntimeSnapshot{}, err
}
return notify.GetClientRuntimeSnapshot(client)
}
func GetServerRuntimeSnapshot(key string) (notify.ServerRuntimeSnapshot, error) {
server, err := Server(key)
if err != nil {
return notify.ServerRuntimeSnapshot{}, err
}
return notify.GetServerRuntimeSnapshot(server)
}
func GetServerClientRuntimeSnapshot(serverKey string, clientID string) (notify.ClientConnRuntimeSnapshot, error) {
return GetServerLogicalRuntimeSnapshot(serverKey, clientID)
}
func GetServerLogicalRuntimeSnapshot(serverKey string, clientID string) (notify.ClientConnRuntimeSnapshot, error) {
server, err := Server(serverKey)
if err != nil {
return notify.ClientConnRuntimeSnapshot{}, err
}
logical := server.GetLogicalConn(clientID)
if logical == nil {
return notify.ClientConnRuntimeSnapshot{}, errors.New("Not Exists Yet")
}
return notify.GetLogicalConnRuntimeSnapshot(logical)
}
func GetServerLogicalConn(serverKey string, clientID string) (*notify.LogicalConn, error) {
server, err := Server(serverKey)
if err != nil {
return nil, err
}
client := server.GetLogicalConn(clientID)
if client == nil {
return nil, errors.New("Not Exists Yet")
}
return client, nil
}
func GetServerCurrentTransportConn(serverKey string, clientID string) (*notify.TransportConn, bool, error) {
server, err := Server(serverKey)
if err != nil {
return nil, false, err
}
transport := server.GetCurrentTransportConn(clientID)
if transport == nil {
if server.GetLogicalConn(clientID) == nil {
return nil, false, errors.New("Not Exists Yet")
}
return nil, false, nil
}
return transport, true, nil
}
func GetServerClientTransportRuntimeSnapshot(serverKey string, clientID string) (notify.TransportConnRuntimeSnapshot, bool, error) {
return GetServerTransportRuntimeSnapshot(serverKey, clientID)
}
func GetServerTransportRuntimeSnapshot(serverKey string, clientID string) (notify.TransportConnRuntimeSnapshot, bool, error) {
server, err := Server(serverKey)
if err != nil {
return notify.TransportConnRuntimeSnapshot{}, false, err
}
transport := server.GetCurrentTransportConn(clientID)
if transport == nil {
if server.GetLogicalConn(clientID) == nil {
return notify.TransportConnRuntimeSnapshot{}, false, errors.New("Not Exists Yet")
}
return notify.TransportConnRuntimeSnapshot{}, false, nil
}
snapshot, err := notify.GetTransportConnRuntimeSnapshot(transport)
if err != nil {
return notify.TransportConnRuntimeSnapshot{}, false, err
}
return snapshot, true, nil
}
func GetServerDetachedClientRuntimeSnapshots(serverKey string) ([]notify.ClientConnRuntimeSnapshot, error) {
server, err := Server(serverKey)
if err != nil {
return nil, err
}
return notify.GetServerDetachedClientRuntimeSnapshots(server)
}
func GetClientTransferSnapshots(key string) ([]notify.TransferSnapshot, error) {
client, err := Client(key)
if err != nil {
return nil, err
}
return notify.GetClientTransferSnapshots(client)
}
func GetServerTransferSnapshots(key string) ([]notify.TransferSnapshot, error) {
server, err := Server(key)
if err != nil {
return nil, err
}
return notify.GetServerTransferSnapshots(server)
}
func GetClientTransferSnapshotByID(key string, transferID string) (notify.TransferSnapshot, bool, error) {
client, err := Client(key)
if err != nil {
return notify.TransferSnapshot{}, false, err
}
return notify.GetClientTransferSnapshotByID(client, transferID)
}
func GetClientTransferSnapshotByIDScope(key string, transferID string, scope string) (notify.TransferSnapshot, bool, error) {
client, err := Client(key)
if err != nil {
return notify.TransferSnapshot{}, false, err
}
return notify.GetClientTransferSnapshotByIDScope(client, transferID, scope)
}
func GetClientTransferSnapshotByIDQuery(key string, transferID string, query notify.TransferSnapshotQuery) (notify.TransferSnapshot, bool, error) {
client, err := Client(key)
if err != nil {
return notify.TransferSnapshot{}, false, err
}
return notify.GetClientTransferSnapshotByIDQuery(client, transferID, query)
}
func GetServerTransferSnapshotByID(key string, transferID string) (notify.TransferSnapshot, bool, error) {
server, err := Server(key)
if err != nil {
return notify.TransferSnapshot{}, false, err
}
return notify.GetServerTransferSnapshotByID(server, transferID)
}
func GetServerTransferSnapshotByIDScope(key string, transferID string, scope string) (notify.TransferSnapshot, bool, error) {
server, err := Server(key)
if err != nil {
return notify.TransferSnapshot{}, false, err
}
return notify.GetServerTransferSnapshotByIDScope(server, transferID, scope)
}
func GetServerTransferSnapshotByIDQuery(key string, transferID string, query notify.TransferSnapshotQuery) (notify.TransferSnapshot, bool, error) {
server, err := Server(key)
if err != nil {
return notify.TransferSnapshot{}, false, err
}
return notify.GetServerTransferSnapshotByIDQuery(server, transferID, query)
}
func GetClientFileTransferActiveSummaries(key string) (notify.FileTransferSummaryGroup, error) {
client, err := Client(key)
if err != nil {
return notify.FileTransferSummaryGroup{}, err
}
return notify.GetClientFileTransferActiveSummaries(client)
}
func GetServerFileTransferActiveSummaries(key string) (notify.FileTransferSummaryGroup, error) {
server, err := Server(key)
if err != nil {
return notify.FileTransferSummaryGroup{}, err
}
return notify.GetServerFileTransferActiveSummaries(server)
}
func GetClientFileTransferCompletedSummaries(key string) (notify.FileTransferSummaryGroup, error) {
client, err := Client(key)
if err != nil {
return notify.FileTransferSummaryGroup{}, err
}
return notify.GetClientFileTransferCompletedSummaries(client)
}
func GetServerFileTransferCompletedSummaries(key string) (notify.FileTransferSummaryGroup, error) {
server, err := Server(key)
if err != nil {
return notify.FileTransferSummaryGroup{}, err
}
return notify.GetServerFileTransferCompletedSummaries(server)
}
func GetClientFileTransferFailedSummaries(key string) (notify.FileTransferSummaryGroup, error) {
client, err := Client(key)
if err != nil {
return notify.FileTransferSummaryGroup{}, err
}
return notify.GetClientFileTransferFailedSummaries(client)
}
func GetServerFileTransferFailedSummaries(key string) (notify.FileTransferSummaryGroup, error) {
server, err := Server(key)
if err != nil {
return notify.FileTransferSummaryGroup{}, err
}
return notify.GetServerFileTransferFailedSummaries(server)
}
func GetClientFileTransferLatestByFileID(key string, fileID string) (notify.FileTransferSummaryGroup, error) {
client, err := Client(key)
if err != nil {
return notify.FileTransferSummaryGroup{}, err
}
return notify.GetClientFileTransferLatestByFileID(client, fileID)
}
func GetServerFileTransferLatestByFileID(key string, fileID string) (notify.FileTransferSummaryGroup, error) {
server, err := Server(key)
if err != nil {
return notify.FileTransferSummaryGroup{}, err
}
return notify.GetServerFileTransferLatestByFileID(server, fileID)
}
func GetClientFileTransferLatestByFileIDQuery(key string, fileID string, query notify.FileTransferSummaryQuery) (notify.FileTransferSummaryGroup, error) {
client, err := Client(key)
if err != nil {
return notify.FileTransferSummaryGroup{}, err
}
return notify.GetClientFileTransferLatestByFileIDQuery(client, fileID, query)
}
func GetServerFileTransferLatestByFileIDQuery(key string, fileID string, query notify.FileTransferSummaryQuery) (notify.FileTransferSummaryGroup, error) {
server, err := Server(key)
if err != nil {
return notify.FileTransferSummaryGroup{}, err
}
return notify.GetServerFileTransferLatestByFileIDQuery(server, fileID, query)
}
func storeClient(key string, client notify.Client) {
cmu.Lock()
starClient[key] = client
cmu.Unlock()
}
func storeServer(key string, server notify.Server) {
smu.Lock()
starServer[key] = server
smu.Unlock()
}
+139
View File
@@ -0,0 +1,139 @@
package starnotify
import (
"errors"
"testing"
"b612.me/notify"
)
func testModernPSKOptions() *notify.ModernPSKOptions {
return &notify.ModernPSKOptions{
Salt: []byte("starnotify-modern-psk-test-salt"),
AAD: []byte("starnotify-modern-psk-test-aad"),
Argon2Params: notify.DefaultModernPSKOptions().Argon2Params,
}
}
func TestNewModernPSKClientStoresConfiguredClient(t *testing.T) {
const key = "modern-client"
_ = DeleteClient(key)
defer DeleteClient(key)
client, err := NewModernPSKClient(key, []byte("shared-secret"), testModernPSKOptions())
if err != nil {
t.Fatalf("NewModernPSKClient failed: %v", err)
}
if got := C(key); got != client {
t.Fatal("stored client does not match returned client")
}
if !client.SkipExchangeKey() {
t.Fatal("modern PSK client should skip legacy key exchange")
}
if len(client.GetSecretKey()) == 0 {
t.Fatal("modern PSK client should derive a transport key")
}
}
func TestNewModernPSKServerStoresConfiguredServer(t *testing.T) {
const key = "modern-server"
_ = DeleteServer(key)
defer DeleteServer(key)
server, err := NewModernPSKServer(key, []byte("shared-secret"), testModernPSKOptions())
if err != nil {
t.Fatalf("NewModernPSKServer failed: %v", err)
}
if got := S(key); got != server {
t.Fatal("stored server does not match returned server")
}
if len(server.GetSecretKey()) == 0 {
t.Fatal("modern PSK server should derive a transport key")
}
}
func TestNewLegacySecurityClientStoresConfiguredClient(t *testing.T) {
const key = "legacy-client"
_ = DeleteClient(key)
defer DeleteClient(key)
client := NewLegacySecurityClient(key)
if got := C(key); got != client {
t.Fatal("stored client does not match returned client")
}
if client.SkipExchangeKey() {
t.Fatal("legacy client should keep legacy key exchange enabled")
}
if len(client.GetSecretKey()) == 0 {
t.Fatal("legacy client should restore a transport key")
}
}
func TestNewLegacySecurityServerStoresConfiguredServer(t *testing.T) {
const key = "legacy-server"
_ = DeleteServer(key)
defer DeleteServer(key)
server := NewLegacySecurityServer(key)
if got := S(key); got != server {
t.Fatal("stored server does not match returned server")
}
if len(server.GetSecretKey()) == 0 {
t.Fatal("legacy server should restore a transport key")
}
}
func TestUseModernPSKClientConfiguresExistingClient(t *testing.T) {
const key = "existing-client"
_ = DeleteClient(key)
defer DeleteClient(key)
client := NewClient(key)
if err := UseModernPSKClient(key, []byte("shared-secret"), testModernPSKOptions()); err != nil {
t.Fatalf("UseModernPSKClient failed: %v", err)
}
if !client.SkipExchangeKey() {
t.Fatal("existing client should skip legacy key exchange after UseModernPSKClient")
}
}
func TestUseLegacySecurityClientConfiguresExistingClient(t *testing.T) {
const key = "existing-legacy-client"
_ = DeleteClient(key)
defer DeleteClient(key)
client := NewClient(key)
if err := UseLegacySecurityClient(key); err != nil {
t.Fatalf("UseLegacySecurityClient failed: %v", err)
}
if client.SkipExchangeKey() {
t.Fatal("existing client should re-enable legacy exchange after UseLegacySecurityClient")
}
}
func TestStarnotifyNewClientUsesModernDefault(t *testing.T) {
const key = "default-modern-client"
_ = DeleteClient(key)
defer DeleteClient(key)
client := NewClient(key)
err := client.Connect("tcp", "127.0.0.1:1")
if err == nil {
t.Fatal("default client should require security configuration before Connect")
}
if !errors.Is(err, notify.NewClient().Connect("tcp", "127.0.0.1:1")) {
t.Fatalf("default client error = %v, want notify modern-default behavior", err)
}
}
func TestUseModernPSKServerMissingKey(t *testing.T) {
if err := UseModernPSKServer("missing-server", []byte("shared-secret"), testModernPSKOptions()); err == nil {
t.Fatal("UseModernPSKServer should fail for missing key")
}
}
func TestUseLegacySecurityServerMissingKey(t *testing.T) {
if err := UseLegacySecurityServer("missing-server"); err == nil {
t.Fatal("UseLegacySecurityServer should fail for missing key")
}
}
+19
View File
@@ -0,0 +1,19 @@
package starnotify
import "b612.me/notify"
func GetClientDiagnosticsSnapshot(key string) (notify.ClientDiagnosticsSnapshot, error) {
client, err := Client(key)
if err != nil {
return notify.ClientDiagnosticsSnapshot{}, err
}
return notify.GetClientDiagnosticsSnapshot(client)
}
func GetServerDiagnosticsSnapshot(key string) (notify.ServerDiagnosticsSnapshot, error) {
server, err := Server(key)
if err != nil {
return notify.ServerDiagnosticsSnapshot{}, err
}
return notify.GetServerDiagnosticsSnapshot(server)
}
+40
View File
@@ -0,0 +1,40 @@
package starnotify
import "testing"
func TestGetDiagnosticsSnapshotByKeyDefaults(t *testing.T) {
const clientKey = "diagnostics-client"
const serverKey = "diagnostics-server"
_ = DeleteClient(clientKey)
_ = DeleteServer(serverKey)
defer DeleteClient(clientKey)
defer DeleteServer(serverKey)
NewClient(clientKey)
NewServer(serverKey)
clientSnapshot, err := GetClientDiagnosticsSnapshot(clientKey)
if err != nil {
t.Fatalf("GetClientDiagnosticsSnapshot failed: %v", err)
}
if got, want := clientSnapshot.Runtime.OwnerState, "idle"; got != want {
t.Fatalf("client Runtime.OwnerState = %q, want %q", got, want)
}
if clientSnapshot.Summary.LogicalCount != 0 || clientSnapshot.Summary.StreamCount != 0 || clientSnapshot.Summary.BulkCount != 0 || clientSnapshot.Summary.TransferCount != 0 {
t.Fatalf("unexpected default client summary: %+v", clientSnapshot.Summary)
}
serverSnapshot, err := GetServerDiagnosticsSnapshot(serverKey)
if err != nil {
t.Fatalf("GetServerDiagnosticsSnapshot failed: %v", err)
}
if got, want := serverSnapshot.Runtime.OwnerState, "idle"; got != want {
t.Fatalf("server Runtime.OwnerState = %q, want %q", got, want)
}
if len(serverSnapshot.Logicals) != 0 || len(serverSnapshot.CurrentTransports) != 0 {
t.Fatalf("unexpected default server diagnostics: %+v", serverSnapshot)
}
if serverSnapshot.Summary.LogicalCount != 0 || serverSnapshot.Summary.StreamCount != 0 || serverSnapshot.Summary.BulkCount != 0 || serverSnapshot.Summary.TransferCount != 0 {
t.Fatalf("unexpected default server summary: %+v", serverSnapshot.Summary)
}
}
+51
View File
@@ -0,0 +1,51 @@
package starnotify
import "testing"
import "b612.me/notify"
func TestGetFileTransferSummariesByKeyDefaults(t *testing.T) {
const clientKey = "file-transfer-public-client"
const serverKey = "file-transfer-public-server"
_ = DeleteClient(clientKey)
_ = DeleteServer(serverKey)
defer DeleteClient(clientKey)
defer DeleteServer(serverKey)
NewClient(clientKey)
NewServer(serverKey)
clientActive, err := GetClientFileTransferActiveSummaries(clientKey)
if err != nil {
t.Fatalf("GetClientFileTransferActiveSummaries failed: %v", err)
}
if len(clientActive.Send) != 0 || len(clientActive.Receive) != 0 {
t.Fatalf("client active summary should be empty: %+v", clientActive)
}
serverCompleted, err := GetServerFileTransferCompletedSummaries(serverKey)
if err != nil {
t.Fatalf("GetServerFileTransferCompletedSummaries failed: %v", err)
}
if len(serverCompleted.Send) != 0 || len(serverCompleted.Receive) != 0 {
t.Fatalf("server completed summary should be empty: %+v", serverCompleted)
}
clientLatest, err := GetClientFileTransferLatestByFileID(clientKey, "missing")
if err != nil {
t.Fatalf("GetClientFileTransferLatestByFileID failed: %v", err)
}
if len(clientLatest.Send) != 0 || len(clientLatest.Receive) != 0 {
t.Fatalf("client latest summary should be empty: %+v", clientLatest)
}
serverLatestQuery, err := GetServerFileTransferLatestByFileIDQuery(serverKey, "missing", notify.FileTransferSummaryQuery{
Scope: "scope-a",
})
if err != nil {
t.Fatalf("GetServerFileTransferLatestByFileIDQuery failed: %v", err)
}
if len(serverLatestQuery.Send) != 0 || len(serverLatestQuery.Receive) != 0 {
t.Fatalf("server latest query summary should be empty: %+v", serverLatestQuery)
}
}
+562
View File
@@ -0,0 +1,562 @@
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)
}
}
+118
View File
@@ -0,0 +1,118 @@
package starnotify
import (
"context"
"errors"
"testing"
"time"
"b612.me/notify"
)
func TestUseSignalReliabilityClient(t *testing.T) {
const key = "signal-reliable-client"
_ = DeleteClient(key)
defer DeleteClient(key)
NewClient(key)
err := UseSignalReliabilityClient(key, &notify.SignalReliabilityOptions{
AckTimeout: 50 * time.Millisecond,
SendRetry: 4,
ReceiveCacheLimit: 8,
})
if err != nil {
t.Fatalf("UseSignalReliabilityClient failed: %v", err)
}
}
func TestUseSignalReliabilityServer(t *testing.T) {
const key = "signal-reliable-server"
_ = DeleteServer(key)
defer DeleteServer(key)
NewServer(key)
opts := notify.DefaultSignalReliabilityOptions()
err := UseSignalReliabilityServer(key, &opts)
if err != nil {
t.Fatalf("UseSignalReliabilityServer failed: %v", err)
}
}
func TestUseSignalReliabilityMissingKey(t *testing.T) {
if err := UseSignalReliabilityClient("missing-client", nil); err == nil {
t.Fatal("UseSignalReliabilityClient should fail for missing client key")
}
if err := UseSignalReliabilityServer("missing-server", nil); err == nil {
t.Fatal("UseSignalReliabilityServer should fail for missing server key")
}
}
func TestGetSignalReliabilityStatsByKey(t *testing.T) {
const clientKey = "signal-reliable-stats-client"
const serverKey = "signal-reliable-stats-server"
_ = DeleteClient(clientKey)
_ = DeleteServer(serverKey)
defer DeleteClient(clientKey)
defer DeleteServer(serverKey)
NewClient(clientKey)
NewServer(serverKey)
clientStats, err := GetSignalReliabilityStatsClient(clientKey)
if err != nil {
t.Fatalf("GetSignalReliabilityStatsClient failed: %v", err)
}
if clientStats != (notify.SignalReliabilityStats{}) {
t.Fatalf("client stats mismatch: %+v", clientStats)
}
serverStats, err := GetSignalReliabilityStatsServer(serverKey)
if err != nil {
t.Fatalf("GetSignalReliabilityStatsServer failed: %v", err)
}
if serverStats != (notify.SignalReliabilityStats{}) {
t.Fatalf("server stats mismatch: %+v", serverStats)
}
}
func TestGetSignalReliabilityStatsMissingKey(t *testing.T) {
if _, err := GetSignalReliabilityStatsClient("missing-client"); err == nil {
t.Fatal("GetSignalReliabilityStatsClient should fail for missing key")
}
if _, err := GetSignalReliabilityStatsServer("missing-server"); err == nil {
t.Fatal("GetSignalReliabilityStatsServer should fail for missing key")
}
}
func TestConnectRetryWrappersContextCanceled(t *testing.T) {
const clientKey = "signal-reliable-retry-client"
const serverKey = "signal-reliable-retry-server"
_ = DeleteClient(clientKey)
_ = DeleteServer(serverKey)
defer DeleteClient(clientKey)
defer DeleteServer(serverKey)
NewClient(clientKey)
NewServer(serverKey)
ctx, cancel := context.WithCancel(context.Background())
cancel()
connectErr := ConnectClientWithRetryCtx(ctx, clientKey, "tcp", "127.0.0.1:1", &notify.ConnectRetryOptions{
MaxAttempts: 3,
BaseDelay: time.Millisecond,
MaxDelay: time.Millisecond,
})
if !errors.Is(connectErr, context.Canceled) {
t.Fatalf("ConnectClientWithRetryCtx error = %v, want %v", connectErr, context.Canceled)
}
listenErr := ListenServerWithRetryCtx(ctx, serverKey, "tcp", "127.0.0.1:1", &notify.ConnectRetryOptions{
MaxAttempts: 3,
BaseDelay: time.Millisecond,
MaxDelay: time.Millisecond,
})
if !errors.Is(listenErr, context.Canceled) {
t.Fatalf("ListenServerWithRetryCtx error = %v, want %v", listenErr, context.Canceled)
}
}