git.iko.soy/typeable/oauth2-proxy
on master help sign in
Use session to lock to protect concurrent refreshes
e2c7ff6Kevin Kreitner
pkg/middleware/stored_session.go+98−11
@@ -14,6 +14,11 @@ import (
14 "github.com/oauth2-proxy/oauth2-proxy/v7/providers"
14 "github.com/oauth2-proxy/oauth2-proxy/v7/providers"
15)
15)
16
16
17+const (
18+ SessionLockExpireTime = 5 * time.Second
19+ SessionLockPeekDelay = 50 * time.Millisecond
20+)
21+
17// StoredSessionLoaderOptions contains all of the requirements to construct
22// StoredSessionLoaderOptions contains all of the requirements to construct
18// a stored session loader.
23// a stored session loader.
19// All options must be provided.
24// All options must be provided.
@@ -91,13 +96,10 @@ func (s *storedSessionLoader) loadSession(next http.Handler) http.Handler {
91// that is is valid.
96// that is is valid.
92func (s *storedSessionLoader) getValidatedSession(rw http.ResponseWriter, req *http.Request) (*sessionsapi.SessionState, error) {
97func (s *storedSessionLoader) getValidatedSession(rw http.ResponseWriter, req *http.Request) (*sessionsapi.SessionState, error) {
93 session, err := s.store.Load(req)
98 session, err := s.store.Load(req)
94− if err != nil {
99+ if err != nil || session == nil {
100+ // No session was found in the storage or error occurred, nothing more to do
95 return nil, err
101 return nil, err
96 }
102 }
97− if session == nil {
98− // No session was found in the storage, nothing more to do
99− return nil, nil
100− }
101
103
102 err = s.refreshSessionIfNeeded(rw, req, session)
104 err = s.refreshSessionIfNeeded(rw, req, session)
103 if err != nil {
105 if err != nil {
@@ -116,12 +118,21 @@ func (s *storedSessionLoader) refreshSessionIfNeeded(rw http.ResponseWriter, req
116 return nil
118 return nil
117 }
119 }
118
120
119− logger.Printf("Refreshing session - User: %s; SessionAge: %s", session.User, session.Age())
121+ wasRefreshed, err := s.checkForConcurrentRefresh(session, req)
120− err := s.refreshSession(rw, req, session)
121 if err != nil {
122 if err != nil {
123+ return err
124+ }
125+
126+ // If session was already refreshed via a concurrent request locked skip refreshing,
127+ // because the refreshed session is already loaded from storage
128+ if !wasRefreshed {
129+ logger.Printf("Refreshing session - User: %s; SessionAge: %s", session.User, session.Age())
130+ err = s.refreshSession(rw, req, session)
131+ if err != nil {
122− // If a preemptive refresh fails, we still keep the session
132+ // If a preemptive refresh fails, we still keep the session
123− // if validateSession succeeds.
133+ // if validateSession succeeds.
124− logger.Errorf("Unable to refresh session: %v", err)
134+ logger.Errorf("Unable to refresh session: %v", err)
135+ }
125 }
136 }
126
137
127 // Validate all sessions after any Redeem/Refresh operation (fail or success)
138 // Validate all sessions after any Redeem/Refresh operation (fail or success)
@@ -131,6 +142,18 @@ func (s *storedSessionLoader) refreshSessionIfNeeded(rw http.ResponseWriter, req
131// refreshSession attempts to refresh the session with the provider
142// refreshSession attempts to refresh the session with the provider
132// and will save the session if it was updated.
143// and will save the session if it was updated.
133func (s *storedSessionLoader) refreshSession(rw http.ResponseWriter, req *http.Request, session *sessionsapi.SessionState) error {
144func (s *storedSessionLoader) refreshSession(rw http.ResponseWriter, req *http.Request, session *sessionsapi.SessionState) error {
145+ err := session.ObtainLock(req.Context(), SessionLockExpireTime)
146+ if err != nil {
147+ logger.Errorf("Unable to obtain lock: %v", err)
148+ return s.handleObtainLockError(req, session)
149+ }
150+ defer func() {
151+ err = session.ReleaseLock(req.Context())
152+ if err != nil {
153+ logger.Errorf("unable to release lock: %v", err)
154+ }
155+ }()
156+
134 refreshed, err := s.sessionRefresher(req.Context(), session)
157 refreshed, err := s.sessionRefresher(req.Context(), session)
135 if err != nil && !errors.Is(err, providers.ErrNotImplemented) {
158 if err != nil && !errors.Is(err, providers.ErrNotImplemented) {
136 return fmt.Errorf("error refreshing tokens: %v", err)
159 return fmt.Errorf("error refreshing tokens: %v", err)
@@ -159,11 +182,75 @@ func (s *storedSessionLoader) refreshSession(rw http.ResponseWriter, req *http.R
159 err = s.store.Save(rw, req, session)
182 err = s.store.Save(rw, req, session)
160 if err != nil {
183 if err != nil {
161 logger.PrintAuthf(session.Email, req, logger.AuthError, "error saving session: %v", err)
184 logger.PrintAuthf(session.Email, req, logger.AuthError, "error saving session: %v", err)
162− return fmt.Errorf("error saving session: %v", err)
185+ err = fmt.Errorf("error saving session: %v", err)
186+ }
187+ return err
188+}
189+
190+func (s *storedSessionLoader) handleObtainLockError(req *http.Request, session *sessionsapi.SessionState) error {
191+ wasRefreshed, err := s.checkForConcurrentRefresh(session, req)
192+ if err != nil {
193+ logger.Errorf("Unable to wait for obtained lock: %v", err)
194+ return err
195+ }
196+ if !wasRefreshed {
197+ return errors.New("unable to obtain lock and session was also not refreshed via concurrent request")
198+ }
199+ return nil
200+}
201+
202+func (s *storedSessionLoader) updateSessionFromStore(req *http.Request, session *sessionsapi.SessionState) error {
203+ sessionStored, err := s.store.Load(req)
204+ if err != nil {
205+ return fmt.Errorf("unable to load updated session from store: %v", err)
206+ }
207+
208+ if sessionStored == nil {
209+ return fmt.Errorf("no session available to udpate from store")
163 }
210 }
211+ *session = *sessionStored
212+
164 return nil
213 return nil
165}
214}
166
215
216+func (s *storedSessionLoader) waitForPossibleSessionLock(session *sessionsapi.SessionState, req *http.Request) (bool, error) {
217+ var wasLocked bool
218+ isLocked, err := session.PeekLock(req.Context())
219+ for isLocked {
220+ wasLocked = true
221+ // delay next peek lock
222+ time.Sleep(SessionLockPeekDelay)
223+ isLocked, err = session.PeekLock(req.Context())
224+ }
225+
226+ if err != nil {
227+ return false, err
228+ }
229+
230+ return wasLocked, nil
231+}
232+
233+// checkForConcurrentRefresh returns true if the session is already refreshed via a concurrent request.
234+func (s *storedSessionLoader) checkForConcurrentRefresh(session *sessionsapi.SessionState, req *http.Request) (bool, error) {
235+ wasLocked, err := s.waitForPossibleSessionLock(session, req)
236+ if err != nil {
237+ return false, err
238+ }
239+
240+ refreshed := false
241+ if wasLocked {
242+ logger.Printf("Update session from store instead of refreshing")
243+ err = s.updateSessionFromStore(req, session)
244+ if err != nil {
245+ logger.Errorf("Unable to update session from store: %v", err)
246+ return false, err
247+ }
248+ refreshed = true
249+ }
250+
251+ return refreshed, nil
252+}
253+
167// validateSession checks whether the session has expired and performs
254// validateSession checks whether the session has expired and performs
168// provider validation on the session.
255// provider validation on the session.
169// An error implies the session is not longer valid.
256// An error implies the session is not longer valid.
pkg/middleware/stored_session_test.go+308−24
@@ -6,6 +6,7 @@ import (
6 "fmt"
6 "fmt"
7 "net/http"
7 "net/http"
8 "net/http/httptest"
8 "net/http/httptest"
9+ "sync"
9 "time"
10 "time"
10
11
11 middlewareapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware"
12 middlewareapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware"
@@ -17,9 +18,104 @@ import (
17 . "github.com/onsi/gomega"
18 . "github.com/onsi/gomega"
18)
19)
19
20
21+type TestLock struct {
22+ Locked bool
23+ WasObtained bool
24+ WasRefreshed bool
25+ WasReleased bool
26+ PeekedCount int
27+ LockedOnPeekCount int
28+ ObtainError error
29+ PeekError error
30+ RefreshError error
31+ ReleaseError error
32+}
33+
34+func (l *TestLock) Obtain(_ context.Context, _ time.Duration) error {
35+ if l.ObtainError != nil {
36+ return l.ObtainError
37+ }
38+ l.Locked = true
39+ l.WasObtained = true
40+ return nil
41+}
42+
43+func (l *TestLock) Peek(_ context.Context) (bool, error) {
44+ if l.PeekError != nil {
45+ return false, l.PeekError
46+ }
47+ locked := l.Locked
48+ l.Locked = false
49+ l.PeekedCount++
50+ // mainly used to test case when peek initially returns false,
51+ // but when trying to obtain lock, it returns true.
52+ if l.LockedOnPeekCount == l.PeekedCount {
53+ return true, nil
54+ }
55+ return locked, nil
56+}
57+
58+func (l *TestLock) Refresh(_ context.Context, _ time.Duration) error {
59+ if l.RefreshError != nil {
60+ return l.ReleaseError
61+ }
62+ l.WasRefreshed = true
63+ return nil
64+}
65+
66+func (l *TestLock) Release(_ context.Context) error {
67+ if l.ReleaseError != nil {
68+ return l.ReleaseError
69+ }
70+ l.Locked = false
71+ l.WasReleased = true
72+ return nil
73+}
74+
75+type LockConc struct {
76+ mu sync.Mutex
77+ lock bool
78+ disablePeek bool
79+}
80+
81+func (l *LockConc) Obtain(_ context.Context, _ time.Duration) error {
82+ l.mu.Lock()
83+ if l.lock {
84+ l.mu.Unlock()
85+ return sessionsapi.ErrLockNotObtained
86+ }
87+ l.lock = true
88+ l.mu.Unlock()
89+ return nil
90+}
91+
92+func (l *LockConc) Peek(_ context.Context) (bool, error) {
93+ var response bool
94+ l.mu.Lock()
95+ if l.disablePeek {
96+ response = false
97+ } else {
98+ response = l.lock
99+ }
100+ l.mu.Unlock()
101+ return response, nil
102+}
103+
104+func (l *LockConc) Refresh(_ context.Context, _ time.Duration) error {
105+ return nil
106+}
107+
108+func (l *LockConc) Release(_ context.Context) error {
109+ l.mu.Lock()
110+ l.lock = false
111+ l.mu.Unlock()
112+ return nil
113+}
114+
20var _ = Describe("Stored Session Suite", func() {
115var _ = Describe("Stored Session Suite", func() {
21 const (
116 const (
22 refresh = "Refresh"
117 refresh = "Refresh"
118+ refreshed = "Refreshed"
23 noRefresh = "NoRefresh"
119 noRefresh = "NoRefresh"
24 notImplemented = "NotImplemented"
120 notImplemented = "NotImplemented"
25 )
121 )
@@ -34,7 +130,7 @@ var _ = Describe("Stored Session Suite", func() {
34 var defaultRefreshFunc = func(_ context.Context, ss *sessionsapi.SessionState) (bool, error) {
130 var defaultRefreshFunc = func(_ context.Context, ss *sessionsapi.SessionState) (bool, error) {
35 switch ss.RefreshToken {
131 switch ss.RefreshToken {
36 case refresh:
132 case refresh:
37− ss.RefreshToken = "Refreshed"
133+ ss.RefreshToken = refreshed
38 return true, nil
134 return true, nil
39 case noRefresh:
135 case noRefresh:
40 return false, nil
136 return false, nil
@@ -181,6 +277,7 @@ var _ = Describe("Stored Session Suite", func() {
181 RefreshToken: noRefresh,
277 RefreshToken: noRefresh,
182 CreatedAt: &createdPast,
278 CreatedAt: &createdPast,
183 ExpiresOn: &createdFuture,
279 ExpiresOn: &createdFuture,
280+ Lock: &sessionsapi.NoOpLock{},
184 },
281 },
185 store: defaultSessionStore,
282 store: defaultSessionStore,
186 refreshPeriod: 1 * time.Minute,
283 refreshPeriod: 1 * time.Minute,
@@ -222,6 +319,7 @@ var _ = Describe("Stored Session Suite", func() {
222 RefreshToken: "Refreshed",
319 RefreshToken: "Refreshed",
223 CreatedAt: &now,
320 CreatedAt: &now,
224 ExpiresOn: &createdFuture,
321 ExpiresOn: &createdFuture,
322+ Lock: &sessionsapi.NoOpLock{},
225 },
323 },
226 store: defaultSessionStore,
324 store: defaultSessionStore,
227 refreshPeriod: 1 * time.Minute,
325 refreshPeriod: 1 * time.Minute,
@@ -237,6 +335,7 @@ var _ = Describe("Stored Session Suite", func() {
237 RefreshToken: "RefreshError",
335 RefreshToken: "RefreshError",
238 CreatedAt: &createdPast,
336 CreatedAt: &createdPast,
239 ExpiresOn: &createdFuture,
337 ExpiresOn: &createdFuture,
338+ Lock: &sessionsapi.NoOpLock{},
240 },
339 },
241 store: defaultSessionStore,
340 store: defaultSessionStore,
242 refreshPeriod: 1 * time.Minute,
341 refreshPeriod: 1 * time.Minute,
@@ -266,15 +365,109 @@ var _ = Describe("Stored Session Suite", func() {
266 validateSession: defaultValidateFunc,
365 validateSession: defaultValidateFunc,
267 }),
366 }),
268 )
367 )
368+
369+ type storedSessionLoaderConcurrentTableInput struct {
370+ existingSession *sessionsapi.SessionState
371+ refreshPeriod time.Duration
372+ numConcReqs int
373+ }
374+
375+ DescribeTable("when serving concurrent requests",
376+ func(in storedSessionLoaderConcurrentTableInput) {
377+ lockConc := &LockConc{}
378+
379+ refreshedChan := make(chan bool, in.numConcReqs)
380+ for i := 0; i < in.numConcReqs; i++ {
381+ go func(refreshedChan chan bool, lockConc sessionsapi.Lock) {
382+ existingSession := *in.existingSession // deep copy existingSession state
383+ existingSession.Lock = lockConc
384+ store := &fakeSessionStore{
385+ LoadFunc: func(req *http.Request) (*sessionsapi.SessionState, error) {
386+ return &existingSession, nil
387+ },
388+ SaveFunc: func(http.ResponseWriter, *http.Request, *sessionsapi.SessionState) error {
389+ return nil
390+ },
391+ }
392+
393+ scope := &middlewareapi.RequestScope{
394+ Session: nil,
395+ }
396+
397+ // Set up the request with the request header and a request scope
398+ req := httptest.NewRequest("", "/", nil)
399+ req = middlewareapi.AddRequestScope(req, scope)
400+
401+ rw := httptest.NewRecorder()
402+
403+ sessionRefreshed := false
404+ opts := &StoredSessionLoaderOptions{
405+ SessionStore: store,
406+ RefreshPeriod: in.refreshPeriod,
407+ RefreshSession: func(ctx context.Context, s *sessionsapi.SessionState) (bool, error) {
408+ time.Sleep(10 * time.Millisecond)
409+ sessionRefreshed = true
410+ return true, nil
411+ },
412+ ValidateSession: func(context.Context, *sessionsapi.SessionState) bool {
413+ return true
414+ },
415+ }
416+
417+ handler := NewStoredSessionLoader(opts)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
418+ handler.ServeHTTP(rw, req)
419+
420+ refreshedChan <- sessionRefreshed
421+ }(refreshedChan, lockConc)
422+ }
423+ var refreshedSlice []bool
424+ for i := 0; i < in.numConcReqs; i++ {
425+ refreshedSlice = append(refreshedSlice, <-refreshedChan)
426+ }
427+ sessionRefreshedCount := 0
428+ for _, sessionRefreshed := range refreshedSlice {
429+ if sessionRefreshed {
430+ sessionRefreshedCount++
431+ }
432+ }
433+ Expect(sessionRefreshedCount).To(Equal(1))
434+ },
435+ Entry("with two concurrent requests", storedSessionLoaderConcurrentTableInput{
436+ existingSession: &sessionsapi.SessionState{
437+ RefreshToken: refresh,
438+ CreatedAt: &createdPast,
439+ },
440+ numConcReqs: 2,
441+ refreshPeriod: 1 * time.Minute,
442+ }),
443+ Entry("with 5 concurrent requests", storedSessionLoaderConcurrentTableInput{
444+ existingSession: &sessionsapi.SessionState{
445+ RefreshToken: refresh,
446+ CreatedAt: &createdPast,
447+ },
448+ numConcReqs: 5,
449+ refreshPeriod: 1 * time.Minute,
450+ }),
451+ Entry("with one request", storedSessionLoaderConcurrentTableInput{
452+ existingSession: &sessionsapi.SessionState{
453+ RefreshToken: refresh,
454+ CreatedAt: &createdPast,
455+ },
456+ numConcReqs: 1,
457+ refreshPeriod: 1 * time.Minute,
458+ }),
459+ )
269 })
460 })
270
461
271 Context("refreshSessionIfNeeded", func() {
462 Context("refreshSessionIfNeeded", func() {
272 type refreshSessionIfNeededTableInput struct {
463 type refreshSessionIfNeededTableInput struct {
273− refreshPeriod time.Duration
464+ refreshPeriod time.Duration
465+ sessionStored bool
274− session *sessionsapi.SessionState
466+ session *sessionsapi.SessionState
275− expectedErr error
467+ expectedErr error
276− expectRefreshed bool
468+ expectRefreshed bool
277− expectValidated bool
469+ expectValidated bool
470+ expectedLockState TestLock
278 }
471 }
279
472
280 createdPast := time.Now().Add(-5 * time.Minute)
473 createdPast := time.Now().Add(-5 * time.Minute)
@@ -285,9 +478,18 @@ var _ = Describe("Stored Session Suite", func() {
285 refreshed := false
478 refreshed := false
286 validated := false
479 validated := false
287
480
481+ store := &fakeSessionStore{}
482+ if in.sessionStored {
483+ store = &fakeSessionStore{
484+ LoadFunc: func(req *http.Request) (*sessionsapi.SessionState, error) {
485+ return in.session, nil
486+ },
487+ }
488+ }
489+
288 s := &storedSessionLoader{
490 s := &storedSessionLoader{
289 refreshPeriod: in.refreshPeriod,
491 refreshPeriod: in.refreshPeriod,
290− store: &fakeSessionStore{},
492+ store: store,
291 sessionRefresher: func(_ context.Context, ss *sessionsapi.SessionState) (bool, error) {
493 sessionRefresher: func(_ context.Context, ss *sessionsapi.SessionState) (bool, error) {
292 refreshed = true
494 refreshed = true
293 switch ss.RefreshToken {
495 switch ss.RefreshToken {
@@ -316,89 +518,171 @@ var _ = Describe("Stored Session Suite", func() {
316 }
518 }
317 Expect(refreshed).To(Equal(in.expectRefreshed))
519 Expect(refreshed).To(Equal(in.expectRefreshed))
318 Expect(validated).To(Equal(in.expectValidated))
520 Expect(validated).To(Equal(in.expectValidated))
521+ testLock, ok := in.session.Lock.(*TestLock)
522+ Expect(ok).To(Equal(true))
523+
524+ Expect(testLock).To(Equal(&in.expectedLockState))
319 },
525 },
320 Entry("when the refresh period is 0, and the session does not need refreshing", refreshSessionIfNeededTableInput{
526 Entry("when the refresh period is 0, and the session does not need refreshing", refreshSessionIfNeededTableInput{
321 refreshPeriod: time.Duration(0),
527 refreshPeriod: time.Duration(0),
322 session: &sessionsapi.SessionState{
528 session: &sessionsapi.SessionState{
323 RefreshToken: refresh,
529 RefreshToken: refresh,
324 CreatedAt: &createdFuture,
530 CreatedAt: &createdFuture,
531+ Lock: &TestLock{},
325 },
532 },
326− expectedErr: nil,
533+ expectedErr: nil,
327− expectRefreshed: false,
534+ expectRefreshed: false,
328− expectValidated: false,
535+ expectValidated: false,
536+ expectedLockState: TestLock{},
329 }),
537 }),
330 Entry("when the refresh period is 0, and the session needs refreshing", refreshSessionIfNeededTableInput{
538 Entry("when the refresh period is 0, and the session needs refreshing", refreshSessionIfNeededTableInput{
331 refreshPeriod: time.Duration(0),
539 refreshPeriod: time.Duration(0),
332 session: &sessionsapi.SessionState{
540 session: &sessionsapi.SessionState{
333 RefreshToken: refresh,
541 RefreshToken: refresh,
334 CreatedAt: &createdPast,
542 CreatedAt: &createdPast,
543+ Lock: &TestLock{},
335 },
544 },
336− expectedErr: nil,
545+ expectedErr: nil,
337− expectRefreshed: false,
546+ expectRefreshed: false,
338− expectValidated: false,
547+ expectValidated: false,
548+ expectedLockState: TestLock{},
339 }),
549 }),
340 Entry("when the session does not need refreshing", refreshSessionIfNeededTableInput{
550 Entry("when the session does not need refreshing", refreshSessionIfNeededTableInput{
341 refreshPeriod: 1 * time.Minute,
551 refreshPeriod: 1 * time.Minute,
342 session: &sessionsapi.SessionState{
552 session: &sessionsapi.SessionState{
343 RefreshToken: refresh,
553 RefreshToken: refresh,
344 CreatedAt: &createdFuture,
554 CreatedAt: &createdFuture,
555+ Lock: &TestLock{},
345 },
556 },
346− expectedErr: nil,
557+ expectedErr: nil,
347− expectRefreshed: false,
558+ expectRefreshed: false,
348− expectValidated: false,
559+ expectValidated: false,
560+ expectedLockState: TestLock{},
349 }),
561 }),
350 Entry("when the session is refreshed by the provider", refreshSessionIfNeededTableInput{
562 Entry("when the session is refreshed by the provider", refreshSessionIfNeededTableInput{
351 refreshPeriod: 1 * time.Minute,
563 refreshPeriod: 1 * time.Minute,
352 session: &sessionsapi.SessionState{
564 session: &sessionsapi.SessionState{
353 RefreshToken: refresh,
565 RefreshToken: refresh,
354 CreatedAt: &createdPast,
566 CreatedAt: &createdPast,
567+ Lock: &TestLock{},
355 },
568 },
356 expectedErr: nil,
569 expectedErr: nil,
357 expectRefreshed: true,
570 expectRefreshed: true,
358 expectValidated: true,
571 expectValidated: true,
572+ expectedLockState: TestLock{
573+ Locked: false,
574+ WasObtained: true,
575+ WasReleased: true,
576+ PeekedCount: 1,
577+ },
359 }),
578 }),
360− Entry("when the session is not refreshed by the provider", refreshSessionIfNeededTableInput{
579+ Entry("when the session is locked and instead loaded from storage", refreshSessionIfNeededTableInput{
361 refreshPeriod: 1 * time.Minute,
580 refreshPeriod: 1 * time.Minute,
362 session: &sessionsapi.SessionState{
581 session: &sessionsapi.SessionState{
363 RefreshToken: noRefresh,
582 RefreshToken: noRefresh,
364 CreatedAt: &createdPast,
583 CreatedAt: &createdPast,
365− ExpiresOn: &createdFuture,
584+ Lock: &TestLock{
585+ Locked: true,
586+ },
366 },
587 },
588+ sessionStored: true,
367 expectedErr: nil,
589 expectedErr: nil,
368− expectRefreshed: true,
590+ expectRefreshed: false,
369 expectValidated: true,
591 expectValidated: true,
592+ expectedLockState: TestLock{
593+ Locked: false,
594+ PeekedCount: 2,
595+ },
370 }),
596 }),
371− Entry("when the provider doesn't implement refresh but validation succeeds", refreshSessionIfNeededTableInput{
597+ Entry("when obtaining lock failed, but concurrent request refreshed", refreshSessionIfNeededTableInput{
372 refreshPeriod: 1 * time.Minute,
598 refreshPeriod: 1 * time.Minute,
373 session: &sessionsapi.SessionState{
599 session: &sessionsapi.SessionState{
374− RefreshToken: notImplemented,
600+ RefreshToken: noRefresh,
375 CreatedAt: &createdPast,
601 CreatedAt: &createdPast,
602+ Lock: &TestLock{
603+ ObtainError: errors.New("not able to obtain lock"),
604+ LockedOnPeekCount: 2,
605+ },
606+ },
607+ expectedErr: nil,
608+ expectRefreshed: false,
609+ expectValidated: true,
610+ expectedLockState: TestLock{
611+ PeekedCount: 3,
612+ LockedOnPeekCount: 2,
613+ ObtainError: errors.New("not able to obtain lock"),
614+ },
615+ }),
616+ Entry("when obtaining lock failed", refreshSessionIfNeededTableInput{
617+ refreshPeriod: 1 * time.Minute,
618+ session: &sessionsapi.SessionState{
619+ RefreshToken: noRefresh,
620+ CreatedAt: &createdPast,
621+ Lock: &TestLock{
622+ ObtainError: errors.New("not able to obtain lock"),
623+ },
624+ },
625+ expectedErr: nil,
626+ expectRefreshed: false,
627+ expectValidated: true,
628+ expectedLockState: TestLock{
629+ PeekedCount: 2,
630+ ObtainError: errors.New("not able to obtain lock"),
631+ },
632+ }),
633+ Entry("when the session is not refreshed by the provider", refreshSessionIfNeededTableInput{
634+ refreshPeriod: 1 * time.Minute,
635+ session: &sessionsapi.SessionState{
636+ RefreshToken: noRefresh,
637+ CreatedAt: &createdPast,
638+ ExpiresOn: &createdFuture,
639+ Lock: &TestLock{},
376 },
640 },
377 expectedErr: nil,
641 expectedErr: nil,
378 expectRefreshed: true,
642 expectRefreshed: true,
379 expectValidated: true,
643 expectValidated: true,
644+ expectedLockState: TestLock{
645+ Locked: false,
646+ WasObtained: true,
647+ WasReleased: true,
648+ PeekedCount: 1,
649+ },
380 }),
650 }),
381− Entry("when the provider refresh fails but validation succeeds", refreshSessionIfNeededTableInput{
651+ Entry("when the provider doesn't implement refresh", refreshSessionIfNeededTableInput{
382 refreshPeriod: 1 * time.Minute,
652 refreshPeriod: 1 * time.Minute,
383 session: &sessionsapi.SessionState{
653 session: &sessionsapi.SessionState{
384− RefreshToken: "RefreshError",
654+ RefreshToken: notImplemented,
385 CreatedAt: &createdPast,
655 CreatedAt: &createdPast,
656+ Lock: &TestLock{},
386 },
657 },
387 expectedErr: nil,
658 expectedErr: nil,
388 expectRefreshed: true,
659 expectRefreshed: true,
389 expectValidated: true,
660 expectValidated: true,
661+ expectedLockState: TestLock{
662+ Locked: false,
663+ WasObtained: true,
664+ WasReleased: true,
665+ PeekedCount: 1,
666+ },
390 }),
667 }),
391− Entry("when the session is not refreshed by the provider and validation fails", refreshSessionIfNeededTableInput{
668+ Entry("when the session is not refreshed by the provider", refreshSessionIfNeededTableInput{
392 refreshPeriod: 1 * time.Minute,
669 refreshPeriod: 1 * time.Minute,
393 session: &sessionsapi.SessionState{
670 session: &sessionsapi.SessionState{
394 AccessToken: "Invalid",
671 AccessToken: "Invalid",
395 RefreshToken: noRefresh,
672 RefreshToken: noRefresh,
396 CreatedAt: &createdPast,
673 CreatedAt: &createdPast,
397 ExpiresOn: &createdFuture,
674 ExpiresOn: &createdFuture,
675+ Lock: &TestLock{},
398 },
676 },
399 expectedErr: errors.New("session is invalid"),
677 expectedErr: errors.New("session is invalid"),
400 expectRefreshed: true,
678 expectRefreshed: true,
401 expectValidated: true,
679 expectValidated: true,
680+ expectedLockState: TestLock{
681+ Locked: false,
682+ WasObtained: true,
683+ WasReleased: true,
684+ PeekedCount: 1,
685+ },
402 }),
686 }),
403 )
687 )
404 })
688 })