zotregistry.dev/zot@v1.4.4-0.20240314164342-eec277e14d20/pkg/extensions/sync/on_demand.go (about)

     1  //go:build sync
     2  // +build sync
     3  
     4  package sync
     5  
     6  import (
     7  	"context"
     8  	"errors"
     9  	"sync"
    10  	"time"
    11  
    12  	"github.com/containers/common/pkg/retry"
    13  
    14  	zerr "zotregistry.dev/zot/errors"
    15  	"zotregistry.dev/zot/pkg/common"
    16  	"zotregistry.dev/zot/pkg/log"
    17  )
    18  
    19  type request struct {
    20  	repo      string
    21  	reference string
    22  	// used for background retries, at most one background retry per service
    23  	serviceID    int
    24  	isBackground bool
    25  }
    26  
    27  /*
    28  	a request can be an image/signature/sbom
    29  
    30  keep track of all parallel requests, if two requests of same image/signature/sbom comes at the same time,
    31  process just the first one, also keep track of all background retrying routines.
    32  */
    33  type BaseOnDemand struct {
    34  	services []Service
    35  	// map[request]chan err
    36  	requestStore *sync.Map
    37  	log          log.Logger
    38  }
    39  
    40  func NewOnDemand(log log.Logger) *BaseOnDemand {
    41  	return &BaseOnDemand{log: log, requestStore: &sync.Map{}}
    42  }
    43  
    44  func (onDemand *BaseOnDemand) Add(service Service) {
    45  	onDemand.services = append(onDemand.services, service)
    46  }
    47  
    48  func (onDemand *BaseOnDemand) SyncImage(ctx context.Context, repo, reference string) error {
    49  	req := request{
    50  		repo:      repo,
    51  		reference: reference,
    52  	}
    53  
    54  	val, found := onDemand.requestStore.Load(req)
    55  	if found {
    56  		onDemand.log.Info().Str("repo", repo).Str("reference", reference).
    57  			Msg("image already demanded, waiting on channel")
    58  
    59  		syncResult, _ := val.(chan error)
    60  
    61  		err, ok := <-syncResult
    62  		// if channel closed exit
    63  		if !ok {
    64  			return nil
    65  		}
    66  
    67  		return err
    68  	}
    69  
    70  	syncResult := make(chan error)
    71  	onDemand.requestStore.Store(req, syncResult)
    72  
    73  	defer onDemand.requestStore.Delete(req)
    74  	defer close(syncResult)
    75  
    76  	go onDemand.syncImage(ctx, repo, reference, syncResult)
    77  
    78  	err, ok := <-syncResult
    79  	if !ok {
    80  		return nil
    81  	}
    82  
    83  	return err
    84  }
    85  
    86  func (onDemand *BaseOnDemand) SyncReference(ctx context.Context, repo string,
    87  	subjectDigestStr string, referenceType string,
    88  ) error {
    89  	var err error
    90  
    91  	for _, service := range onDemand.services {
    92  		err = service.SetNextAvailableURL()
    93  		if err != nil {
    94  			return err
    95  		}
    96  
    97  		err = service.SyncReference(ctx, repo, subjectDigestStr, referenceType)
    98  		if err != nil {
    99  			continue
   100  		} else {
   101  			return nil
   102  		}
   103  	}
   104  
   105  	return err
   106  }
   107  
   108  func (onDemand *BaseOnDemand) syncImage(ctx context.Context, repo, reference string, syncResult chan error) {
   109  	var err error
   110  	for serviceID, service := range onDemand.services {
   111  		err = service.SetNextAvailableURL()
   112  
   113  		isPingErr := errors.Is(err, zerr.ErrSyncPingRegistry)
   114  		if err != nil && !isPingErr {
   115  			syncResult <- err
   116  
   117  			return
   118  		}
   119  
   120  		// no need to try to sync inline if there is a ping error, we want to retry in background
   121  		if !isPingErr {
   122  			err = service.SyncImage(ctx, repo, reference)
   123  		}
   124  
   125  		if err != nil || isPingErr {
   126  			if errors.Is(err, zerr.ErrManifestNotFound) ||
   127  				errors.Is(err, zerr.ErrSyncImageFilteredOut) ||
   128  				errors.Is(err, zerr.ErrSyncImageNotSigned) {
   129  				continue
   130  			}
   131  
   132  			req := request{
   133  				repo:         repo,
   134  				reference:    reference,
   135  				serviceID:    serviceID,
   136  				isBackground: true,
   137  			}
   138  
   139  			// if there is already a background routine, skip
   140  			if _, requested := onDemand.requestStore.LoadOrStore(req, struct{}{}); requested {
   141  				continue
   142  			}
   143  
   144  			retryOptions := service.GetRetryOptions()
   145  
   146  			if retryOptions.MaxRetry > 0 {
   147  				// retry in background
   148  				go func(service Service) {
   149  					// remove image after syncing
   150  					defer func() {
   151  						onDemand.requestStore.Delete(req)
   152  						onDemand.log.Info().Str("repo", repo).Str("reference", reference).
   153  							Msg("sync routine for image exited")
   154  					}()
   155  
   156  					onDemand.log.Info().Str("repo", repo).Str(reference, "reference").Str("err", err.Error()).
   157  						Str("component", "sync").Msg("starting routine to copy image, because of error")
   158  
   159  					time.Sleep(retryOptions.Delay)
   160  
   161  					// retrying in background, can't use the same context which should be cancelled by now.
   162  					if err = retry.RetryIfNecessary(context.Background(), func() error {
   163  						err := service.SyncImage(context.Background(), repo, reference)
   164  
   165  						return err
   166  					}, retryOptions); err != nil {
   167  						onDemand.log.Error().Str("errorType", common.TypeOf(err)).Str("repo", repo).Str("reference", reference).
   168  							Err(err).Str("component", "sync").Msg("failed to copy image")
   169  					}
   170  				}(service)
   171  			}
   172  		} else {
   173  			break
   174  		}
   175  	}
   176  
   177  	syncResult <- err
   178  }