Files
Evan Lowry 29cfb0b4cb control/controlclient: add support for retry-after in map sessions (#21188)
Previously retry-after support was added to register requests in #19403,
in order to have more fine tuned client/control interactions in cases of
rate limiting.

This change expands that support to map sessions as well.

Updates tailscale/corp#47299

Signed-off-by: Evan Lowry <evan@tailscale.com>
2026-09-11 16:31:55 -03:00

121 lines
2.4 KiB
Go

// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package controlclient
import (
"context"
"sync"
"testing"
"testing/synctest"
"time"
"tailscale.com/tailcfg"
"tailscale.com/tstime"
)
type userProfileUpdateObserver struct{}
func (userProfileUpdateObserver) SetControlClientStatus(Client, Status) {}
func (userProfileUpdateObserver) UpdateUserProfiles(map[tailcfg.UserID]tailcfg.UserProfileView) bool {
return true
}
func TestMapRoutineStateUpdateUserProfilesConcurrentCancelMapCtx(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
c := &Auto{
logf: func(string, ...any) {},
observer: userProfileUpdateObserver{},
mapCtx: ctx,
mapCancel: cancel,
loggedIn: true,
inMapPoll: true,
}
mrs := mapRoutineState{c: c}
start := make(chan struct{})
var wg sync.WaitGroup
for range 4 {
wg.Go(func() {
<-start
for range 2000 {
c.mu.Lock()
c.cancelMapCtxLocked()
c.mu.Unlock()
}
})
}
for range 4 {
wg.Go(func() {
<-start
for range 2000 {
mrs.UpdateUserProfiles(nil)
}
})
}
close(start)
wg.Wait()
waitCtx, cancel := context.WithTimeout(t.Context(), 5*time.Second)
defer cancel()
if err := c.observerQueue.Wait(waitCtx); err != nil {
t.Fatal(err)
}
c.observerQueue.Shutdown()
c.mapCancel()
}
func TestWaitRetryAfter(t *testing.T) {
t.Parallel()
tests := []struct {
name string
retryAfter time.Duration
cancelAfter time.Duration
want time.Duration
}{
{
name: "returns_on_context_cancellation",
retryAfter: time.Minute,
cancelAfter: time.Second,
want: time.Second,
},
{
name: "returns_on_specified_retry_time",
retryAfter: time.Minute,
want: time.Minute,
},
{
name: "does_not_exceed_max_wait_time",
retryAfter: maxRetryWindow + 1,
want: maxRetryWindow,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
synctest.Test(t, func(t *testing.T) {
ctx, cancel := context.WithCancel(t.Context())
defer cancel()
if tt.cancelAfter != 0 {
time.AfterFunc(tt.cancelAfter, cancel)
}
c := &Auto{
clock: tstime.StdClock{},
logf: t.Logf,
}
start := time.Now()
c.waitRetryAfter(ctx, "test", &rateLimitError{retryAfter: tt.retryAfter})
if got := time.Since(start); got != tt.want {
t.Errorf("waitRetryAfter; got = %v, want %v", got, tt.want)
}
})
})
}
}