332 lines
10 KiB
Go
332 lines
10 KiB
Go
package mtprotoedge
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"errors"
|
|
"io"
|
|
"sync"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/iamxvbaba/td/bin"
|
|
"github.com/iamxvbaba/td/clock"
|
|
"github.com/iamxvbaba/td/crypto"
|
|
"github.com/iamxvbaba/td/mt"
|
|
"github.com/iamxvbaba/td/proto"
|
|
)
|
|
|
|
type gatedRequiredControlTransport struct {
|
|
started chan struct{}
|
|
release chan struct{}
|
|
sendErr error
|
|
|
|
startOnce sync.Once
|
|
closeOnce sync.Once
|
|
sends atomic.Int32
|
|
closes atomic.Int32
|
|
}
|
|
|
|
func newGatedRequiredControlTransport(sendErr error) *gatedRequiredControlTransport {
|
|
return &gatedRequiredControlTransport{
|
|
started: make(chan struct{}),
|
|
release: make(chan struct{}),
|
|
sendErr: sendErr,
|
|
}
|
|
}
|
|
|
|
func (t *gatedRequiredControlTransport) Send(context.Context, *bin.Buffer) error {
|
|
t.sends.Add(1)
|
|
t.startOnce.Do(func() { close(t.started) })
|
|
<-t.release
|
|
return t.sendErr
|
|
}
|
|
|
|
func (t *gatedRequiredControlTransport) Recv(context.Context, *bin.Buffer) error {
|
|
return io.EOF
|
|
}
|
|
|
|
func (t *gatedRequiredControlTransport) Close() error {
|
|
t.closes.Add(1)
|
|
t.closeOnce.Do(func() { close(t.release) })
|
|
return nil
|
|
}
|
|
|
|
func (t *gatedRequiredControlTransport) unblock() {
|
|
t.closeOnce.Do(func() { close(t.release) })
|
|
}
|
|
|
|
func TestServiceTaskResponsesWaitForPhysicalWrite(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
send func(*Server, context.Context, *Conn) error
|
|
}{
|
|
{
|
|
name: "pong",
|
|
send: func(s *Server, ctx context.Context, c *Conn) error {
|
|
return s.sendPong(ctx, c, 11, 22)
|
|
},
|
|
},
|
|
{
|
|
name: "future_salts",
|
|
send: func(s *Server, ctx context.Context, c *Conn) error {
|
|
return s.sendFutureSalts(ctx, c, 11, 32)
|
|
},
|
|
},
|
|
{
|
|
name: "msgs_state_info",
|
|
send: func(s *Server, ctx context.Context, c *Conn) error {
|
|
return s.sendMsgsStateInfo(ctx, c, 11, []byte{4})
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
tr := newGatedRequiredControlTransport(nil)
|
|
c := newOutboundTestConn(t, tr, newOutboundTrackedBudget(1<<20))
|
|
c.outboundControlTrackedBudget = newOutboundTrackedBudget(1 << 20)
|
|
srv := &Server{clock: clock.System}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
|
defer cancel()
|
|
done := make(chan error, 1)
|
|
go func() {
|
|
done <- tc.send(srv, ctx, c)
|
|
}()
|
|
|
|
select {
|
|
case <-tr.started:
|
|
case <-time.After(time.Second):
|
|
t.Fatal("service response did not reach the physical writer")
|
|
}
|
|
select {
|
|
case err := <-done:
|
|
t.Fatalf("service response returned before physical write completed: %v", err)
|
|
case <-time.After(20 * time.Millisecond):
|
|
}
|
|
|
|
tr.unblock()
|
|
select {
|
|
case err := <-done:
|
|
if err != nil {
|
|
t.Fatalf("service response: %v", err)
|
|
}
|
|
case <-time.After(time.Second):
|
|
t.Fatal("service response did not return after physical write")
|
|
}
|
|
if c.isRetired() {
|
|
t.Fatal("successful service response terminally closed the connection")
|
|
}
|
|
if got := tr.sends.Load(); got != 1 {
|
|
t.Fatalf("physical sends = %d, want 1", got)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSendRequiredControlWaitsForPhysicalWriteAndReturnsBudget(t *testing.T) {
|
|
tr := newGatedRequiredControlTransport(nil)
|
|
controlBudget := newOutboundTrackedBudget(1 << 20)
|
|
c := newOutboundTestConn(t, tr, newOutboundTrackedBudget(1<<20))
|
|
c.outboundControlTrackedBudget = controlBudget
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
|
defer cancel()
|
|
done := make(chan error, 1)
|
|
go func() {
|
|
done <- c.SendRequiredControl(ctx, proto.MessageServerResponse, &mt.Pong{MsgID: 1, PingID: 2})
|
|
}()
|
|
|
|
select {
|
|
case <-tr.started:
|
|
case <-time.After(time.Second):
|
|
t.Fatal("required control did not reach the physical writer")
|
|
}
|
|
select {
|
|
case err := <-done:
|
|
t.Fatalf("SendRequiredControl returned before physical write completed: %v", err)
|
|
case <-time.After(20 * time.Millisecond):
|
|
}
|
|
|
|
tr.unblock()
|
|
select {
|
|
case err := <-done:
|
|
if err != nil {
|
|
t.Fatalf("SendRequiredControl: %v", err)
|
|
}
|
|
case <-time.After(time.Second):
|
|
t.Fatal("SendRequiredControl did not return after physical write")
|
|
}
|
|
if c.isRetired() {
|
|
t.Fatal("successful required control terminally closed the connection")
|
|
}
|
|
if got := controlBudget.snapshot(); got != 0 {
|
|
t.Fatalf("control budget after non-pending physical write = %d, want 0", got)
|
|
}
|
|
if got := tr.sends.Load(); got != 1 {
|
|
t.Fatalf("physical sends = %d, want 1", got)
|
|
}
|
|
}
|
|
|
|
func TestSendRequiredControlReturnsAfterWriteWithoutWaitingForAck(t *testing.T) {
|
|
tr := &failAfterTransport{}
|
|
controlBudget := newOutboundTrackedBudget(1 << 20)
|
|
c := newOutboundTestConn(t, tr, newOutboundTrackedBudget(1<<20))
|
|
c.outboundControlTrackedBudget = controlBudget
|
|
created := &mt.NewSessionCreated{FirstMsgID: 1, UniqueID: 2, ServerSalt: 3}
|
|
encoded, err := encodeOutboundMessageWithoutSlot(created)
|
|
if err != nil {
|
|
t.Fatalf("encode new_session_created: %v", err)
|
|
}
|
|
|
|
if err := c.SendRequiredControl(context.Background(), proto.MessageFromServer, created); err != nil {
|
|
t.Fatalf("SendRequiredControl: %v", err)
|
|
}
|
|
if got := tr.stored.Load(); got != 1 {
|
|
t.Fatalf("completed physical sends = %d, want 1", got)
|
|
}
|
|
if got := controlBudget.snapshot(); got != int64(len(encoded.body)) {
|
|
t.Fatalf("pending control budget = %d, want %d until client ACK", got, len(encoded.body))
|
|
}
|
|
|
|
frame, err := crypto.NewClientCipher(rand.Reader).DecryptFromBuffer(c.key, &bin.Buffer{Buf: tr.lastFrame()})
|
|
if err != nil {
|
|
t.Fatalf("decrypt new_session_created: %v", err)
|
|
}
|
|
c.AckServerMessages([]int64{frame.MessageID})
|
|
deadline := time.Now().Add(time.Second)
|
|
for controlBudget.snapshot() != 0 && time.Now().Before(deadline) {
|
|
time.Sleep(time.Millisecond)
|
|
}
|
|
if got := controlBudget.snapshot(); got != 0 {
|
|
t.Fatalf("control budget after ACK = %d, want 0", got)
|
|
}
|
|
}
|
|
|
|
func TestSendRequiredControlQueueDeadlineTerminatesAndReturnsBudget(t *testing.T) {
|
|
tr := &failAfterTransport{}
|
|
controlBudget := newOutboundTrackedBudget(1 << 20)
|
|
c := &Conn{
|
|
transport: tr,
|
|
writer: tr,
|
|
metrics: NopMetrics{},
|
|
writeTimeout: time.Second,
|
|
outboundTrackedBudget: newOutboundTrackedBudget(1 << 20),
|
|
outboundControlTrackedBudget: controlBudget,
|
|
outbound: make(chan *outboundOp, 1),
|
|
outboundControl: make(chan *outboundOp, 1),
|
|
outboundStop: make(chan struct{}),
|
|
}
|
|
// No actor is running and the bounded control queue is full, so the parent
|
|
// deadline must cover queue admission and make the failure terminal.
|
|
c.outboundControl <- &outboundOp{kind: outboundAck}
|
|
ctx, cancel := context.WithTimeout(context.Background(), 25*time.Millisecond)
|
|
defer cancel()
|
|
|
|
started := time.Now()
|
|
err := c.SendRequiredControl(ctx, proto.MessageServerResponse, &mt.Pong{MsgID: 1, PingID: 2})
|
|
if !errors.Is(err, context.DeadlineExceeded) {
|
|
t.Fatalf("full control queue error = %v, want context deadline", err)
|
|
}
|
|
if elapsed := time.Since(started); elapsed > 500*time.Millisecond {
|
|
t.Fatalf("full control queue waited %v, want parent-deadline-bounded admission", elapsed)
|
|
}
|
|
if !c.isRetired() {
|
|
t.Fatal("required control queue failure did not terminally close the connection")
|
|
}
|
|
if got := tr.sends.Load(); got != 0 {
|
|
t.Fatalf("physical sends = %d, want 0", got)
|
|
}
|
|
if got := tr.closes.Load(); got != 1 {
|
|
t.Fatalf("transport closes = %d, want 1", got)
|
|
}
|
|
if got := controlBudget.snapshot(); got != 0 {
|
|
t.Fatalf("control budget after queue timeout = %d, want 0", got)
|
|
}
|
|
}
|
|
|
|
func TestSendRequiredControlBlockedWriteUsesWholeOperationDeadline(t *testing.T) {
|
|
tr := newGatedRequiredControlTransport(io.ErrClosedPipe)
|
|
controlBudget := newOutboundTrackedBudget(1 << 20)
|
|
c := newOutboundTestConn(t, tr, newOutboundTrackedBudget(1<<20))
|
|
c.outboundControlTrackedBudget = controlBudget
|
|
c.writeTimeout = 25 * time.Millisecond
|
|
|
|
started := time.Now()
|
|
err := c.SendRequiredControl(context.Background(), proto.MessageServerResponse, &mt.Pong{MsgID: 1, PingID: 2})
|
|
if !errors.Is(err, context.DeadlineExceeded) {
|
|
t.Fatalf("blocked required control error = %v, want context deadline", err)
|
|
}
|
|
if elapsed := time.Since(started); elapsed > 500*time.Millisecond {
|
|
t.Fatalf("blocked required control waited %v, want write-timeout-bounded operation", elapsed)
|
|
}
|
|
select {
|
|
case <-c.outboundDone:
|
|
case <-time.After(time.Second):
|
|
t.Fatal("outbound actor did not stop after required-control write timeout")
|
|
}
|
|
if !c.isRetired() {
|
|
t.Fatal("blocked required control did not terminally close the connection")
|
|
}
|
|
if got := tr.closes.Load(); got != 1 {
|
|
t.Fatalf("transport closes = %d, want 1", got)
|
|
}
|
|
if got := controlBudget.snapshot(); got != 0 {
|
|
t.Fatalf("control budget after blocked write = %d, want 0", got)
|
|
}
|
|
}
|
|
|
|
func TestSendRequiredControlWriteFailureTerminatesAndReturnsBudget(t *testing.T) {
|
|
tr := &failAfterTransport{}
|
|
tr.failAt.Store(1)
|
|
controlBudget := newOutboundTrackedBudget(1 << 20)
|
|
c := newOutboundTestConn(t, tr, newOutboundTrackedBudget(1<<20))
|
|
c.outboundControlTrackedBudget = controlBudget
|
|
|
|
err := c.SendRequiredControl(context.Background(), proto.MessageServerResponse, &mt.Pong{MsgID: 1, PingID: 2})
|
|
if err == nil {
|
|
t.Fatal("write-failed required control unexpectedly succeeded")
|
|
}
|
|
select {
|
|
case <-c.outboundDone:
|
|
case <-time.After(time.Second):
|
|
t.Fatal("outbound actor did not stop after required-control write failure")
|
|
}
|
|
if !c.isRetired() {
|
|
t.Fatal("write-failed required control did not terminally close the connection")
|
|
}
|
|
if got := tr.closes.Load(); got != 1 {
|
|
t.Fatalf("transport closes = %d, want 1", got)
|
|
}
|
|
if got := controlBudget.snapshot(); got != 0 {
|
|
t.Fatalf("control budget after write failure = %d, want 0", got)
|
|
}
|
|
}
|
|
|
|
func TestSendRequiredControlBudgetFailureIsTerminal(t *testing.T) {
|
|
tr := &failAfterTransport{}
|
|
controlBudget := newOutboundTrackedBudget(1)
|
|
c := newOutboundTestConn(t, tr, newOutboundTrackedBudget(1<<20))
|
|
c.outboundControlTrackedBudget = controlBudget
|
|
|
|
err := c.SendRequiredControl(context.Background(), proto.MessageServerResponse, &mt.Pong{MsgID: 1, PingID: 2})
|
|
if !errors.Is(err, ErrOutboundTrackedBudget) {
|
|
t.Fatalf("required control over budget = %v, want ErrOutboundTrackedBudget", err)
|
|
}
|
|
select {
|
|
case <-c.outboundDone:
|
|
case <-time.After(time.Second):
|
|
t.Fatal("outbound actor did not stop after required-control budget failure")
|
|
}
|
|
if !c.isRetired() {
|
|
t.Fatal("required-control budget failure did not terminally close the connection")
|
|
}
|
|
if got := tr.sends.Load(); got != 0 {
|
|
t.Fatalf("budget-rejected required control wrote %d frames, want 0", got)
|
|
}
|
|
if got := controlBudget.snapshot(); got != 0 {
|
|
t.Fatalf("control budget after reservation failure = %d, want 0", got)
|
|
}
|
|
}
|