814 lines
24 KiB
Go
814 lines
24 KiB
Go
package proto_socket_test
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"errors"
|
|
"net"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"google.golang.org/protobuf/proto"
|
|
|
|
toki "git.toki-labs.com/toki/proto-socket/go"
|
|
"git.toki-labs.com/toki/proto-socket/go/packets"
|
|
)
|
|
|
|
type fakeTransport struct {
|
|
mu sync.Mutex
|
|
packets []*packets.PacketBase
|
|
}
|
|
|
|
func (f *fakeTransport) WritePacket(base *packets.PacketBase) error {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
f.packets = append(f.packets, base)
|
|
return nil
|
|
}
|
|
|
|
func (f *fakeTransport) Close() error {
|
|
return nil
|
|
}
|
|
|
|
func (f *fakeTransport) sent() []*packets.PacketBase {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
return append([]*packets.PacketBase{}, f.packets...)
|
|
}
|
|
|
|
func testParserMap() toki.ParserMap {
|
|
return toki.ParserMap{
|
|
toki.TypeNameOf(&packets.TestData{}): func(b []byte) (proto.Message, error) {
|
|
m := &packets.TestData{}
|
|
return m, proto.Unmarshal(b, m)
|
|
},
|
|
}
|
|
}
|
|
|
|
func TestSendRequestTypeMismatch(t *testing.T) {
|
|
transport := &fakeTransport{}
|
|
communicator := toki.NewCommunicator(transport, testParserMap())
|
|
defer communicator.Close()
|
|
|
|
errCh := make(chan error, 1)
|
|
go func() {
|
|
_, err := toki.SendRequestTyped[*packets.TestData, *packets.TestData](
|
|
communicator,
|
|
&packets.TestData{Index: 1, Message: "hello"},
|
|
time.Second,
|
|
)
|
|
errCh <- err
|
|
}()
|
|
|
|
var requestNonce int32
|
|
for deadline := time.Now().Add(time.Second); time.Now().Before(deadline); {
|
|
sent := transport.sent()
|
|
if len(sent) == 1 {
|
|
requestNonce = sent[0].GetNonce()
|
|
break
|
|
}
|
|
time.Sleep(time.Millisecond)
|
|
}
|
|
if requestNonce == 0 {
|
|
t.Fatal("request packet was not sent")
|
|
}
|
|
|
|
hb, err := proto.Marshal(&packets.HeartBeat{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
communicator.OnReceivedData(toki.TypeNameOf(&packets.HeartBeat{}), hb, 0, requestNonce)
|
|
|
|
err = <-errCh
|
|
if err == nil {
|
|
t.Fatal("expected response type mismatch error")
|
|
}
|
|
}
|
|
|
|
func TestSendRequestTimeout(t *testing.T) {
|
|
transport := &fakeTransport{}
|
|
communicator := toki.NewCommunicator(transport, testParserMap())
|
|
defer communicator.Close()
|
|
|
|
_, err := toki.SendRequestTyped[*packets.TestData, *packets.TestData](
|
|
communicator,
|
|
&packets.TestData{Index: 1, Message: "no response"},
|
|
25*time.Millisecond,
|
|
)
|
|
if err == nil {
|
|
t.Fatal("expected request timeout error")
|
|
}
|
|
if !strings.Contains(err.Error(), "timeout") {
|
|
t.Fatalf("expected timeout error, got %v", err)
|
|
}
|
|
if len(transport.sent()) != 1 {
|
|
t.Fatalf("expected one request packet to be sent, got %d", len(transport.sent()))
|
|
}
|
|
}
|
|
|
|
func TestListenerAndRequestListenerConflict(t *testing.T) {
|
|
transport := &fakeTransport{}
|
|
communicator := toki.NewCommunicator(transport, testParserMap())
|
|
defer communicator.Close()
|
|
|
|
toki.AddListenerTyped[*packets.TestData](communicator, func(*packets.TestData) {})
|
|
|
|
defer func() {
|
|
if recover() == nil {
|
|
t.Fatal("expected panic")
|
|
}
|
|
}()
|
|
toki.AddRequestListenerTyped[*packets.TestData, *packets.TestData](communicator, func(req *packets.TestData) (*packets.TestData, error) {
|
|
return req, nil
|
|
})
|
|
}
|
|
|
|
// TestInboundQueueFIFO: onReceivedData가 여러 메시지를 순서대로 전달한다.
|
|
func TestInboundQueueFIFO(t *testing.T) {
|
|
transport := &fakeTransport{}
|
|
communicator := toki.NewCommunicator(transport, testParserMap())
|
|
defer communicator.Close()
|
|
|
|
var mu sync.Mutex
|
|
var received []int32
|
|
|
|
toki.AddListenerTyped[*packets.TestData](communicator, func(m *packets.TestData) {
|
|
mu.Lock()
|
|
received = append(received, m.GetIndex())
|
|
mu.Unlock()
|
|
})
|
|
|
|
for i := int32(1); i <= 5; i++ {
|
|
data, _ := proto.Marshal(&packets.TestData{Index: i})
|
|
communicator.OnReceivedData(toki.TypeNameOf(&packets.TestData{}), data, int32(i), 0)
|
|
}
|
|
|
|
deadline := time.Now().Add(time.Second)
|
|
for time.Now().Before(deadline) {
|
|
mu.Lock()
|
|
n := len(received)
|
|
mu.Unlock()
|
|
if n == 5 {
|
|
break
|
|
}
|
|
time.Sleep(time.Millisecond)
|
|
}
|
|
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
if len(received) != 5 {
|
|
t.Fatalf("expected 5 messages, got %d", len(received))
|
|
}
|
|
for i, idx := range received {
|
|
if idx != int32(i+1) {
|
|
t.Fatalf("FIFO violated: index %d at position %d", idx, i)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestSlowRequestHandlerResponseOrder: 느린 request handler라도 자동 응답이 FIFO를 보존한다.
|
|
func TestSlowRequestHandlerResponseOrder(t *testing.T) {
|
|
transport := &fakeTransport{}
|
|
communicator := toki.NewCommunicator(transport, testParserMap())
|
|
defer communicator.Close()
|
|
|
|
toki.AddRequestListenerTyped[*packets.TestData, *packets.TestData](communicator, func(req *packets.TestData) (*packets.TestData, error) {
|
|
// 첫 번째 요청은 100ms 지연
|
|
if req.GetIndex() == 1 {
|
|
time.Sleep(100 * time.Millisecond)
|
|
}
|
|
return &packets.TestData{Index: req.GetIndex() + 100, Message: "echo"}, nil
|
|
})
|
|
|
|
data1, _ := proto.Marshal(&packets.TestData{Index: 1, Message: "first"})
|
|
data2, _ := proto.Marshal(&packets.TestData{Index: 2, Message: "second"})
|
|
communicator.OnReceivedData(toki.TypeNameOf(&packets.TestData{}), data1, 10, 0)
|
|
communicator.OnReceivedData(toki.TypeNameOf(&packets.TestData{}), data2, 20, 0)
|
|
|
|
// 두 응답이 전송될 때까지 대기
|
|
deadline := time.Now().Add(2 * time.Second)
|
|
for time.Now().Before(deadline) {
|
|
if len(transport.sent()) >= 2 {
|
|
break
|
|
}
|
|
time.Sleep(5 * time.Millisecond)
|
|
}
|
|
|
|
sent := transport.sent()
|
|
if len(sent) < 2 {
|
|
t.Fatalf("expected 2 responses, got %d", len(sent))
|
|
}
|
|
// 첫 번째 응답이 nonce=10에 대응, 두 번째가 nonce=20에 대응 (FIFO 순서)
|
|
if sent[0].GetResponseNonce() != 10 {
|
|
t.Errorf("expected first response for nonce 10, got %d", sent[0].GetResponseNonce())
|
|
}
|
|
if sent[1].GetResponseNonce() != 20 {
|
|
t.Errorf("expected second response for nonce 20, got %d", sent[1].GetResponseNonce())
|
|
}
|
|
}
|
|
|
|
// TestCloseCanelsPendingRequests: Close 시 pending sendRequest가 ErrNotConnected로 완료된다.
|
|
func TestCloseCancelsPendingRequests(t *testing.T) {
|
|
transport := &fakeTransport{}
|
|
communicator := toki.NewCommunicator(transport, testParserMap())
|
|
|
|
errCh := make(chan error, 1)
|
|
go func() {
|
|
_, err := toki.SendRequestTyped[*packets.TestData, *packets.TestData](
|
|
communicator,
|
|
&packets.TestData{Index: 99, Message: "pending"},
|
|
5*time.Second,
|
|
)
|
|
errCh <- err
|
|
}()
|
|
|
|
// 요청 패킷이 전송될 때까지 대기
|
|
deadline := time.Now().Add(time.Second)
|
|
for time.Now().Before(deadline) {
|
|
if len(transport.sent()) == 1 {
|
|
break
|
|
}
|
|
time.Sleep(time.Millisecond)
|
|
}
|
|
|
|
communicator.Close()
|
|
|
|
select {
|
|
case err := <-errCh:
|
|
if !errors.Is(err, toki.ErrNotConnected) {
|
|
t.Fatalf("expected ErrNotConnected, got %v", err)
|
|
}
|
|
case <-time.After(time.Second):
|
|
t.Fatal("timed out waiting for pending request to be cancelled")
|
|
}
|
|
}
|
|
|
|
// TestInboundBackpressureBlocksAndResumes: inbound channel이 가득 차면 추가 enqueue goroutine이 block되고,
|
|
// handler가 release된 후 resume되는지 검증한다.
|
|
func TestInboundBackpressureBlocksAndResumes(t *testing.T) {
|
|
transport := &fakeTransport{}
|
|
communicator := toki.NewCommunicator(transport, testParserMap())
|
|
defer communicator.Close()
|
|
|
|
handlerBlock := make(chan struct{})
|
|
handlerRelease := make(chan struct{})
|
|
var blockOnce sync.Once
|
|
|
|
toki.AddRequestListenerTyped[*packets.TestData, *packets.TestData](communicator, func(req *packets.TestData) (*packets.TestData, error) {
|
|
blockOnce.Do(func() {
|
|
close(handlerBlock)
|
|
})
|
|
<-handlerRelease
|
|
return &packets.TestData{Index: req.GetIndex() + 100, Message: "echo"}, nil
|
|
})
|
|
|
|
data, _ := proto.Marshal(&packets.TestData{Index: 1})
|
|
|
|
// 1번째: 핸들러 내부에서 블록
|
|
communicator.EnqueueInbound(toki.TypeNameOf(&packets.TestData{}), data, 1, 0)
|
|
|
|
// 핸들러 시작 대기
|
|
select {
|
|
case <-handlerBlock:
|
|
case <-time.After(time.Second):
|
|
t.Fatal("timed out waiting for request handler to start")
|
|
}
|
|
|
|
// 2 ~ 65번째(총 64개)를 Enqueue하여 큐 버퍼(capacity=64)를 가득 채운다
|
|
for i := 2; i <= 65; i++ {
|
|
communicator.EnqueueInbound(toki.TypeNameOf(&packets.TestData{}), data, int32(i), 0)
|
|
}
|
|
|
|
// 66번째 Enqueue 시도는 block되어야 함
|
|
enqueueDone := make(chan struct{})
|
|
go func() {
|
|
communicator.EnqueueInbound(toki.TypeNameOf(&packets.TestData{}), data, 66, 0)
|
|
close(enqueueDone)
|
|
}()
|
|
|
|
// block되어 대기 중인지 50ms 대기하며 확인
|
|
select {
|
|
case <-enqueueDone:
|
|
t.Fatal("expected EnqueueInbound to block on full queue, but it completed")
|
|
case <-time.After(50 * time.Millisecond):
|
|
// OK
|
|
}
|
|
|
|
// 핸들러 해제
|
|
close(handlerRelease)
|
|
|
|
// 이제 66번째 enqueue가 완료되어야 함
|
|
select {
|
|
case <-enqueueDone:
|
|
// OK
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("timed out waiting for blocked EnqueueInbound to resume")
|
|
}
|
|
}
|
|
|
|
// TestGracefulCloseDrainsInFlightRequestHandler: in-flight handler가 실행 중일 때,
|
|
// Close()가 호출되어도 남은 request가 정상적으로 drain(완료)되고 그 자동 응답이 기록되는지 검증한다.
|
|
func TestGracefulCloseDrainsInFlightRequestHandler(t *testing.T) {
|
|
transport := &fakeTransport{}
|
|
communicator := toki.NewCommunicator(transport, testParserMap())
|
|
|
|
handlerBlock := make(chan struct{})
|
|
handlerRelease := make(chan struct{})
|
|
|
|
toki.AddRequestListenerTyped[*packets.TestData, *packets.TestData](communicator, func(req *packets.TestData) (*packets.TestData, error) {
|
|
close(handlerBlock)
|
|
<-handlerRelease
|
|
return &packets.TestData{Index: req.GetIndex() + 100, Message: "graceful_response"}, nil
|
|
})
|
|
|
|
data, _ := proto.Marshal(&packets.TestData{Index: 5})
|
|
communicator.EnqueueInbound(toki.TypeNameOf(&packets.TestData{}), data, 42, 0)
|
|
|
|
// 핸들러가 실행 시작할 때까지 대기
|
|
select {
|
|
case <-handlerBlock:
|
|
case <-time.After(time.Second):
|
|
t.Fatal("timed out waiting for request handler to start")
|
|
}
|
|
|
|
// 다른 고루틴에서 50ms 후 handlerRelease 하도록 설정
|
|
go func() {
|
|
time.Sleep(50 * time.Millisecond)
|
|
close(handlerRelease)
|
|
}()
|
|
|
|
// Close()를 비동기가 아닌 동기로 호출. Close()는 receiveDone이 닫힐 때까지 대기함
|
|
err := communicator.Close()
|
|
if err != nil {
|
|
t.Fatalf("unexpected error on Close: %v", err)
|
|
}
|
|
|
|
// Close()가 리턴했다는 것은 receiveLoop가 완전히 종료(drain 완료)되었다는 뜻임
|
|
if communicator.IsAlive() {
|
|
t.Fatal("expected communicator to be inactive after Close")
|
|
}
|
|
|
|
// 자동 응답이 transport에 잘 들어갔는지 검증
|
|
sent := transport.sent()
|
|
if len(sent) != 1 {
|
|
t.Fatalf("expected 1 sent packet, got %d", len(sent))
|
|
}
|
|
response := sent[0]
|
|
if response.GetResponseNonce() != 42 {
|
|
t.Errorf("expected response nonce 42, got %d", response.GetResponseNonce())
|
|
}
|
|
var resData packets.TestData
|
|
err = proto.Unmarshal(response.GetData(), &resData)
|
|
if err != nil {
|
|
t.Fatalf("failed to unmarshal response data: %v", err)
|
|
}
|
|
if resData.GetIndex() != 105 || resData.GetMessage() != "graceful_response" {
|
|
t.Errorf("unexpected response content: index=%d message=%s", resData.GetIndex(), resData.GetMessage())
|
|
}
|
|
}
|
|
|
|
type fakeGatewayItem struct {
|
|
seq int
|
|
typeName string
|
|
data []byte
|
|
nonce int32
|
|
}
|
|
|
|
func TestWorkerGatewayReorderAndDispatch(t *testing.T) {
|
|
transport := &fakeTransport{}
|
|
communicator := toki.NewCommunicator(transport, testParserMap())
|
|
defer communicator.Close()
|
|
|
|
var mu sync.Mutex
|
|
var received []int32
|
|
|
|
toki.AddListenerTyped[*packets.TestData](communicator, func(m *packets.TestData) {
|
|
mu.Lock()
|
|
received = append(received, m.GetIndex())
|
|
mu.Unlock()
|
|
})
|
|
|
|
// Fake gateway: out-of-order 아이템들을 seq 오름차순으로 정렬 후 coordinator로 밀어넣음
|
|
items := []fakeGatewayItem{
|
|
{seq: 2, typeName: toki.TypeNameOf(&packets.TestData{}), data: func() []byte { d, _ := proto.Marshal(&packets.TestData{Index: 2}); return d }(), nonce: 2},
|
|
{seq: 1, typeName: toki.TypeNameOf(&packets.TestData{}), data: func() []byte { d, _ := proto.Marshal(&packets.TestData{Index: 1}); return d }(), nonce: 1},
|
|
}
|
|
|
|
// Reorder by seq
|
|
for i := 0; i < len(items); i++ {
|
|
for j := i + 1; j < len(items); j++ {
|
|
if items[i].seq > items[j].seq {
|
|
items[i], items[j] = items[j], items[i]
|
|
}
|
|
}
|
|
}
|
|
|
|
// Dispatch to coordinator in reordered sequence
|
|
for _, item := range items {
|
|
communicator.OnReceivedData(item.typeName, item.data, item.nonce, 0)
|
|
}
|
|
|
|
deadline := time.Now().Add(time.Second)
|
|
for time.Now().Before(deadline) {
|
|
mu.Lock()
|
|
n := len(received)
|
|
mu.Unlock()
|
|
if n == 2 {
|
|
break
|
|
}
|
|
time.Sleep(time.Millisecond)
|
|
}
|
|
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
if len(received) != 2 {
|
|
t.Fatalf("expected 2 messages, got %d", len(received))
|
|
}
|
|
if received[0] != 1 || received[1] != 2 {
|
|
t.Fatalf("seq ordering was not preserved, got: %v", received)
|
|
}
|
|
}
|
|
|
|
// TestInboundGatewayPreservesDispatchOrder: with the worker-pool gateway
|
|
// attached, a burst of raw frames decoded in parallel is still dispatched to the
|
|
// listener in input order, proving the coordinator keeps FIFO ownership.
|
|
func TestInboundGatewayPreservesDispatchOrder(t *testing.T) {
|
|
transport := &fakeTransport{}
|
|
communicator := toki.NewCommunicator(transport, testParserMap())
|
|
communicator.EnableInboundGateway(4, 64)
|
|
defer communicator.Close()
|
|
|
|
var mu sync.Mutex
|
|
var received []int32
|
|
|
|
toki.AddListenerTyped[*packets.TestData](communicator, func(m *packets.TestData) {
|
|
mu.Lock()
|
|
received = append(received, m.GetIndex())
|
|
mu.Unlock()
|
|
})
|
|
|
|
const total = 200
|
|
typeName := toki.TypeNameOf(&packets.TestData{})
|
|
for i := int32(1); i <= total; i++ {
|
|
data, _ := proto.Marshal(&packets.TestData{Index: i})
|
|
frame, err := proto.Marshal(&packets.PacketBase{TypeName: typeName, Nonce: i, Data: data})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
communicator.OnReceivedFrame(frame)
|
|
}
|
|
|
|
deadline := time.Now().Add(3 * time.Second)
|
|
for time.Now().Before(deadline) {
|
|
mu.Lock()
|
|
n := len(received)
|
|
mu.Unlock()
|
|
if n == total {
|
|
break
|
|
}
|
|
time.Sleep(time.Millisecond)
|
|
}
|
|
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
if len(received) != total {
|
|
t.Fatalf("expected %d messages, got %d", total, len(received))
|
|
}
|
|
for i, idx := range received {
|
|
if idx != int32(i+1) {
|
|
t.Fatalf("gateway dispatch order violated at position %d: got index %d", i, idx)
|
|
}
|
|
}
|
|
}
|
|
|
|
// frameTCP builds a TCP wire frame: 4-byte big-endian length prefix + PacketBase.
|
|
func frameTCP(t *testing.T, base *packets.PacketBase) []byte {
|
|
t.Helper()
|
|
b, err := proto.Marshal(base)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
out := make([]byte, 4+len(b))
|
|
binary.BigEndian.PutUint32(out[:4], uint32(len(b)))
|
|
copy(out[4:], b)
|
|
return out
|
|
}
|
|
|
|
// countingGateway is a test InboundGateway that records every Submit and then
|
|
// forwards the decoded frame to the supplied sink. It lets a transport read-path
|
|
// test prove the read loop actually routes raw frames through the gateway rather
|
|
// than decoding inline and bypassing it.
|
|
type countingGateway struct {
|
|
onSubmit func(toki.InboundFrame)
|
|
}
|
|
|
|
func (g *countingGateway) Submit(frame toki.InboundFrame) { g.onSubmit(frame) }
|
|
func (g *countingGateway) Close() {}
|
|
|
|
// TestTcpReadLoopRoutesThroughGateway: a real TcpClient.readLoop must hand raw
|
|
// frames to the attached gateway. The counting gateway fails the test if the
|
|
// read loop decodes inline and bypasses the gateway (the original wiring gap).
|
|
func TestTcpReadLoopRoutesThroughGateway(t *testing.T) {
|
|
serverConn, clientConn := net.Pipe()
|
|
// intervalSec=0 disables heartbeats, so the client never writes to the pipe.
|
|
client := toki.NewTcpClient(clientConn, 0, 0, testParserMap())
|
|
defer client.Close()
|
|
|
|
var mu sync.Mutex
|
|
var received []int32
|
|
submitted := 0
|
|
|
|
gw := &countingGateway{onSubmit: func(frame toki.InboundFrame) {
|
|
mu.Lock()
|
|
submitted++
|
|
mu.Unlock()
|
|
base := &packets.PacketBase{}
|
|
if err := proto.Unmarshal(frame.Bytes, base); err != nil {
|
|
return
|
|
}
|
|
client.OnReceivedData(base.GetTypeName(), base.GetData(), base.GetNonce(), base.GetResponseNonce())
|
|
}}
|
|
client.AttachInboundGateway(gw)
|
|
|
|
toki.AddListenerTyped[*packets.TestData](&client.Communicator, func(m *packets.TestData) {
|
|
mu.Lock()
|
|
received = append(received, m.GetIndex())
|
|
mu.Unlock()
|
|
})
|
|
|
|
const total = 20
|
|
typeName := toki.TypeNameOf(&packets.TestData{})
|
|
frames := make([][]byte, 0, total)
|
|
for i := int32(1); i <= total; i++ {
|
|
data, err := proto.Marshal(&packets.TestData{Index: i})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
frames = append(frames, frameTCP(t, &packets.PacketBase{TypeName: typeName, Nonce: i, Data: data}))
|
|
}
|
|
go func() {
|
|
for _, f := range frames {
|
|
if _, err := serverConn.Write(f); err != nil {
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
|
|
deadline := time.Now().Add(3 * time.Second)
|
|
for time.Now().Before(deadline) {
|
|
mu.Lock()
|
|
n := len(received)
|
|
mu.Unlock()
|
|
if n == total {
|
|
break
|
|
}
|
|
time.Sleep(time.Millisecond)
|
|
}
|
|
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
if submitted != total {
|
|
t.Fatalf("read loop bypassed gateway: submitted=%d, want %d", submitted, total)
|
|
}
|
|
if len(received) != total {
|
|
t.Fatalf("expected %d messages, got %d", total, len(received))
|
|
}
|
|
for i, idx := range received {
|
|
if idx != int32(i+1) {
|
|
t.Fatalf("dispatch order violated at position %d: got index %d", i, idx)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestTcpReadLoopWorkerGatewayPreservesOrder: a real TcpClient.readLoop with the
|
|
// production worker-pool gateway (EnableInboundGateway) still dispatches frames
|
|
// to the listener in input order despite parallel decode.
|
|
func TestTcpReadLoopWorkerGatewayPreservesOrder(t *testing.T) {
|
|
serverConn, clientConn := net.Pipe()
|
|
client := toki.NewTcpClient(clientConn, 0, 0, testParserMap())
|
|
defer client.Close()
|
|
client.EnableInboundGateway(4, 64)
|
|
|
|
var mu sync.Mutex
|
|
var received []int32
|
|
toki.AddListenerTyped[*packets.TestData](&client.Communicator, func(m *packets.TestData) {
|
|
mu.Lock()
|
|
received = append(received, m.GetIndex())
|
|
mu.Unlock()
|
|
})
|
|
|
|
const total = 100
|
|
typeName := toki.TypeNameOf(&packets.TestData{})
|
|
frames := make([][]byte, 0, total)
|
|
for i := int32(1); i <= total; i++ {
|
|
data, err := proto.Marshal(&packets.TestData{Index: i})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
frames = append(frames, frameTCP(t, &packets.PacketBase{TypeName: typeName, Nonce: i, Data: data}))
|
|
}
|
|
go func() {
|
|
for _, f := range frames {
|
|
if _, err := serverConn.Write(f); err != nil {
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
|
|
deadline := time.Now().Add(3 * time.Second)
|
|
for time.Now().Before(deadline) {
|
|
mu.Lock()
|
|
n := len(received)
|
|
mu.Unlock()
|
|
if n == total {
|
|
break
|
|
}
|
|
time.Sleep(time.Millisecond)
|
|
}
|
|
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
if len(received) != total {
|
|
t.Fatalf("expected %d messages, got %d", total, len(received))
|
|
}
|
|
for i, idx := range received {
|
|
if idx != int32(i+1) {
|
|
t.Fatalf("dispatch order violated at position %d: got index %d", i, idx)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestTcpReadLoopParseErrorDisconnects: with no gateway, a malformed frame in
|
|
// the real read loop must still trigger the parse-error disconnect.
|
|
func TestTcpReadLoopParseErrorDisconnects(t *testing.T) {
|
|
serverConn, clientConn := net.Pipe()
|
|
client := toki.NewTcpClient(clientConn, 0, 0, testParserMap())
|
|
defer client.Close()
|
|
|
|
// length=3 prefix followed by bytes that are not valid protobuf wire format.
|
|
bad := []byte{0, 0, 0, 3, 0xff, 0xff, 0xff}
|
|
go func() { _, _ = serverConn.Write(bad) }()
|
|
|
|
deadline := time.Now().Add(2 * time.Second)
|
|
for time.Now().Before(deadline) {
|
|
if r := client.DisconnectInfo().Reason; r != toki.DisconnectReasonUnknown {
|
|
break
|
|
}
|
|
time.Sleep(time.Millisecond)
|
|
}
|
|
|
|
info := client.DisconnectInfo()
|
|
if info.Reason != toki.DisconnectReasonTCPPacketParse {
|
|
t.Fatalf("expected %s disconnect, got %q", toki.DisconnectReasonTCPPacketParse, info.Reason)
|
|
}
|
|
}
|
|
|
|
// TestTcpReadLoopGatewayParseErrorDisconnects: with the gateway attached, a
|
|
// malformed frame decoded off the read loop must still surface as the same
|
|
// parse-error disconnect via the gateway error path.
|
|
func TestTcpReadLoopGatewayParseErrorDisconnects(t *testing.T) {
|
|
serverConn, clientConn := net.Pipe()
|
|
client := toki.NewTcpClient(clientConn, 0, 0, testParserMap())
|
|
defer client.Close()
|
|
client.EnableInboundGateway(2, 16)
|
|
|
|
// length=3 prefix followed by bytes that are not valid protobuf wire format.
|
|
bad := []byte{0, 0, 0, 3, 0xff, 0xff, 0xff}
|
|
go func() { _, _ = serverConn.Write(bad) }()
|
|
|
|
deadline := time.Now().Add(2 * time.Second)
|
|
for time.Now().Before(deadline) {
|
|
if r := client.DisconnectInfo().Reason; r != toki.DisconnectReasonUnknown {
|
|
break
|
|
}
|
|
time.Sleep(time.Millisecond)
|
|
}
|
|
|
|
info := client.DisconnectInfo()
|
|
if info.Reason != toki.DisconnectReasonTCPPacketParse {
|
|
t.Fatalf("expected %s disconnect via gateway error path, got %q", toki.DisconnectReasonTCPPacketParse, info.Reason)
|
|
}
|
|
}
|
|
|
|
func TestAliasCollisionOnInit(t *testing.T) {
|
|
defer func() {
|
|
if recover() == nil {
|
|
t.Fatal("expected panic on alias collision")
|
|
}
|
|
}()
|
|
|
|
badParserMap := toki.ParserMap{
|
|
"a.b.TestData": func(b []byte) (proto.Message, error) {
|
|
return &packets.TestData{}, nil
|
|
},
|
|
"x.y.TestData": func(b []byte) (proto.Message, error) {
|
|
return &packets.TestData{}, nil
|
|
},
|
|
}
|
|
transport := &fakeTransport{}
|
|
_ = toki.NewCommunicator(transport, badParserMap)
|
|
}
|
|
|
|
func TestLegacyAliasRouting(t *testing.T) {
|
|
transport := &fakeTransport{}
|
|
communicator := toki.NewCommunicator(transport, testParserMap())
|
|
defer communicator.Close()
|
|
|
|
// 1. Test Listener routing with legacy simple name
|
|
var listenerCalled bool
|
|
var receivedMsg proto.Message
|
|
toki.AddListenerTyped[*packets.TestData](communicator, func(m *packets.TestData) {
|
|
listenerCalled = true
|
|
receivedMsg = m
|
|
})
|
|
|
|
testMsgBytes, _ := proto.Marshal(&packets.TestData{Index: 42, Message: "test"})
|
|
communicator.OnReceivedData("TestData", testMsgBytes, 1, 0)
|
|
|
|
deadline := time.Now().Add(time.Second)
|
|
for time.Now().Before(deadline) {
|
|
if listenerCalled {
|
|
break
|
|
}
|
|
time.Sleep(time.Millisecond)
|
|
}
|
|
if !listenerCalled {
|
|
t.Fatal("listener was not called with legacy simple name")
|
|
}
|
|
if receivedMsg.(*packets.TestData).Index != 42 {
|
|
t.Fatalf("expected Index 42, got %d", receivedMsg.(*packets.TestData).Index)
|
|
}
|
|
|
|
// 2. Test Pending request matching with legacy simple name
|
|
errCh := make(chan error, 1)
|
|
var responseMsg proto.Message
|
|
go func() {
|
|
res, err := toki.SendRequestTyped[*packets.TestData, *packets.TestData](
|
|
communicator,
|
|
&packets.TestData{Index: 1, Message: "req"},
|
|
time.Second,
|
|
)
|
|
responseMsg = res
|
|
errCh <- err
|
|
}()
|
|
|
|
var requestNonce int32
|
|
for deadline = time.Now().Add(time.Second); time.Now().Before(deadline); {
|
|
sent := transport.sent()
|
|
if len(sent) == 1 {
|
|
requestNonce = sent[0].GetNonce()
|
|
break
|
|
}
|
|
time.Sleep(time.Millisecond)
|
|
}
|
|
if requestNonce == 0 {
|
|
t.Fatal("request packet was not sent")
|
|
}
|
|
|
|
communicator.OnReceivedData("TestData", testMsgBytes, 0, requestNonce)
|
|
|
|
err := <-errCh
|
|
if err != nil {
|
|
t.Fatalf("expected successful response matching, got error: %v", err)
|
|
}
|
|
if responseMsg.(*packets.TestData).Index != 42 {
|
|
t.Fatalf("expected response Index 42, got %d", responseMsg.(*packets.TestData).Index)
|
|
}
|
|
}
|
|
|
|
func TestRequestHandlerAliasSimpleNameInbound(t *testing.T) {
|
|
transport := &fakeTransport{}
|
|
communicator := toki.NewCommunicator(transport, testParserMap())
|
|
defer communicator.Close()
|
|
|
|
toki.AddRequestListenerTyped[*packets.TestData, *packets.TestData](communicator, func(req *packets.TestData) (*packets.TestData, error) {
|
|
return &packets.TestData{Index: req.GetIndex() + 1, Message: "echo"}, nil
|
|
})
|
|
|
|
reqBytes, _ := proto.Marshal(&packets.TestData{Index: 7, Message: "req"})
|
|
communicator.OnReceivedData("TestData", reqBytes, 42, 0)
|
|
|
|
deadline := time.Now().Add(time.Second)
|
|
for time.Now().Before(deadline) {
|
|
if len(transport.sent()) >= 1 {
|
|
break
|
|
}
|
|
time.Sleep(time.Millisecond)
|
|
}
|
|
|
|
sent := transport.sent()
|
|
if len(sent) != 1 {
|
|
t.Fatalf("expected 1 response packet, got %d", len(sent))
|
|
}
|
|
if sent[0].GetResponseNonce() != 42 {
|
|
t.Errorf("expected responseNonce 42, got %d", sent[0].GetResponseNonce())
|
|
}
|
|
decoded := &packets.TestData{}
|
|
if err := proto.Unmarshal(sent[0].GetData(), decoded); err != nil {
|
|
t.Fatalf("failed to decode response payload: %v", err)
|
|
}
|
|
if decoded.GetIndex() != 8 {
|
|
t.Errorf("expected response index 8, got %d", decoded.GetIndex())
|
|
}
|
|
}
|