notify/client_session_runtime_test.go

353 lines
9.9 KiB
Go
Raw Normal View History

package notify
import (
"b612.me/stario"
"context"
"io"
"math"
"net"
"sync/atomic"
"testing"
"time"
)
func TestClientWriteToTransportUsesRuntimeConn(t *testing.T) {
client := NewClient().(*ClientCommon)
fallbackLeft, fallbackRight := net.Pipe()
defer fallbackLeft.Close()
defer fallbackRight.Close()
runtimeLeft, runtimeRight := net.Pipe()
defer runtimeLeft.Close()
defer runtimeRight.Close()
client.conn = fallbackLeft
runtimeCtx, runtimeCancel := context.WithCancel(context.Background())
defer runtimeCancel()
client.setClientSessionRuntime(&clientSessionRuntime{
conn: runtimeLeft,
stopCtx: runtimeCtx,
stopFn: runtimeCancel,
epoch: 1,
})
payload := []byte("runtime-conn")
recvCh := make(chan []byte, 1)
errCh := make(chan error, 1)
go func() {
buf := make([]byte, len(payload))
_, err := io.ReadFull(runtimeRight, buf)
if err != nil {
errCh <- err
return
}
recvCh <- buf
}()
if err := client.writeToTransport(payload); err != nil {
t.Fatalf("writeToTransport failed: %v", err)
}
select {
case err := <-errCh:
t.Fatalf("runtime conn read failed: %v", err)
case got := <-recvCh:
if string(got) != string(payload) {
t.Fatalf("runtime payload mismatch: got %q want %q", string(got), string(payload))
}
case <-time.After(time.Second):
t.Fatal("runtime conn did not receive payload")
}
_ = fallbackRight.SetReadDeadline(time.Now().Add(20 * time.Millisecond))
buf := make([]byte, 1)
if _, err := fallbackRight.Read(buf); err == nil {
t.Fatal("fallback conn should not receive payload when runtime conn is active")
}
}
func TestClientMarkSessionStoppedUsesRuntimeStopFn(t *testing.T) {
client := NewClient().(*ClientCommon)
if !client.beginClientSessionStart() {
t.Fatal("beginClientSessionStart should succeed")
}
client.markSessionStarted()
runtimeCtx, runtimeCancel := context.WithCancel(context.Background())
defer runtimeCancel()
client.setClientSessionRuntime(&clientSessionRuntime{
stopCtx: runtimeCtx,
stopFn: runtimeCancel,
epoch: 1,
})
fallbackCtx, fallbackCancel := context.WithCancel(context.Background())
defer fallbackCancel()
client.stopCtx = fallbackCtx
client.stopFn = fallbackCancel
client.markSessionStopped("runtime stop", nil)
select {
case <-runtimeCtx.Done():
case <-time.After(time.Second):
t.Fatal("runtime stop context should be canceled by markSessionStopped")
}
select {
case <-fallbackCtx.Done():
t.Fatal("fallback owner stop context should not be canceled when runtime stopFn is active")
default:
}
rt := client.clientSessionRuntimeSnapshot()
if rt == nil {
t.Fatal("runtime snapshot should remain available after stop")
}
if rt.conn != nil || rt.queue != nil {
t.Fatalf("runtime transport should be cleared after stop: %+v", rt)
}
if rt.stopCtx == nil {
t.Fatalf("runtime stop context should be preserved after stop: %+v", rt)
}
}
func TestClientClearSessionRuntimeTransportPreservesStopState(t *testing.T) {
client := NewClient().(*ClientCommon)
left, right := net.Pipe()
defer left.Close()
defer right.Close()
stopCtx, stopFn := context.WithCancel(context.Background())
defer stopFn()
queue := stario.NewQueueCtx(stopCtx, 4, math.MaxUint32)
client.setClientSessionRuntime(&clientSessionRuntime{
conn: left,
stopCtx: stopCtx,
stopFn: stopFn,
queue: queue,
epoch: 7,
})
client.clearClientSessionRuntimeTransport()
rt := client.clientSessionRuntimeSnapshot()
if rt == nil {
t.Fatal("runtime snapshot should remain after transport clear")
}
if rt.conn != nil {
t.Fatalf("runtime conn should be cleared: %+v", rt)
}
if rt.queue != queue {
t.Fatalf("runtime queue should be preserved across pure transport clear: got %v want %v", rt.queue, queue)
}
if rt.stopCtx != stopCtx || rt.stopFn == nil || rt.epoch != 7 {
t.Fatalf("runtime control state should be preserved: %+v", rt)
}
if client.clientTransportAttachedSnapshot() {
t.Fatal("client transport should be marked detached after runtime clear")
}
if got := client.clientQueueSnapshot(); got != queue {
t.Fatalf("client queue snapshot should be preserved after transport clear: got %v want %v", got, queue)
}
}
func TestClientTransportBindingSnapshotUsesRuntimeBinding(t *testing.T) {
client := NewClient().(*ClientCommon)
left, right := net.Pipe()
defer left.Close()
defer right.Close()
stopCtx, stopFn := context.WithCancel(context.Background())
defer stopFn()
queue := stario.NewQueueCtx(stopCtx, 4, math.MaxUint32)
client.setClientSessionRuntime(&clientSessionRuntime{
transport: newTransportBinding(left, queue),
conn: left,
stopCtx: stopCtx,
stopFn: stopFn,
queue: queue,
epoch: 9,
})
binding := client.clientTransportBindingSnapshot()
if binding == nil {
t.Fatal("runtime transport binding should exist")
}
if got := binding.connSnapshot(); got != left {
t.Fatal("runtime transport binding conn should match runtime conn")
}
if got := binding.queueSnapshot(); got != queue {
t.Fatal("runtime transport binding queue should match runtime queue")
}
}
func TestRetireClientSessionRuntimeCancelsTransportOnly(t *testing.T) {
client := NewClient().(*ClientCommon)
stopCtx, stopFn := context.WithCancel(context.Background())
defer stopFn()
queue := stario.NewQueueCtx(stopCtx, 4, math.MaxUint32)
left, right := net.Pipe()
defer left.Close()
defer right.Close()
rt := newClientSessionRuntime(left, stopCtx, stopFn, queue, 3)
client.setClientSessionRuntime(rt)
client.retireClientSessionRuntime(rt, true)
transportStopCtx := client.clientTransportStopContextSnapshot()
if transportStopCtx == nil {
t.Fatal("transport stop context should exist")
}
select {
case <-transportStopCtx.Done():
case <-time.After(time.Second):
t.Fatal("transport stop context should be canceled by retireClientSessionRuntime")
}
select {
case <-client.clientStopContextSnapshot().Done():
t.Fatal("logical stop context should remain active when only retiring transport")
default:
}
}
func TestClientClearSessionRuntimeTransportPreservesQueueForEncoding(t *testing.T) {
client := NewClient().(*ClientCommon)
UseLegacySecurityClient(client)
left, right := net.Pipe()
defer left.Close()
defer right.Close()
stopCtx, stopFn := context.WithCancel(context.Background())
defer stopFn()
queue := stario.NewQueueCtx(stopCtx, 4, math.MaxUint32)
client.setClientSessionRuntime(&clientSessionRuntime{
conn: left,
stopCtx: stopCtx,
stopFn: stopFn,
queue: queue,
epoch: 8,
})
client.markSessionStarted()
defer client.markSessionStopped("test done", nil)
client.clearClientSessionRuntimeTransport()
data, err := client.encodeEnvelope(newSignalAckEnvelope(1003))
if err != nil {
t.Fatalf("encodeEnvelope failed after pure transport clear: %v", err)
}
if len(data) == 0 {
t.Fatal("encodeEnvelope should still return framed payload after pure transport clear")
}
}
func TestAttachClientSessionTransportRebindsRuntimeAndDispatchesOnNewConn(t *testing.T) {
client := NewClient().(*ClientCommon)
UseLegacySecurityClient(client)
stopCtx, stopFn := context.WithCancel(context.Background())
defer stopFn()
queue := stario.NewQueueCtx(stopCtx, 4, math.MaxUint32)
oldLeft, oldRight := net.Pipe()
defer oldRight.Close()
client.setClientSessionRuntime(&clientSessionRuntime{
conn: oldLeft,
stopCtx: stopCtx,
stopFn: stopFn,
queue: queue,
epoch: 11,
suppressGoodByeOnStop: &atomic.Bool{},
})
client.markSessionStarted()
defer client.markSessionStopped("test done", nil)
recvCh := make(chan Message, 1)
client.SetLink("reattach", func(message *Message) {
recvCh <- *message
})
newLeft, newRight := net.Pipe()
defer newRight.Close()
if err := client.attachClientSessionTransport(newLeft); err != nil {
t.Fatalf("attachClientSessionTransport failed: %v", err)
}
rt := client.clientSessionRuntimeSnapshot()
if rt == nil {
t.Fatal("runtime snapshot should exist after attach")
}
if rt.conn != newLeft || !rt.transportAttached || rt.queue != queue || rt.epoch != 11 {
t.Fatalf("attached runtime mismatch: %+v", rt)
}
env, err := wrapTransferMsgEnvelope(TransferMsg{
ID: 42,
Key: "reattach",
Value: []byte("ok"),
Type: MSG_ASYNC,
}, client.sequenceEn)
if err != nil {
t.Fatalf("wrapTransferMsgEnvelope failed: %v", err)
}
wire, err := client.encodeEnvelope(env)
if err != nil {
t.Fatalf("encodeEnvelope failed: %v", err)
}
if _, err := newRight.Write(wire); err != nil {
t.Fatalf("new transport write failed: %v", err)
}
select {
case message := <-recvCh:
if got, want := message.Key, "reattach"; got != want {
t.Fatalf("message key mismatch: got %q want %q", got, want)
}
if got, want := string(message.Value), "ok"; got != want {
t.Fatalf("message value mismatch: got %q want %q", got, want)
}
case <-time.After(time.Second):
t.Fatal("reattached transport did not dispatch message")
}
}
func TestSetClientSessionRuntimeStopsOldBindingWorkersOnReattach(t *testing.T) {
client := NewClient().(*ClientCommon)
stopCtx, stopFn := context.WithCancel(context.Background())
defer stopFn()
queue := stario.NewQueueCtx(stopCtx, 4, math.MaxUint32)
oldLeft, oldRight := net.Pipe()
defer oldLeft.Close()
defer oldRight.Close()
oldBinding := newTransportBinding(oldLeft, queue)
oldSender := oldBinding.bulkBatchSenderSnapshot()
client.setClientSessionRuntime(&clientSessionRuntime{
transport: oldBinding,
conn: oldLeft,
stopCtx: stopCtx,
stopFn: stopFn,
queue: queue,
epoch: 1,
})
newLeft, newRight := net.Pipe()
defer newLeft.Close()
defer newRight.Close()
newBinding := newTransportBinding(newLeft, queue)
client.setClientSessionRuntime(&clientSessionRuntime{
transport: newBinding,
conn: newLeft,
stopCtx: stopCtx,
stopFn: stopFn,
queue: queue,
epoch: 2,
})
err := oldSender.submit(context.Background(), []byte("payload"))
if err != errTransportDetached {
t.Fatalf("old sender submit after reattach = %v, want %v", err, errTransportDetached)
}
}