Skip to content

Commit 83811da

Browse files
enjsimo5
authored andcommitted
Fix timeout validator unit test flakes
Signed-off-by: Monis Khan <[email protected]> Signed-off-by: Simo Sorce <[email protected]>
1 parent 0ed56a9 commit 83811da

File tree

2 files changed

+177
-43
lines changed

2 files changed

+177
-43
lines changed

pkg/auth/oauth/registry/registry_test.go

Lines changed: 130 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1515
"k8s.io/apimachinery/pkg/labels"
1616
"k8s.io/apimachinery/pkg/runtime"
17+
"k8s.io/apimachinery/pkg/util/clock"
1718
"k8s.io/apiserver/pkg/authentication/authenticator"
1819
"k8s.io/apiserver/pkg/authentication/user"
1920
clienttesting "k8s.io/client-go/testing"
@@ -486,15 +487,34 @@ func (f fakeOAuthClientLister) Get(name string) (*oapi.OAuthClient, error) {
486487
}
487488

488489
func (f fakeOAuthClientLister) List(selector labels.Selector) ([]*oapi.OAuthClient, error) {
489-
var list []*oapi.OAuthClient
490-
ret, _ := f.clients.List(metav1.ListOptions{})
491-
for i := range ret.Items {
492-
list = append(list, &ret.Items[i])
493-
}
494-
return list, nil
490+
panic("not used")
491+
}
492+
493+
type fakeTicker struct {
494+
clock *clock.FakeClock
495+
ch <-chan time.Time
496+
}
497+
498+
func (t *fakeTicker) Now() time.Time {
499+
return t.clock.Now()
500+
}
501+
502+
func (t *fakeTicker) C() <-chan time.Time {
503+
return t.ch
504+
}
505+
506+
func (t *fakeTicker) Stop() {}
507+
508+
func (t *fakeTicker) NewTicker(d time.Duration) {
509+
t.ch = t.clock.Tick(d)
495510
}
496511

497-
func checkToken(t *testing.T, name string, authf authenticator.Token, tokens oauthclient.OAuthAccessTokenInterface, present bool) {
512+
func (t *fakeTicker) Sleep(d time.Duration) {
513+
t.clock.Sleep(d)
514+
}
515+
516+
func checkToken(t *testing.T, name string, authf authenticator.Token, tokens oauthclient.OAuthAccessTokenInterface, current *fakeTicker, present bool) {
517+
t.Helper()
498518
userInfo, found, err := authf.AuthenticateToken(name)
499519
if present {
500520
if !found {
@@ -508,9 +528,12 @@ func checkToken(t *testing.T, name string, authf authenticator.Token, tokens oau
508528
}
509529
} else {
510530
if found {
511-
token, _ := tokens.Get(name, metav1.GetOptions{})
531+
token, tokenErr := tokens.Get(name, metav1.GetOptions{})
532+
if tokenErr != nil {
533+
t.Fatal(tokenErr)
534+
}
512535
t.Errorf("Found token (created=%s, timeout=%di, now=%s), but it should be gone!",
513-
token.CreationTimestamp, token.InactivityTimeoutSeconds, time.Now())
536+
token.CreationTimestamp, token.InactivityTimeoutSeconds, current.Now())
514537
}
515538
if err != errTimedout {
516539
t.Errorf("Unexpected error checking absence of token %s: %v", name, err)
@@ -521,13 +544,24 @@ func checkToken(t *testing.T, name string, authf authenticator.Token, tokens oau
521544
}
522545
}
523546

547+
func wait(t *testing.T, c chan struct{}) {
548+
t.Helper()
549+
select {
550+
case <-c:
551+
case <-time.After(30 * time.Second):
552+
t.Fatal("failed to see channel event")
553+
}
554+
}
555+
524556
func TestAuthenticateTokenTimeout(t *testing.T) {
525557
stopCh := make(chan struct{})
526558
defer close(stopCh)
527559

560+
testClock := &fakeTicker{clock: clock.NewFakeClock(time.Time{})}
561+
528562
defaultTimeout := int32(30) // 30 seconds
529-
clientTimeout := int32(15) // 15 seconds so flush -> 5 seconds
530-
minTimeout := int32(10) // 10 seconds
563+
clientTimeout := int32(15) // 15 seconds
564+
minTimeout := int32(10) // 10 seconds -> 10/3 = a tick per 3 seconds
531565

532566
testClient := oapi.OAuthClient{
533567
ObjectMeta: metav1.ObjectMeta{Name: "testClient"},
@@ -541,31 +575,31 @@ func TestAuthenticateTokenTimeout(t *testing.T) {
541575
ObjectMeta: metav1.ObjectMeta{Name: "slowClient"},
542576
}
543577
testToken := oapi.OAuthAccessToken{
544-
ObjectMeta: metav1.ObjectMeta{Name: "testToken", CreationTimestamp: metav1.Time{Time: time.Now()}},
578+
ObjectMeta: metav1.ObjectMeta{Name: "testToken", CreationTimestamp: metav1.Time{Time: testClock.Now()}},
545579
ClientName: "testClient",
546580
ExpiresIn: 600, // 10 minutes
547581
UserName: "foo",
548582
UserUID: string("bar"),
549583
InactivityTimeoutSeconds: clientTimeout,
550584
}
551585
quickToken := oapi.OAuthAccessToken{
552-
ObjectMeta: metav1.ObjectMeta{Name: "quickToken", CreationTimestamp: metav1.Time{Time: time.Now()}},
586+
ObjectMeta: metav1.ObjectMeta{Name: "quickToken", CreationTimestamp: metav1.Time{Time: testClock.Now()}},
553587
ClientName: "quickClient",
554588
ExpiresIn: 600, // 10 minutes
555589
UserName: "foo",
556590
UserUID: string("bar"),
557591
InactivityTimeoutSeconds: minTimeout,
558592
}
559593
slowToken := oapi.OAuthAccessToken{
560-
ObjectMeta: metav1.ObjectMeta{Name: "slowToken", CreationTimestamp: metav1.Time{Time: time.Now()}},
594+
ObjectMeta: metav1.ObjectMeta{Name: "slowToken", CreationTimestamp: metav1.Time{Time: testClock.Now()}},
561595
ClientName: "slowClient",
562596
ExpiresIn: 600, // 10 minutes
563597
UserName: "foo",
564598
UserUID: string("bar"),
565599
InactivityTimeoutSeconds: defaultTimeout,
566600
}
567601
emergToken := oapi.OAuthAccessToken{
568-
ObjectMeta: metav1.ObjectMeta{Name: "emergToken", CreationTimestamp: metav1.Time{Time: time.Now()}},
602+
ObjectMeta: metav1.ObjectMeta{Name: "emergToken", CreationTimestamp: metav1.Time{Time: testClock.Now()}},
569603
ClientName: "quickClient",
570604
ExpiresIn: 600, // 10 minutes
571605
UserName: "foo",
@@ -580,34 +614,77 @@ func TestAuthenticateTokenTimeout(t *testing.T) {
580614
lister := &fakeOAuthClientLister{
581615
clients: oauthClients,
582616
}
617+
583618
timeouts := NewTimeoutValidator(accessTokenGetter, lister, defaultTimeout, minTimeout)
619+
620+
// inject fake clock, which has some interesting properties
621+
// 1. A sleep will cause at most one ticker event, regardless of how long the sleep was
622+
// 2. The clock will hold one tick event and will drop the next one if something does not consume it first
623+
timeouts.ticker = testClock
624+
625+
// decorate flush
626+
// The fake clock 1. and 2. require that we issue a wait(t, timeoutsSync) after each testClock.Sleep that causes a tick
627+
originalFlush := timeouts.flushHandler
628+
timeoutsSync := make(chan struct{})
629+
timeouts.flushHandler = func(flushHorizon time.Time) {
630+
originalFlush(flushHorizon)
631+
timeoutsSync <- struct{}{} // signal that flush is complete so we never race against it
632+
}
633+
634+
// decorate putToken
635+
// We must issue a wait(t, putTokenSync) after each call to checkToken that should be successful
636+
originalPutToken := timeouts.putTokenHandler
637+
putTokenSync := make(chan struct{})
638+
timeouts.putTokenHandler = func(td *tokenData) {
639+
originalPutToken(td)
640+
putTokenSync <- struct{}{} // signal that putToken is complete so we never race against it
641+
}
642+
643+
// add some padding to all sleep invocations to make sure we are not failing on any boundary values
644+
buffer := time.Nanosecond
645+
584646
tokenAuthenticator := NewTokenAuthenticator(accessTokenGetter, userRegistry, identitymapper.NoopGroupMapper{}, timeouts)
585647

586648
go timeouts.Run(stopCh)
587649

588-
// wait to see that the other thread has updated the timeouts
589-
time.Sleep(1 * time.Second)
590-
591-
// TIME: 1 seconds have passed here
650+
// TIME: 0 seconds have passed here
592651

593652
// first time should succeed for all
594-
checkToken(t, "testToken", tokenAuthenticator, accessTokenGetter, true)
595-
checkToken(t, "quickToken", tokenAuthenticator, accessTokenGetter, true)
596-
checkToken(t, "slowToken", tokenAuthenticator, accessTokenGetter, true)
653+
checkToken(t, "testToken", tokenAuthenticator, accessTokenGetter, testClock, true)
654+
wait(t, putTokenSync)
655+
656+
checkToken(t, "quickToken", tokenAuthenticator, accessTokenGetter, testClock, true)
657+
wait(t, putTokenSync)
658+
659+
wait(t, timeoutsSync) // from emergency flush because quickToken has a short enough timeout
660+
661+
checkToken(t, "slowToken", tokenAuthenticator, accessTokenGetter, testClock, true)
662+
wait(t, putTokenSync)
663+
597664
// this should cause an emergency flush, if not the next auth will fail,
598665
// as the token will be timed out
599-
checkToken(t, "emergToken", tokenAuthenticator, accessTokenGetter, true)
666+
checkToken(t, "emergToken", tokenAuthenticator, accessTokenGetter, testClock, true)
667+
wait(t, putTokenSync)
668+
669+
wait(t, timeoutsSync) // from emergency flush because emergToken has a super short timeout
600670

601-
// wait 5 seconds
602-
time.Sleep(5 * time.Second)
671+
// wait 6 seconds
672+
testClock.Sleep(5*time.Second + buffer)
673+
674+
// a tick happens every 3 seconds
675+
wait(t, timeoutsSync)
603676

604677
// TIME: 6th second
605678

606679
// See if emergency flush happened
607-
checkToken(t, "emergToken", tokenAuthenticator, accessTokenGetter, true)
680+
checkToken(t, "emergToken", tokenAuthenticator, accessTokenGetter, testClock, true)
681+
wait(t, putTokenSync)
682+
683+
wait(t, timeoutsSync) // from emergency flush because emergToken has a super short timeout
608684

609685
// wait for timeout (minTimeout + 1 - the previously waited 6 seconds)
610-
time.Sleep(time.Duration(minTimeout-5) * time.Second)
686+
testClock.Sleep(time.Duration(minTimeout-5)*time.Second + buffer)
687+
wait(t, timeoutsSync)
611688

612689
// TIME: 11th second
613690

@@ -625,21 +702,34 @@ func TestAuthenticateTokenTimeout(t *testing.T) {
625702
}
626703
}
627704

628-
// this should fail
629-
checkToken(t, "quickToken", tokenAuthenticator, accessTokenGetter, false)
705+
// this should fail, thus no call to wait(t, putTokenSync)
706+
checkToken(t, "quickToken", tokenAuthenticator, accessTokenGetter, testClock, false)
707+
630708
// while this should get updated
631-
checkToken(t, "testToken", tokenAuthenticator, accessTokenGetter, true)
709+
checkToken(t, "testToken", tokenAuthenticator, accessTokenGetter, testClock, true)
710+
wait(t, putTokenSync)
711+
712+
wait(t, timeoutsSync)
632713

633714
// wait for timeout
634-
time.Sleep(time.Duration(clientTimeout+1) * time.Second)
715+
testClock.Sleep(time.Duration(clientTimeout+1)*time.Second + buffer)
716+
717+
// 16 seconds equals 5 more flushes, but the fake clock will only tick once during this time
718+
wait(t, timeoutsSync)
635719

636720
// TIME: 27th second
637721

638722
// this should get updated
639-
checkToken(t, "slowToken", tokenAuthenticator, accessTokenGetter, true)
723+
checkToken(t, "slowToken", tokenAuthenticator, accessTokenGetter, testClock, true)
724+
wait(t, putTokenSync)
725+
726+
wait(t, timeoutsSync)
640727

641728
// while this should not fail
642-
checkToken(t, "testToken", tokenAuthenticator, accessTokenGetter, true)
729+
checkToken(t, "testToken", tokenAuthenticator, accessTokenGetter, testClock, true)
730+
wait(t, putTokenSync)
731+
732+
wait(t, timeoutsSync)
643733
// and should be updated to last at least till the 31st second
644734
token, err := accessTokenGetter.Get("testToken", metav1.GetOptions{})
645735
if err != nil {
@@ -663,10 +753,15 @@ func TestAuthenticateTokenTimeout(t *testing.T) {
663753
}
664754

665755
// and wait until test token should time out, and has been flushed for sure
666-
time.Sleep(time.Duration(minTimeout) * time.Second)
756+
testClock.Sleep(time.Duration(minTimeout)*time.Second + buffer)
757+
wait(t, timeoutsSync)
667758

668759
// while this should not fail
669-
checkToken(t, "testToken", tokenAuthenticator, accessTokenGetter, true)
760+
checkToken(t, "testToken", tokenAuthenticator, accessTokenGetter, testClock, true)
761+
wait(t, putTokenSync)
762+
763+
wait(t, timeoutsSync)
764+
670765
// and should be updated to have a ZERO timeout
671766
token, err = accessTokenGetter.Get("testToken", metav1.GetOptions{})
672767
if err != nil {

pkg/auth/oauth/registry/timeoutvalidator.go

Lines changed: 47 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -43,13 +43,48 @@ func timeoutAsDuration(timeout int32) time.Duration {
4343
return time.Duration(timeout) * time.Second
4444
}
4545

46+
// This interface is used to allow mocking time from unit tests
47+
// NOTE: Never use time.Now() directly in the code, use this interface
48+
// Now() function instead.
49+
type internalTickerInterface interface {
50+
Now() time.Time
51+
NewTicker(d time.Duration)
52+
C() <-chan time.Time
53+
Stop()
54+
}
55+
56+
type internalTicker struct {
57+
ticker *time.Ticker
58+
}
59+
60+
func (t *internalTicker) Now() time.Time {
61+
return time.Now()
62+
}
63+
64+
func (t *internalTicker) C() <-chan time.Time {
65+
return t.ticker.C
66+
}
67+
68+
func (t *internalTicker) Stop() {
69+
t.ticker.Stop()
70+
}
71+
72+
func (t *internalTicker) NewTicker(d time.Duration) {
73+
t.ticker = time.NewTicker(d)
74+
}
75+
4676
type TimeoutValidator struct {
4777
oauthClients oauthclientlister.OAuthClientLister
4878
tokens oauthclient.OAuthAccessTokenInterface
4979
tokenChannel chan *tokenData
5080
data *rankedset.RankedSet
5181
defaultTimeout time.Duration
5282
tickerInterval time.Duration
83+
84+
// fields that are used to have a deterministic order of events in unit tests
85+
flushHandler func(flushHorizon time.Time) // allows us to decorate this func during unit tests
86+
putTokenHandler func(td *tokenData) // allows us to decorate this func during unit tests
87+
ticker internalTickerInterface // allows us to control time during unit tests
5388
}
5489

5590
func NewTimeoutValidator(tokens oauthclient.OAuthAccessTokenInterface, oauthClients oauthclientlister.OAuthClientLister, defaultTimeout int32, minValidTimeout int32) *TimeoutValidator {
@@ -60,7 +95,10 @@ func NewTimeoutValidator(tokens oauthclient.OAuthAccessTokenInterface, oauthClie
6095
data: rankedset.New(),
6196
defaultTimeout: timeoutAsDuration(defaultTimeout),
6297
tickerInterval: timeoutAsDuration(minValidTimeout / 3), // we tick at least 3 times within each timeout period
98+
ticker: &internalTicker{},
6399
}
100+
a.flushHandler = a.flush
101+
a.putTokenHandler = a.putToken
64102
glog.V(5).Infof("Token Timeout Validator primed with defaultTimeout=%s tickerInterval=%s", a.defaultTimeout, a.tickerInterval)
65103
return a
66104
}
@@ -75,7 +113,7 @@ func (a *TimeoutValidator) Validate(token *oauth.OAuthAccessToken, _ *user.User)
75113

76114
td := &tokenData{
77115
token: token,
78-
seen: time.Now(),
116+
seen: a.ticker.Now(),
79117
}
80118
if td.timeout().Before(td.seen) {
81119
return errTimedout
@@ -88,7 +126,7 @@ func (a *TimeoutValidator) Validate(token *oauth.OAuthAccessToken, _ *user.User)
88126
// After a positive timeout check we need to update the timeout and
89127
// schedule an update so that we can either set or update the Timeout
90128
// we do that launching a micro goroutine to avoid blocking
91-
go a.putToken(td)
129+
go a.putTokenHandler(td)
92130

93131
return nil
94132
}
@@ -186,16 +224,16 @@ func (a *TimeoutValidator) flush(flushHorizon time.Time) {
186224
func (a *TimeoutValidator) nextTick() time.Time {
187225
// Add a small safety Margin so flushes tend to
188226
// overlap a little rather than have gaps
189-
return time.Now().Add(a.tickerInterval + 10*time.Second)
227+
return a.ticker.Now().Add(a.tickerInterval + 10*time.Second)
190228
}
191229

192230
func (a *TimeoutValidator) Run(stopCh <-chan struct{}) {
193231
defer runtime.HandleCrash()
194232
glog.V(5).Infof("Started Token Timeout Flush Handling thread!")
195233

196-
ticker := time.NewTicker(a.tickerInterval)
234+
a.ticker.NewTicker(a.tickerInterval)
197235
// make sure to kill the ticker when we exit
198-
defer ticker.Stop()
236+
defer a.ticker.Stop()
199237

200238
nextTick := a.nextTick()
201239

@@ -204,19 +242,20 @@ func (a *TimeoutValidator) Run(stopCh <-chan struct{}) {
204242
case <-stopCh:
205243
// if channel closes terminate
206244
return
245+
207246
case td := <-a.tokenChannel:
208247
a.data.Insert(td)
209248
// if this token is going to time out before the timer, flush now
210249
tokenTimeout := td.timeout()
211250
if tokenTimeout.Before(nextTick) {
212251
glog.V(5).Infof("Timeout for user=%q client=%q scopes=%v falls before next ticker (%s < %s), forcing flush!",
213252
td.token.UserName, td.token.ClientName, td.token.Scopes, tokenTimeout, nextTick)
214-
a.flush(nextTick)
253+
a.flushHandler(nextTick)
215254
}
216255

217-
case <-ticker.C:
256+
case <-a.ticker.C():
218257
nextTick = a.nextTick()
219-
a.flush(nextTick)
258+
a.flushHandler(nextTick)
220259
}
221260
}
222261
}

0 commit comments

Comments
 (0)