zotregistry.io/zot@v1.4.4-0.20231124084042-02a8ed785457/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.io/zot/errors"
    15  	"zotregistry.io/zot/pkg/common"
    16  	"zotregistry.io/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  		if err != nil {
   113  			syncResult <- err
   114  
   115  			return
   116  		}
   117  
   118  		err = service.SyncImage(ctx, repo, reference)
   119  		if err != nil {
   120  			if errors.Is(err, zerr.ErrManifestNotFound) ||
   121  				errors.Is(err, zerr.ErrSyncImageFilteredOut) ||
   122  				errors.Is(err, zerr.ErrSyncImageNotSigned) {
   123  				continue
   124  			}
   125  
   126  			req := request{
   127  				repo:         repo,
   128  				reference:    reference,
   129  				serviceID:    serviceID,
   130  				isBackground: true,
   131  			}
   132  
   133  			// if there is already a background routine, skip
   134  			if _, requested := onDemand.requestStore.LoadOrStore(req, struct{}{}); requested {
   135  				continue
   136  			}
   137  
   138  			retryOptions := service.GetRetryOptions()
   139  
   140  			if retryOptions.MaxRetry > 0 {
   141  				// retry in background
   142  				go func(service Service) {
   143  					// remove image after syncing
   144  					defer func() {
   145  						onDemand.requestStore.Delete(req)
   146  						onDemand.log.Info().Str("repo", repo).Str("reference", reference).
   147  							Msg("sync routine for image exited")
   148  					}()
   149  
   150  					onDemand.log.Info().Str("repo", repo).Str(reference, "reference").Str("err", err.Error()).
   151  						Msg("sync routine: starting routine to copy image, because of error")
   152  
   153  					time.Sleep(retryOptions.Delay)
   154  
   155  					// retrying in background, can't use the same context which should be cancelled by now.
   156  					if err = retry.RetryIfNecessary(context.Background(), func() error {
   157  						err := service.SyncImage(context.Background(), repo, reference)
   158  
   159  						return err
   160  					}, retryOptions); err != nil {
   161  						onDemand.log.Error().Str("errorType", common.TypeOf(err)).Str("repo", repo).Str("reference", reference).
   162  							Err(err).Msg("sync routine: error while copying image")
   163  					}
   164  				}(service)
   165  			}
   166  		} else {
   167  			break
   168  		}
   169  	}
   170  
   171  	syncResult <- err
   172  }