- Add gateway contract for inbound queue ordering (03+02_gateway_contract) - Update receive thread model across Dart, Go, Kotlin, Python, TypeScript - Implement queue ordering logic in BaseClient and Communicator - Add comprehensive tests for queue ordering in all languages - Update documentation (PORTING_GUIDE, PROTOCOL, README) - Archive task completion logs for completed subtasks
307 lines
8.1 KiB
Go
307 lines
8.1 KiB
Go
package proto_socket_test
|
|
|
|
import (
|
|
"errors"
|
|
"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")
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
|
|
|