github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/resource/retryclient.go (about)

     1  // Copyright 2020 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package resource
     5  
     6  import (
     7  	"fmt"
     8  	"time"
     9  
    10  	"github.com/juju/charm/v12"
    11  	"github.com/juju/clock"
    12  	"github.com/juju/errors"
    13  	"github.com/juju/retry"
    14  )
    15  
    16  // ResourceRetryClient is a wrapper around a Juju repository client that
    17  // retries GetResource() calls.
    18  type ResourceRetryClient struct {
    19  	ResourceGetter
    20  	retryArgs retry.CallArgs
    21  }
    22  
    23  func newRetryClient(client ResourceGetter) *ResourceRetryClient {
    24  	retryArgs := retry.CallArgs{
    25  		// (anastasiamac 2017-05-25) This might not work as the error types
    26  		// may be lost after a call to some clients.
    27  		IsFatalError: func(err error) bool {
    28  			return errors.Is(err, errors.NotFound) || errors.Is(err, errors.NotValid)
    29  		},
    30  		// We don't want to retry for ever.
    31  		// If we cannot get a resource after trying a few times,
    32  		// most likely user intervention is needed.
    33  		Attempts: 3,
    34  		// A one minute gives enough time for potential connection
    35  		// issues to sort themselves out without making the caller wait
    36  		// for an exceptional amount of time.
    37  		Delay: 1 * time.Minute,
    38  		Clock: clock.WallClock,
    39  	}
    40  	return &ResourceRetryClient{
    41  		ResourceGetter: client,
    42  		retryArgs:      retryArgs,
    43  	}
    44  }
    45  
    46  // GetResource returns a reader for the resource's data.
    47  func (client ResourceRetryClient) GetResource(req ResourceRequest) (ResourceData, error) {
    48  	args := client.retryArgs // a copy
    49  
    50  	var data ResourceData
    51  	args.Func = func() error {
    52  		var err error
    53  		data, err = client.ResourceGetter.GetResource(req)
    54  		if err != nil {
    55  			return errors.Trace(err)
    56  		}
    57  		return nil
    58  	}
    59  
    60  	var channelStr string
    61  	stChannel := req.CharmID.Origin.Channel
    62  	if stChannel != nil {
    63  		// Empty string is valid for CharmStore charms.
    64  		channel, err := charm.MakeChannel(stChannel.Track, stChannel.Risk, stChannel.Branch)
    65  		if err != nil {
    66  			return data, errors.Trace(err)
    67  		}
    68  		channelStr = fmt.Sprintf("channel (%v), ", channel.String())
    69  	}
    70  
    71  	var lastErr error
    72  	args.NotifyFunc = func(err error, i int) {
    73  		// Remember the error we're hiding and then retry!
    74  		logger.Warningf("attempt %d/%d to download resource %q from charm store [%scharm (%v), resource revision (%v)] failed with error (will retry): %v",
    75  			i, client.retryArgs.Attempts, req.Name, channelStr, req.CharmID.URL, req.Revision, err)
    76  		logger.Tracef("resource get error stack: %v", errors.ErrorStack(err))
    77  		lastErr = err
    78  	}
    79  
    80  	err := retry.Call(args)
    81  	if retry.IsAttemptsExceeded(err) {
    82  		return data, errors.Annotate(lastErr, "failed after retrying")
    83  	}
    84  	if err != nil {
    85  		return data, errors.Trace(err)
    86  	}
    87  
    88  	return data, nil
    89  }