Skip to content

Commit bc31388

Browse files
committed
Only obtain a bearer token once at a time
Currently, on pushes, we can start several concurrent layer pushes; each one will check for a bearer token in tokenCache, find none, and ask the server for one, and then write it into the cache. So, we can hammer the server with 6 basically-concurrent token requests. That's unnecessary, slower than just asking once, and potentially might impact rate limiting heuristics. Instead, serialize writes to a bearerToken so that we only have one request in flight at a time. This does not benefit pulls, where the first request is for a manifest; that obtains a token, so subsequent concurrent layer pulls will not request a token again. Signed-off-by: Miloslav Trmač <[email protected]>
1 parent 839b56d commit bc31388

File tree

1 file changed

+53
-19
lines changed

1 file changed

+53
-19
lines changed

image/docker/docker_client.go

Lines changed: 53 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import (
3535
"go.podman.io/image/v5/types"
3636
"go.podman.io/storage/pkg/fileutils"
3737
"go.podman.io/storage/pkg/homedir"
38+
"golang.org/x/sync/semaphore"
3839
)
3940

4041
const (
@@ -86,8 +87,19 @@ type extensionSignatureList struct {
8687
Signatures []extensionSignature `json:"signatures"`
8788
}
8889

89-
// bearerToken records a cached token we can use to authenticate.
90+
// bearerToken records a cached token we can use to authenticate, or a pending process to obtain one.
91+
//
92+
// The goroutine obtaining the token holds lock to block concurrent token requests, and fills the structure (err and possibly the other fields)
93+
// before releasing the lock.
94+
// Other goroutines obtain lock to block on the token request, if any; and then inspect err to see if the token is usable.
95+
// If it is not, they try to get a new one.
9096
type bearerToken struct {
97+
// lock is held while obtaining the token. Potentially nested inside dockerClient.tokenCacheLock.
98+
// This is a counting semaphore only because we need a cancellable lock operation.
99+
lock *semaphore.Weighted
100+
101+
// The following fields can only be accessed with lock held.
102+
err error // nil if the token was successfully obtained (but may be expired); an error if the next lock holder _must_ obtain a new token.
91103
token string
92104
expirationTime time.Time
93105
}
@@ -756,31 +768,53 @@ func (c *dockerClient) obtainBearerToken(ctx context.Context, challenge challeng
756768
scopes = append(scopes, *extraScope)
757769
}
758770

759-
var token *bearerToken
760-
var inCache bool
761-
func() { // A scope for defer
771+
token, newEntry, err := func() (*bearerToken, bool, error) { // A scope for defer
762772
c.tokenCacheLock.Lock()
763773
defer c.tokenCacheLock.Unlock()
764-
token, inCache = c.tokenCache[cacheKey]
765-
}()
766-
if !inCache || time.Now().After(token.expirationTime) {
767-
token = &bearerToken{}
768-
769-
var err error
770-
if c.auth.IdentityToken != "" {
771-
err = c.getBearerTokenOAuth2(ctx, token, challenge, scopes)
774+
token, ok := c.tokenCache[cacheKey]
775+
if ok {
776+
return token, false, nil
772777
} else {
773-
err = c.getBearerToken(ctx, token, challenge, scopes)
778+
token = &bearerToken{
779+
lock: semaphore.NewWeighted(1),
780+
}
781+
// If this is a new *bearerToken, lock the entry before adding it to the cache, so that any other goroutine that finds
782+
// this entry blocks until we obtain the token for the first time, and does not see an empty object
783+
// (and does not try to obtain the token itself when we are going to do so).
784+
if err := token.lock.Acquire(ctx, 1); err != nil {
785+
// We do not block on this Acquire, so we don’t really expect to fail here — but if ctx is canceled,
786+
// there is no point in trying to continue anyway.
787+
return nil, false, err
788+
}
789+
c.tokenCache[cacheKey] = token
790+
return token, true, nil
774791
}
775-
if err != nil {
792+
}()
793+
if err != nil {
794+
return "", err
795+
}
796+
if !newEntry {
797+
// If this is an existing *bearerToken, obtain the lock only after releasing c.tokenCacheLock,
798+
// so that users of other cacheKey values are not blocked for the whole duration of our HTTP roundtrip.
799+
if err := token.lock.Acquire(ctx, 1); err != nil {
776800
return "", err
777801
}
802+
}
778803

779-
func() { // A scope for defer
780-
c.tokenCacheLock.Lock()
781-
defer c.tokenCacheLock.Unlock()
782-
c.tokenCache[cacheKey] = token
783-
}()
804+
defer token.lock.Release(1)
805+
806+
if !newEntry && token.err == nil && !time.Now().After(token.expirationTime) {
807+
return token.token, nil // We have a usable token already.
808+
}
809+
810+
if c.auth.IdentityToken != "" {
811+
err = c.getBearerTokenOAuth2(ctx, token, challenge, scopes)
812+
} else {
813+
err = c.getBearerToken(ctx, token, challenge, scopes)
814+
}
815+
token.err = err
816+
if token.err != nil {
817+
return "", token.err
784818
}
785819
return token.token, nil
786820
}

0 commit comments

Comments
 (0)