353 lines
9.9 KiB
Go
353 lines
9.9 KiB
Go
|
|
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)
|
||
|
|
}
|
||
|
|
}
|