Ctrl K
ComunidadGuías…

internal/mtprotoedge/rpc_delivery_coordinator_test.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package mtprotoedge

import (
	"context"
	"errors"
	"sync"
	"sync/atomic"
	"testing"
	"time"
)

func TestRPCDeliveryCoordinatorWaitsFromClaimedThroughDone(t *testing.T) {
	coordinator := newRPCResultDelivery(1).coordinator
	started := make(chan struct{})
	release := make(chan struct{})
	coordinator.setHook(func() {
		close(started)
		<-release
	})

	claim, err := coordinator.claimReplayDeliveredHook(context.Background(), false)
	if err != nil || claim == nil {
		t.Fatalf("initial hook claim = %p, err=%v", claim, err)
	}
	if got := coordinator.hookState(); got != rpcResultDeliveryHookClaimed {
		t.Fatalf("hook state after claim = %d, want claimed", got)
	}

	type claimResult struct {
		claim *rpcResultDeliveryHookClaim
		err   error
	}
	waited := make(chan claimResult, 1)
	go func() {
		other, waitErr := coordinator.claimReplayDeliveredHook(context.Background(), false)
		waited <- claimResult{claim: other, err: waitErr}
	}()
	select {
	case result := <-waited:
		t.Fatalf("second replay passed a claimed hook: claim=%p err=%v", result.claim, result.err)
	case <-time.After(20 * time.Millisecond):
	}

	go claim.run()
	select {
	case <-started:
	case <-time.After(time.Second):
		t.Fatal("claimed hook did not enter in-progress")
	}
	if got := coordinator.hookState(); got != rpcResultDeliveryHookInProgress {
		t.Fatalf("hook state while callback blocked = %d, want in-progress", got)
	}
	select {
	case result := <-waited:
		t.Fatalf("second replay passed an in-progress hook: claim=%p err=%v", result.claim, result.err)
	case <-time.After(20 * time.Millisecond):
	}

	close(release)
	select {
	case result := <-waited:
		if result.err != nil || result.claim != nil {
			t.Fatalf("wait after done = claim:%p err:%v", result.claim, result.err)
		}
	case <-time.After(time.Second):
		t.Fatal("completion did not notify the waiting replay")
	}
	if got := coordinator.hookState(); got != rpcResultDeliveryHookDone {
		t.Fatalf("terminal hook state = %d, want done", got)
	}
}

func TestRPCDeliveryCoordinatorAbandonedClaimCannotRunAfterReacquire(t *testing.T) {
	coordinator := newRPCResultDelivery(1).coordinator
	var calls atomic.Int32
	coordinator.setHook(func() { calls.Add(1) })

	stale, err := coordinator.claimReplayDeliveredHook(context.Background(), false)
	if err != nil || stale == nil || !stale.abandon() {
		t.Fatalf("abandon initial claim = %p err=%v", stale, err)
	}
	current, err := coordinator.claimReplayDeliveredHook(context.Background(), false)
	if err != nil || current == nil {
		t.Fatalf("reacquire hook = %p err=%v", current, err)
	}
	stale.run()
	if got := calls.Load(); got != 0 {
		t.Fatalf("stale claim calls = %d, want 0", got)
	}
	if got := coordinator.hookState(); got != rpcResultDeliveryHookClaimed {
		t.Fatalf("stale claim changed current state to %d", got)
	}
	current.run()
	if got := calls.Load(); got != 1 {
		t.Fatalf("current claim calls = %d, want 1", got)
	}
	if got := coordinator.hookState(); got != rpcResultDeliveryHookDone {
		t.Fatalf("terminal hook state = %d, want done", got)
	}
}

func TestBoundedRPCReplayRestoreDoesNotRetainWorkerOrBarrier(t *testing.T) {
	s := New(Options{})
	c := &Conn{metrics: NopMetrics{}}
	finishBarrier := c.beginRPCReplayRestore()

	coordinator := newRPCResultDelivery(1).coordinator
	var hookCalls atomic.Int32
	coordinator.setHook(func() { hookCalls.Add(1) })
	claim, err := coordinator.claimReplayDeliveredHook(context.Background(), false)
	if err != nil || claim == nil {
		t.Fatalf("claim hook = %p err=%v", claim, err)
	}

	replacementStarted := make(chan struct{})
	releaseReplacement := make(chan struct{})
	t.Cleanup(func() {
		select {
		case <-releaseReplacement:
		default:
			close(releaseReplacement)
		}
	})
	ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
	defer cancel()
	started := time.Now()
	restoreResult := make(chan error, 1)
	go func() {
		restoreResult <- s.runBoundedRPCReplayRestore(ctx, c, "non-cooperative replacement", claim, func() error {
			close(replacementStarted)
			<-releaseReplacement // Deliberately ignores ctx.
			return nil
		})
	}()
	select {
	case <-replacementStarted:
	case <-time.After(time.Second):
		t.Fatal("replacement callback never started")
	}
	select {
	case err = <-restoreResult:
	case <-time.After(time.Second):
		t.Fatal("bounded restore retained its caller past the watchdog")
	}
	if !errors.Is(err, context.DeadlineExceeded) {
		t.Fatalf("bounded restore error = %v, want deadline", err)
	}
	if elapsed := time.Since(started); elapsed > 500*time.Millisecond {
		t.Fatalf("non-cooperative restore retained caller for %v", elapsed)
	}
	finishBarrier()
	if !c.isRetired() {
		t.Fatal("timed-out restore did not fence its physical generation")
	}
	c.rpcMu.Lock()
	pendingBarriers := c.rpcReplayRestores
	c.rpcMu.Unlock()
	if pendingBarriers != 0 {
		t.Fatalf("timed-out restore retained %d scheduler barriers", pendingBarriers)
	}
	if got := coordinator.hookState(); got != rpcResultDeliveryHookPending {
		t.Fatalf("not-yet-started timed-out hook state = %d, want pending", got)
	}

	// A replacement replay can now acquire the logical hook. The old blocked
	// goroutine holds a stale token and must not execute it when it eventually
	// returns.
	retry, err := coordinator.claimReplayDeliveredHook(context.Background(), false)
	if err != nil || retry == nil {
		t.Fatalf("retry hook claim = %p err=%v", retry, err)
	}
	retry.run()
	close(releaseReplacement)
	deadline := time.Now().Add(time.Second)
	for len(rpcReplayRestoreSlots) != 0 && time.Now().Before(deadline) {
		time.Sleep(time.Millisecond)
	}
	if got := hookCalls.Load(); got != 1 {
		t.Fatalf("logical hook calls after stale restore return = %d, want 1", got)
	}
}

func TestBoundedRPCReplayRestoreKeepsInProgressHookAtMostOnce(t *testing.T) {
	s := New(Options{})
	c := &Conn{metrics: NopMetrics{}}
	finishBarrier := c.beginRPCReplayRestore()

	coordinator := newRPCResultDelivery(1).coordinator
	hookStarted := make(chan struct{})
	releaseHook := make(chan struct{})
	t.Cleanup(func() {
		select {
		case <-releaseHook:
		default:
			close(releaseHook)
		}
	})
	coordinator.setHook(func() {
		close(hookStarted)
		<-releaseHook // Deliberately ignores the restore deadline.
	})
	claim, err := coordinator.claimReplayDeliveredHook(context.Background(), false)
	if err != nil || claim == nil {
		t.Fatalf("claim hook = %p err=%v", claim, err)
	}
	ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
	defer cancel()
	restoreResult := make(chan error, 1)
	go func() {
		restoreResult <- s.runBoundedRPCReplayRestore(ctx, c, "non-cooperative logical hook", claim, nil)
	}()
	select {
	case <-hookStarted:
	case <-time.After(time.Second):
		t.Fatal("logical hook never entered in-progress")
	}
	select {
	case err = <-restoreResult:
	case <-time.After(time.Second):
		t.Fatal("non-cooperative logical hook retained its caller past the watchdog")
	}
	if !errors.Is(err, context.DeadlineExceeded) {
		t.Fatalf("bounded logical-hook error = %v, want deadline", err)
	}
	if got := coordinator.hookState(); got != rpcResultDeliveryHookInProgress {
		t.Fatalf("timed-out logical hook state = %d, want in-progress", got)
	}
	finishBarrier()
	if !c.isRetired() {
		t.Fatal("timed-out logical hook did not fence its physical generation")
	}
	c.rpcMu.Lock()
	pendingBarriers := c.rpcReplayRestores
	c.rpcMu.Unlock()
	if pendingBarriers != 0 {
		t.Fatalf("timed-out logical hook retained %d scheduler barriers", pendingBarriers)
	}

	waitCtx, waitCancel := context.WithTimeout(context.Background(), 20*time.Millisecond)
	defer waitCancel()
	if retry, retryErr := coordinator.claimReplayDeliveredHook(waitCtx, false); retry != nil ||
		!errors.Is(retryErr, context.DeadlineExceeded) {
		t.Fatalf("retry during in-progress hook = claim:%p err:%v", retry, retryErr)
	}
	close(releaseHook)
	deadline := time.Now().Add(time.Second)
	for coordinator.hookState() != rpcResultDeliveryHookDone && time.Now().Before(deadline) {
		time.Sleep(time.Millisecond)
	}
	if got := coordinator.hookState(); got != rpcResultDeliveryHookDone {
		t.Fatalf("released logical hook state = %d, want done", got)
	}
}

func TestCachedReplacementReplayBarrierWaitsForLogicalHookDone(t *testing.T) {
	s := New(Options{})
	const reqMsgID = int64(71001)
	encoded := encodedRPCResultForPriorityTest(reqMsgID, 0)
	encoded.delivery = newRPCResultDelivery(reqMsgID)

	var order atomic.Int32
	hookStarted := make(chan struct{})
	releaseHook := make(chan struct{})
	t.Cleanup(func() {
		select {
		case <-releaseHook:
		default:
			close(releaseHook)
		}
	})
	encoded.setDeliveryHook(func() {
		close(hookStarted)
		<-releaseHook
		if !order.CompareAndSwap(1, 2) {
			panic("logical hook did not follow first replacement restore")
		}
	})

	firstConn := newOutboundTestConn(t, &collectingSessionTransport{}, newOutboundTrackedBudget(1<<20))
	firstResult := make(chan error, 1)
	go func() {
		firstResult <- s.sendReplayedRPCResultWithHook(context.Background(), firstConn, encoded, func() error {
			if !order.CompareAndSwap(0, 1) {
				return errors.New("first replacement restore ran out of order")
			}
			return nil
		})
	}()
	select {
	case <-hookStarted:
	case <-time.After(time.Second):
		t.Fatal("first replacement did not enter logical hook")
	}
	if got := encoded.delivery.coordinator.hookState(); got != rpcResultDeliveryHookInProgress {
		t.Fatalf("shared hook state = %d, want in-progress", got)
	}

	secondTransport := &collectingSessionTransport{}
	secondConn := newOutboundTestConn(t, secondTransport, newOutboundTrackedBudget(1<<20))
	secondReplacement := make(chan struct{})
	secondResult := make(chan error, 1)
	go func() {
		secondResult <- s.sendReplayedRPCResultWithHook(context.Background(), secondConn, encoded, func() error {
			if !order.CompareAndSwap(2, 3) {
				return errors.New("second replacement restore passed logical hook completion")
			}
			close(secondReplacement)
			return nil
		})
	}()
	deadline := time.Now().Add(time.Second)
	for len(secondTransport.snapshot()) == 0 && time.Now().Before(deadline) {
		time.Sleep(time.Millisecond)
	}
	if len(secondTransport.snapshot()) == 0 {
		t.Fatal("second replacement did not physically deliver cached result")
	}
	secondConn.rpcMu.Lock()
	secondBarriers := secondConn.rpcReplayRestores
	secondConn.rpcMu.Unlock()
	if secondBarriers != 1 {
		t.Fatalf("second replacement barriers while hook in progress = %d, want 1", secondBarriers)
	}
	select {
	case err := <-secondResult:
		t.Fatalf("second replacement returned before hook done: %v", err)
	case <-time.After(20 * time.Millisecond):
	}
	select {
	case <-secondReplacement:
		t.Fatal("second replacement metadata ran before shared hook done")
	default:
	}

	close(releaseHook)
	for name, result := range map[string]<-chan error{
		"first": firstResult, "second": secondResult,
	} {
		select {
		case err := <-result:
			if err != nil {
				t.Fatalf("%s replacement replay: %v", name, err)
			}
		case <-time.After(time.Second):
			t.Fatalf("%s replacement replay did not finish", name)
		}
	}
	if got := order.Load(); got != 3 {
		t.Fatalf("replacement/logical terminal order = %d, want 3", got)
	}
	secondConn.rpcMu.Lock()
	secondBarriers = secondConn.rpcReplayRestores
	secondConn.rpcMu.Unlock()
	if secondBarriers != 0 {
		t.Fatalf("second replacement retained %d barriers after hook done", secondBarriers)
	}
}

func TestRPCRewrapWatchdogCoversCommittedRestoreWithoutBlockingTimerOrWorker(t *testing.T) {
	s := New(Options{})
	c := &Conn{metrics: NopMetrics{}}
	const reqMsgID = int64(72001)
	encoded := encodedRPCResultForPriorityTest(reqMsgID, 0)
	encoded.delivery = newRPCResultDelivery(reqMsgID)
	var logicalCalls atomic.Int32
	encoded.setDeliveryHook(func() { logicalCalls.Add(1) })

	replacementStarted := make(chan struct{})
	releaseReplacement := make(chan struct{})
	t.Cleanup(func() {
		select {
		case <-releaseReplacement:
		default:
			close(releaseReplacement)
		}
	})
	alias := &rpcRewrapAlias{
		conn: c, newReqID: reqMsgID, method: "help.getConfig",
		afterSuccessfulDelivery: func() error {
			close(replacementStarted)
			<-releaseReplacement // Deliberately ignores every deadline.
			return nil
		},
	}
	alias.executionOK.Store(true)
	alias.beginReplayRestore()

	var (
		once sync.Once
		jobs chan rpcRewrapDeliveryJob
	)
	workerDone := make(chan error, 1)
	watchdogDone := make(chan struct{})
	job := rpcRewrapDeliveryJob{
		deadline: time.Now().Add(80 * time.Millisecond),
		run: func(control *rpcRewrapDeliveryControl, _ time.Time) {
			if !control.commit() {
				workerDone <- errors.New("commit lost before restore")
				return
			}
			restoreCtx, cancel := context.WithTimeout(context.Background(), 250*time.Millisecond)
			defer cancel()
			workerDone <- s.completeDeliveredRPCRewrapResult(
				restoreCtx, alias, encoded, "committed restore watchdog regression",
			)
		},
		fail: func(error) {
			c.fenceUndeliveredRPCResult()
			alias.releaseReplayRestoreBarrier()
			close(watchdogDone)
		},
	}
	if !scheduleRPCRewrapJob(job, &once, &jobs, 1, 2) {
		t.Fatal("schedule committed restore job")
	}
	select {
	case <-replacementStarted:
	case <-time.After(time.Second):
		t.Fatal("committed replacement restore never started")
	}
	select {
	case <-watchdogDone:
	case <-time.After(500 * time.Millisecond):
		t.Fatal("committed-state watchdog did not return promptly")
	}
	if !c.isRetired() {
		t.Fatal("committed-state watchdog did not fence connection")
	}
	c.rpcMu.Lock()
	pendingBarriers := c.rpcReplayRestores
	c.rpcMu.Unlock()
	if pendingBarriers != 0 {
		t.Fatalf("watchdog retained %d scheduler barriers", pendingBarriers)
	}
	select {
	case err := <-workerDone:
		if !errors.Is(err, context.DeadlineExceeded) {
			t.Fatalf("bounded committed restore error = %v, want deadline", err)
		}
	case <-time.After(time.Second):
		t.Fatal("non-cooperative restore permanently retained rewrap worker")
	}

	followingRan := make(chan struct{})
	if !scheduleRPCRewrapJob(rpcRewrapDeliveryJob{
		deadline: time.Now().Add(time.Second),
		run:      func(*rpcRewrapDeliveryControl, time.Time) { close(followingRan) },
	}, &once, &jobs, 1, 2) {
		t.Fatal("schedule following rewrap job")
	}
	select {
	case <-followingRan:
	case <-time.After(time.Second):
		t.Fatal("rewrap worker did not accept following job after restore timeout")
	}
	if got := encoded.delivery.coordinator.hookState(); got != rpcResultDeliveryHookPending {
		t.Fatalf("timed-out pre-hook restore state = %d, want pending", got)
	}
	if got := logicalCalls.Load(); got != 0 {
		t.Fatalf("logical hook ran behind non-cooperative replacement = %d", got)
	}
	close(releaseReplacement)
	deadline := time.Now().Add(time.Second)
	for len(rpcReplayRestoreSlots) != 0 && time.Now().Before(deadline) {
		time.Sleep(time.Millisecond)
	}
}