@ -8,6 +8,7 @@ import (
"io"
"log/slog"
"math"
"math/rand/v2"
"net/http"
"net/url"
"os"
@ -141,6 +142,32 @@ func (b *blobDownload) Run(ctx context.Context, requestURL *url.URL, opts *regis
b . err = b . run ( ctx , requestURL , opts )
}
func newBackoff ( maxBackoff time . Duration ) func ( ctx context . Context ) error {
var n int
return func ( ctx context . Context ) error {
if ctx . Err ( ) != nil {
return ctx . Err ( )
}
n ++
// n^2 backoff timer is a little smoother than the
// common choice of 2^n.
d := min ( time . Duration ( n * n ) * 10 * time . Millisecond , maxBackoff )
// Randomize the delay between 0.5-1.5 x msec, in order
// to prevent accidental "thundering herd" problems.
d = time . Duration ( float64 ( d ) * ( rand . Float64 ( ) + 0.5 ) )
t := time . NewTimer ( d )
defer t . Stop ( )
select {
case <- ctx . Done ( ) :
return ctx . Err ( )
case <- t . C :
return nil
}
}
}
func ( b * blobDownload ) run ( ctx context . Context , requestURL * url . URL , opts * registryOptions ) error {
defer blobDownloadManager . Delete ( b . Digest )
ctx , b . CancelFunc = context . WithCancel ( ctx )
@ -153,6 +180,52 @@ func (b *blobDownload) run(ctx context.Context, requestURL *url.URL, opts *regis
_ = file . Truncate ( b . Total )
directURL , err := func ( ) ( * url . URL , error ) {
ctx , cancel := context . WithTimeout ( ctx , 30 * time . Second )
defer cancel ( )
backoff := newBackoff ( 10 * time . Second )
for {
// shallow clone opts to be used in the closure
// without affecting the outer opts.
newOpts := new ( registryOptions )
* newOpts = * opts
newOpts . CheckRedirect = func ( req * http . Request , via [ ] * http . Request ) error {
if len ( via ) > 10 {
return errors . New ( "maxium redirects exceeded (10) for directURL" )
}
// if the hostname is the same, allow the redirect
if req . URL . Hostname ( ) == requestURL . Hostname ( ) {
return nil
}
// stop at the first redirect that is not
// the same hostname as the original
// request.
return http . ErrUseLastResponse
}
resp , err := makeRequestWithRetry ( ctx , http . MethodGet , requestURL , nil , nil , newOpts )
if err != nil {
slog . Warn ( "failed to get direct URL; backing off and retrying" , "err" , err )
if err := backoff ( ctx ) ; err != nil {
return nil , err
}
continue
}
defer resp . Body . Close ( )
if resp . StatusCode != http . StatusTemporaryRedirect {
return nil , fmt . Errorf ( "unexpected status code %d" , resp . StatusCode )
}
return resp . Location ( )
}
} ( )
if err != nil {
return err
}
g , inner := errgroup . WithContext ( ctx )
g . SetLimit ( numDownloadParts )
for i := range b . Parts {
@ -165,7 +238,7 @@ func (b *blobDownload) run(ctx context.Context, requestURL *url.URL, opts *regis
var err error
for try := 0 ; try < maxRetries ; try ++ {
w := io . NewOffsetWriter ( file , part . StartsAt ( ) )
err = b . downloadChunk ( inner , reques tURL, w , part , opts )
err = b . downloadChunk ( inner , direc tURL, w , part , opts )
switch {
case errors . Is ( err , context . Canceled ) , errors . Is ( err , syscall . ENOSPC ) :
// return immediately if the context is canceled or the device is out of space