166 lines
4.7 KiB
Go
166 lines
4.7 KiB
Go
package main
|
|
|
|
import (
|
|
"net"
|
|
"net/rpc"
|
|
"sync"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/tinode/chat/server/concurrency"
|
|
"github.com/tinode/chat/server/store/types"
|
|
)
|
|
|
|
type orderedTopicProxy struct {
|
|
mu sync.Mutex
|
|
received []int64
|
|
done chan struct{}
|
|
}
|
|
|
|
func (proxy *orderedTopicProxy) TopicProxy(msg *ClusterResp, rejected *bool) error {
|
|
if msg.SrvMsg.Data.SeqId == 1 {
|
|
time.Sleep(50 * time.Millisecond)
|
|
}
|
|
proxy.mu.Lock()
|
|
proxy.received = append(proxy.received, msg.SrvMsg.Data.SeqId)
|
|
if len(proxy.received) == 2 {
|
|
close(proxy.done)
|
|
}
|
|
proxy.mu.Unlock()
|
|
return nil
|
|
}
|
|
|
|
func TestClusterWriteLoopPreservesTopicOrder(t *testing.T) {
|
|
serverConn, clientConn := net.Pipe()
|
|
rpcServer := rpc.NewServer()
|
|
proxy := &orderedTopicProxy{done: make(chan struct{})}
|
|
if err := rpcServer.RegisterName("Cluster", proxy); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
go rpcServer.ServeConn(serverConn)
|
|
|
|
pool := concurrency.NewGoRoutinePool(4)
|
|
previousCluster := globals.cluster
|
|
globals.cluster = &Cluster{proxyEventQueue: pool}
|
|
t.Cleanup(func() {
|
|
globals.cluster = previousCluster
|
|
_ = clientConn.Close()
|
|
_ = serverConn.Close()
|
|
pool.Stop()
|
|
})
|
|
|
|
sess := &Session{
|
|
sid: "multiplex-order-test",
|
|
proto: MULTIPLEX,
|
|
proxiedTopic: "p2p-order-test",
|
|
send: make(chan any, 2),
|
|
clnode: &ClusterNode{endpoint: rpc.NewClient(clientConn), connected: true},
|
|
}
|
|
tenantID := types.TenantID(1)
|
|
sess.send <- &ServerComMessage{TenantID: tenantID, Data: &MsgServerData{SeqId: 1}}
|
|
sess.send <- &ServerComMessage{TenantID: tenantID, Data: &MsgServerData{SeqId: 2}}
|
|
|
|
// Scheduling twice used to start two concurrent drains for the same topic.
|
|
sess.scheduleClusterWriteLoop()
|
|
sess.scheduleClusterWriteLoop()
|
|
|
|
select {
|
|
case <-proxy.done:
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("timed out waiting for cluster responses")
|
|
}
|
|
deadline := time.Now().Add(2 * time.Second)
|
|
for sess.clusterWriteScheduled.Load() && time.Now().Before(deadline) {
|
|
time.Sleep(time.Millisecond)
|
|
}
|
|
if sess.clusterWriteScheduled.Load() {
|
|
t.Fatal("cluster write task did not release session ownership")
|
|
}
|
|
|
|
proxy.mu.Lock()
|
|
defer proxy.mu.Unlock()
|
|
if len(proxy.received) != 2 || proxy.received[0] != 1 || proxy.received[1] != 2 {
|
|
t.Fatalf("cluster responses arrived out of order: %v", proxy.received)
|
|
}
|
|
}
|
|
|
|
func TestClusterWriteLoopRejectsWritesAfterTermination(t *testing.T) {
|
|
previousSessionStore := globals.sessionStore
|
|
globals.sessionStore = NewSessionStore(time.Minute)
|
|
t.Cleanup(func() { globals.sessionStore = previousSessionStore })
|
|
|
|
sess := &Session{
|
|
sid: "multiplex-termination-test",
|
|
proto: MULTIPLEX,
|
|
send: make(chan any, 1),
|
|
stop: make(chan any, 1),
|
|
detach: make(chan string, 1),
|
|
subs: make(map[string]*Subscription),
|
|
}
|
|
sess.stop <- nil
|
|
|
|
sess.clusterWriteLoop("grp-termination-test")
|
|
|
|
if atomic.LoadInt32(&sess.terminating) == 0 {
|
|
t.Fatal("terminated multiplexing session still accepts writes")
|
|
}
|
|
if !sess.queueOut(&ServerComMessage{TenantID: types.TenantID(1)}) {
|
|
t.Fatal("write to terminated multiplexing session should be discarded")
|
|
}
|
|
if len(sess.send) != 0 {
|
|
t.Fatalf("terminated multiplexing session retained %d outbound messages", len(sess.send))
|
|
}
|
|
}
|
|
|
|
func TestMultiplexEnqueueWaitsForDrain(t *testing.T) {
|
|
sess := &Session{proto: MULTIPLEX, send: make(chan any, 1)}
|
|
sess.send <- "existing"
|
|
|
|
go func() {
|
|
time.Sleep(10 * time.Millisecond)
|
|
<-sess.send
|
|
}()
|
|
|
|
payload := &ServerComMessage{TenantID: types.TenantID(1)}
|
|
if !sess.enqueueMultiplexOutbound(payload, 200*time.Millisecond) {
|
|
t.Fatal("multiplex enqueue failed while the drain was making progress")
|
|
}
|
|
if got := <-sess.send; got != payload {
|
|
t.Fatalf("unexpected queued payload: %#v", got)
|
|
}
|
|
}
|
|
|
|
func TestMultiplexEnqueueTimesOutWithoutDrain(t *testing.T) {
|
|
sess := &Session{proto: MULTIPLEX, send: make(chan any, 1)}
|
|
sess.send <- "existing"
|
|
|
|
started := time.Now()
|
|
if sess.enqueueMultiplexOutbound("next", 20*time.Millisecond) {
|
|
t.Fatal("multiplex enqueue succeeded without queue capacity")
|
|
}
|
|
if elapsed := time.Since(started); elapsed < 15*time.Millisecond || elapsed > 200*time.Millisecond {
|
|
t.Fatalf("multiplex enqueue timeout outside expected bounds: %s", elapsed)
|
|
}
|
|
}
|
|
|
|
func TestMultiplexEnqueueStopsWhenSessionTerminates(t *testing.T) {
|
|
sess := &Session{proto: MULTIPLEX, send: make(chan any, 1)}
|
|
sess.send <- "existing"
|
|
|
|
go func() {
|
|
time.Sleep(10 * time.Millisecond)
|
|
sess.clusterQueueMu.Lock()
|
|
atomic.StoreInt32(&sess.terminating, 1)
|
|
<-sess.send
|
|
sess.clusterQueueMu.Unlock()
|
|
}()
|
|
|
|
if !sess.enqueueMultiplexOutbound("next", 200*time.Millisecond) {
|
|
t.Fatal("terminating multiplex session should discard the pending write")
|
|
}
|
|
if len(sess.send) != 0 {
|
|
t.Fatalf("terminating multiplex session retained %d outbound messages", len(sess.send))
|
|
}
|
|
}
|