owpengram-server/internal/mtprotoedge/inbound_rpc_test.go

586 lines
16 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package mtprotoedge
import (
"context"
"errors"
"sync/atomic"
"testing"
"time"
)
func newInboundTestConn(s *inboundRPCScheduler, maxInflight, queueSize int, timeout time.Duration) *Conn {
c := &Conn{metrics: NopMetrics{}}
c.startInboundRPCScheduler(s, maxInflight, queueSize, timeout)
return c
}
func TestInboundRPCSchedulerIsLazyPerConnectionAndServer(t *testing.T) {
scheduler := newInboundRPCScheduler(4, 16, 1<<20)
scheduler.start()
c := newInboundTestConn(scheduler, 2, 4, time.Second)
defer func() {
c.closeInboundRPCScheduler()
scheduler.stop(time.Second)
}()
if c.rpcQueue != nil {
t.Fatal("new connection eagerly allocated an inbound queue")
}
scheduler.lifecycleMu.Lock()
workersStarted := scheduler.workersStarted
scheduler.lifecycleMu.Unlock()
if workersStarted {
t.Fatal("empty server eagerly started inbound RPC workers")
}
}
func TestInboundRPCSchedulerBoundsConcurrentWork(t *testing.T) {
scheduler := newInboundRPCScheduler(2, 32, 1<<20)
scheduler.start()
c := newInboundTestConn(scheduler, 2, 4, time.Second)
defer func() {
c.closeInboundRPCScheduler()
scheduler.stop(time.Second)
}()
var active atomic.Int64
var maxActive atomic.Int64
var done atomic.Int64
started := make(chan struct{}, 6)
release := make(chan struct{})
task := inboundRPC{
method: "test.method",
run: func(ctx context.Context) error {
cur := active.Add(1)
for {
old := maxActive.Load()
if cur <= old || maxActive.CompareAndSwap(old, cur) {
break
}
}
started <- struct{}{}
select {
case <-release:
case <-ctx.Done():
}
active.Add(-1)
done.Add(1)
return nil
},
}
for i := 0; i < 2; i++ {
if err := c.enqueueInboundRPC(context.Background(), task); err != nil {
t.Fatalf("enqueue active task %d: %v", i, err)
}
}
for i := 0; i < 2; i++ {
select {
case <-started:
case <-time.After(time.Second):
t.Fatal("timed out waiting for active rpc workers")
}
}
for i := 0; i < 4; i++ {
if err := c.enqueueInboundRPC(context.Background(), task); err != nil {
t.Fatalf("enqueue queued task %d: %v", i, err)
}
}
if err := c.enqueueInboundRPC(context.Background(), task); !errors.Is(err, ErrInboundRPCQueueFull) {
t.Fatalf("enqueue over capacity err = %v, want ErrInboundRPCQueueFull", err)
}
if got := maxActive.Load(); got != 2 {
t.Fatalf("max active = %d, want 2", got)
}
close(release)
deadline := time.After(2 * time.Second)
for done.Load() != 6 {
select {
case <-deadline:
t.Fatalf("done = %d, want 6", done.Load())
default:
time.Sleep(10 * time.Millisecond)
}
}
if tasks, bytes := scheduler.budgetSnapshot(); tasks != 0 || bytes != 0 {
t.Fatalf("global budget after completion = (%d tasks, %d bytes), want zero", tasks, bytes)
}
}
func TestInboundRPCSchedulerSkipsUnresolvedDependencyGate(t *testing.T) {
scheduler := newInboundRPCScheduler(1, 8, 1<<20)
scheduler.start()
c := newInboundTestConn(scheduler, 1, 4, time.Second)
defer func() {
c.closeInboundRPCScheduler()
scheduler.stop(time.Second)
}()
blockedRan := make(chan struct{}, 1)
independentRan := make(chan struct{}, 1)
gate := newInboundRPCGate(1, c.wakeInboundRPC)
gate.resolve(true) // subscriber-installation sentinel; one dependency remains.
if err := c.enqueueInboundRPC(context.Background(), inboundRPC{
method: "invokeAfter",
gate: gate,
run: func(context.Context) error {
blockedRan <- struct{}{}
return nil
},
}); err != nil {
t.Fatal(err)
}
if err := c.enqueueInboundRPC(context.Background(), inboundRPC{
method: "independent",
run: func(context.Context) error {
independentRan <- struct{}{}
return nil
},
}); err != nil {
t.Fatal(err)
}
select {
case <-independentRan:
case <-time.After(time.Second):
t.Fatal("independent task was starved behind unresolved invokeAfter")
}
select {
case <-blockedRan:
t.Fatal("invokeAfter ran before its dependency completed")
default:
}
gate.resolve(true)
select {
case <-blockedRan:
case <-time.After(time.Second):
t.Fatal("resolved invokeAfter was not rescheduled")
}
}
func TestInboundRPCSchedulerFairAcrossConnections(t *testing.T) {
scheduler := newInboundRPCScheduler(1, 16, 1<<20)
c1 := newInboundTestConn(scheduler, 1, 4, time.Second)
c2 := newInboundTestConn(scheduler, 1, 4, time.Second)
defer func() {
c1.closeInboundRPCScheduler()
c2.closeInboundRPCScheduler()
scheduler.stop(time.Second)
}()
order := make(chan string, 3)
enqueue := func(c *Conn, label string) {
t.Helper()
if err := c.enqueueInboundRPC(context.Background(), inboundRPC{
method: label,
run: func(context.Context) error {
order <- label
return nil
},
}); err != nil {
t.Fatalf("enqueue %s: %v", label, err)
}
}
// 先在 worker 启动前形成 [c1, c2] ready 顺序。c1 每次只执行一条后回到队尾,
// 因此 c2 必须在 c1 的第二条之前获得执行机会。
enqueue(c1, "c1-first")
enqueue(c1, "c1-second")
enqueue(c2, "c2-first")
scheduler.start()
want := []string{"c1-first", "c2-first", "c1-second"}
for i := range want {
select {
case got := <-order:
if got != want[i] {
t.Fatalf("execution[%d] = %q, want %q", i, got, want[i])
}
case <-time.After(time.Second):
t.Fatalf("timed out waiting for execution[%d]", i)
}
}
}
func TestInboundRPCBudgetReservedBeforeCommitAndFullyReturned(t *testing.T) {
scheduler := newInboundRPCScheduler(1, 2, 10)
c1 := newInboundTestConn(scheduler, 1, 4, time.Second)
c2 := newInboundTestConn(scheduler, 1, 4, time.Second)
defer func() {
c1.closeInboundRPCScheduler()
c2.closeInboundRPCScheduler()
scheduler.stop(time.Second)
}()
r1, err := c1.reserveInboundRPC(context.Background(), "one", 6)
if err != nil {
t.Fatalf("reserve first body: %v", err)
}
if tasks, bytes := scheduler.budgetSnapshot(); tasks != 1 || bytes != 6 {
t.Fatalf("budget after first pre-Copy reservation = (%d, %d), want (1, 6)", tasks, bytes)
}
if _, err := c2.reserveInboundRPC(context.Background(), "too-large", 5); !errors.Is(err, ErrInboundRPCQueueFull) {
t.Fatalf("reserve over byte budget err = %v, want queue full", err)
}
r2, err := c2.reserveInboundRPC(context.Background(), "two", 4)
if err != nil {
t.Fatalf("reserve second body: %v", err)
}
if _, err := c1.reserveInboundRPC(context.Background(), "too-many", 0); !errors.Is(err, ErrInboundRPCQueueFull) {
t.Fatalf("reserve over task budget err = %v, want queue full", err)
}
r1.abort()
r2.abort()
if tasks, bytes := scheduler.budgetSnapshot(); tasks != 0 || bytes != 0 {
t.Fatalf("budget after aborts = (%d, %d), want zero", tasks, bytes)
}
if got := c1.inflightRPCBytes.Load(); got != 0 {
t.Fatalf("c1 inflight bytes = %d, want zero", got)
}
if got := c2.inflightRPCBytes.Load(); got != 0 {
t.Fatalf("c2 inflight bytes = %d, want zero", got)
}
}
func TestInboundRPCPerConnectionByteBudgetRejectedBeforeCommit(t *testing.T) {
scheduler := newInboundRPCScheduler(1, 2, int64(maxInflightRPCBytes)+1)
c := newInboundTestConn(scheduler, 1, 2, time.Second)
defer func() {
c.closeInboundRPCScheduler()
scheduler.stop(time.Second)
}()
if _, err := c.reserveInboundRPC(context.Background(), "oversized", maxInflightRPCBytes+1); !errors.Is(err, ErrInboundRPCQueueFull) {
t.Fatalf("reserve over per-connection byte budget err = %v, want queue full", err)
}
if tasks, bytes := scheduler.budgetSnapshot(); tasks != 0 || bytes != 0 {
t.Fatalf("global budget after per-connection rejection = (%d, %d), want zero", tasks, bytes)
}
if got := c.inflightRPCBytes.Load(); got != 0 {
t.Fatalf("connection bytes after rejection = %d, want zero", got)
}
}
func TestInboundRPCCommitRacingCloseReturnsReservation(t *testing.T) {
scheduler := newInboundRPCScheduler(1, 4, 1<<20)
c := newInboundTestConn(scheduler, 1, 2, time.Second)
defer scheduler.stop(time.Second)
reservation, err := c.reserveInboundRPC(context.Background(), "closing", 13)
if err != nil {
t.Fatalf("reserve: %v", err)
}
closed := make(chan struct{})
go func() {
c.closeInboundRPCScheduler()
close(closed)
}()
deadline := time.Now().Add(time.Second)
for {
c.rpcMu.Lock()
isClosed := c.rpcClosed
c.rpcMu.Unlock()
if isClosed {
break
}
if time.Now().After(deadline) {
t.Fatal("connection scheduler was not marked closed")
}
time.Sleep(time.Millisecond)
}
if err := reservation.commit(inboundRPC{run: func(context.Context) error { return nil }}); !errors.Is(err, ErrConnClosed) {
t.Fatalf("commit after close err = %v, want ErrConnClosed", err)
}
select {
case <-closed:
case <-time.After(time.Second):
t.Fatal("close did not finish after reservation commit")
}
if tasks, bytes := scheduler.budgetSnapshot(); tasks != 0 || bytes != 0 {
t.Fatalf("global budget after close/commit race = (%d, %d), want zero", tasks, bytes)
}
if got := c.inflightRPCBytes.Load(); got != 0 {
t.Fatalf("connection bytes after close/commit race = %d, want zero", got)
}
}
func TestInboundRPCSchedulerCloseRemovesReadyTokenBeforeStart(t *testing.T) {
scheduler := newInboundRPCScheduler(1, 1, 1<<20)
defer scheduler.stop(time.Second)
// A bounded ready channel used to retain one stale token per closed connection. With workers
// not started yet, the second connection then blocked forever trying to publish its token even
// though the first connection had returned every task/byte budget.
for i := 0; i < 32; i++ {
c := newInboundTestConn(scheduler, 1, 1, time.Second)
done := make(chan error, 1)
go func() {
done <- c.enqueueInboundRPC(context.Background(), inboundRPC{
method: "close-before-start",
run: func(context.Context) error { return nil },
})
}()
select {
case err := <-done:
if err != nil {
t.Fatalf("enqueue iteration %d: %v", i, err)
}
case <-time.After(time.Second):
t.Fatalf("enqueue iteration %d blocked behind a stale ready token", i)
}
c.closeInboundRPCScheduler()
if got := scheduler.readyLen(); got != 0 {
t.Fatalf("ready tokens after close iteration %d = %d, want zero", i, got)
}
}
if tasks, bytes := scheduler.budgetSnapshot(); tasks != 0 || bytes != 0 {
t.Fatalf("budget after close churn = (%d, %d), want zero", tasks, bytes)
}
}
func TestInboundRPCExpiredInQueueNeverRunsAndSignalsTimeout(t *testing.T) {
scheduler := newInboundRPCScheduler(1, 8, 1<<20)
scheduler.start()
c := newInboundTestConn(scheduler, 1, 4, 40*time.Millisecond)
defer func() {
c.closeInboundRPCScheduler()
scheduler.stop(time.Second)
}()
started := make(chan struct{})
release := make(chan struct{})
if err := c.enqueueInboundRPC(context.Background(), inboundRPC{
method: "blocker",
size: 7,
run: func(context.Context) error {
close(started)
<-release // 刻意忽略 deadline确保下一条在队列中到期。
return nil
},
}); err != nil {
t.Fatalf("enqueue blocker: %v", err)
}
select {
case <-started:
case <-time.After(time.Second):
t.Fatal("blocker did not start")
}
var ran atomic.Bool
timedOut := make(chan struct{})
if err := c.enqueueInboundRPC(context.Background(), inboundRPC{
method: "expires",
size: 11,
onTimeout: func() {
close(timedOut)
},
run: func(context.Context) error {
ran.Store(true)
return nil
},
}); err != nil {
t.Fatalf("enqueue expiring task: %v", err)
}
select {
case <-timedOut:
case <-time.After(time.Second):
t.Fatal("queued task did not signal timeout while the worker was still blocked")
}
deadline := time.Now().Add(time.Second)
for {
tasks, bytes := scheduler.budgetSnapshot()
if tasks == 1 && bytes == 7 {
break
}
if time.Now().After(deadline) {
t.Fatalf("budget while blocker still runs = (%d, %d), want only blocker (1, 7)", tasks, bytes)
}
time.Sleep(time.Millisecond)
}
close(release)
if ran.Load() {
t.Fatal("expired queued task entered business handler")
}
deadline = time.Now().Add(time.Second)
for {
tasks, bytes := scheduler.budgetSnapshot()
if tasks == 0 && bytes == 0 {
break
}
if time.Now().After(deadline) {
t.Fatalf("budget after timeout = (%d, %d), want zero", tasks, bytes)
}
time.Sleep(time.Millisecond)
}
}
func TestInboundRPCCloseDisarmsQueuedTimeout(t *testing.T) {
scheduler := newInboundRPCScheduler(1, 8, 1<<20)
c := newInboundTestConn(scheduler, 1, 4, 30*time.Millisecond)
defer scheduler.stop(time.Second)
timedOut := make(chan struct{}, 1)
if err := c.enqueueInboundRPC(context.Background(), inboundRPC{
method: "queued",
size: 11,
onTimeout: func() {
timedOut <- struct{}{}
},
}); err != nil {
t.Fatalf("enqueue queued task: %v", err)
}
c.closeInboundRPCScheduler()
time.Sleep(60 * time.Millisecond)
select {
case <-timedOut:
t.Fatal("connection close emitted a queued RPC timeout")
default:
}
}
func TestInboundRPCRunningDeadlineCancelsWithoutEarlyTimeout(t *testing.T) {
scheduler := newInboundRPCScheduler(1, 8, 1<<20)
scheduler.start()
c := newInboundTestConn(scheduler, 1, 4, 30*time.Millisecond)
defer func() {
c.closeInboundRPCScheduler()
scheduler.stop(time.Second)
}()
started := make(chan struct{})
release := make(chan struct{})
timedOut := make(chan struct{}, 1)
if err := c.enqueueInboundRPC(context.Background(), inboundRPC{
method: "running",
size: 7,
onTimeout: func() {
timedOut <- struct{}{}
},
run: func(context.Context) error {
close(started)
<-release
return nil
},
}); err != nil {
t.Fatalf("enqueue running task: %v", err)
}
<-started
time.Sleep(80 * time.Millisecond)
select {
case <-timedOut:
t.Fatal("running task emitted an early timeout before handler convergence")
default:
}
if tasks, bytes := scheduler.budgetSnapshot(); tasks != 1 || bytes != 7 {
t.Fatalf("running body budget after timeout = (%d, %d), want retained (1, 7)", tasks, bytes)
}
close(release)
deadline := time.Now().Add(time.Second)
for {
tasks, bytes := scheduler.budgetSnapshot()
if tasks == 0 && bytes == 0 {
break
}
if time.Now().After(deadline) {
t.Fatalf("running body budget after completion = (%d, %d), want zero", tasks, bytes)
}
time.Sleep(time.Millisecond)
}
}
func TestInboundRPCCloseDrainsQueueAndReturnsBudgets(t *testing.T) {
scheduler := newInboundRPCScheduler(1, 8, 1<<20)
scheduler.start()
c := newInboundTestConn(scheduler, 1, 4, time.Second)
defer scheduler.stop(time.Second)
started := make(chan struct{})
if err := c.enqueueInboundRPC(context.Background(), inboundRPC{
method: "running",
size: 7,
run: func(ctx context.Context) error {
close(started)
<-ctx.Done()
return ctx.Err()
},
}); err != nil {
t.Fatalf("enqueue running task: %v", err)
}
select {
case <-started:
case <-time.After(time.Second):
t.Fatal("running task did not start")
}
var queuedRan atomic.Bool
if err := c.enqueueInboundRPC(context.Background(), inboundRPC{
method: "queued",
size: 11,
run: func(context.Context) error {
queuedRan.Store(true)
return nil
},
}); err != nil {
t.Fatalf("enqueue queued task: %v", err)
}
if tasks, bytes := scheduler.budgetSnapshot(); tasks != 2 || bytes != 18 {
t.Fatalf("budget before close = (%d, %d), want (2, 18)", tasks, bytes)
}
c.closeInboundRPCScheduler()
if queuedRan.Load() {
t.Fatal("queued task ran during connection close")
}
if tasks, bytes := scheduler.budgetSnapshot(); tasks != 0 || bytes != 0 {
t.Fatalf("budget after close = (%d, %d), want zero", tasks, bytes)
}
if got := c.inflightRPCBytes.Load(); got != 0 {
t.Fatalf("connection inflight bytes after close = %d, want zero", got)
}
}
func TestInboundRPCReplayRestoreBarrierKeepsFollowingTaskOffWorkers(t *testing.T) {
scheduler := newInboundRPCScheduler(1, 8, 1<<20)
scheduler.start()
c := newInboundTestConn(scheduler, 1, 4, time.Second)
defer func() {
c.closeInboundRPCScheduler()
scheduler.stop(time.Second)
}()
finishFirst := c.beginRPCReplayRestore()
finishSecond := c.beginRPCReplayRestore()
ran := make(chan struct{})
if err := c.enqueueInboundRPC(context.Background(), inboundRPC{
method: "following.naked.rpc",
size: 4,
run: func(context.Context) error {
close(ran)
return nil
},
}); err != nil {
t.Fatalf("enqueue following task: %v", err)
}
select {
case <-ran:
t.Fatal("following RPC ran before replay restore completed")
case <-time.After(30 * time.Millisecond):
}
finishFirst()
select {
case <-ran:
t.Fatal("one of two replay restores released the scheduler early")
case <-time.After(30 * time.Millisecond):
}
finishSecond()
select {
case <-ran:
case <-time.After(time.Second):
t.Fatal("following RPC did not run after the final replay restore")
}
}