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 }