github.com/pf-qiu/concourse/v6@v6.7.3-0.20201207032516-1f455d73275f/atc/engine/check_delegate.go (about) 1 package engine 2 3 import ( 4 "context" 5 "fmt" 6 "time" 7 8 "code.cloudfoundry.org/clock" 9 "code.cloudfoundry.org/lager/lagerctx" 10 "github.com/pf-qiu/concourse/v6/atc" 11 "github.com/pf-qiu/concourse/v6/atc/db" 12 "github.com/pf-qiu/concourse/v6/atc/db/lock" 13 "github.com/pf-qiu/concourse/v6/atc/event" 14 "github.com/pf-qiu/concourse/v6/atc/exec" 15 "github.com/pf-qiu/concourse/v6/atc/policy" 16 ) 17 18 //go:generate counterfeiter . RateLimiter 19 20 type RateLimiter interface { 21 Wait(context.Context) error 22 } 23 24 func NewCheckDelegate( 25 build db.Build, 26 plan atc.Plan, 27 state exec.RunState, 28 clock clock.Clock, 29 limiter RateLimiter, 30 policyChecker policy.Checker, 31 ) exec.CheckDelegate { 32 return &checkDelegate{ 33 BuildStepDelegate: NewBuildStepDelegate(build, plan.ID, state, clock, policyChecker), 34 35 build: build, 36 plan: plan.Check, 37 eventOrigin: event.Origin{ID: event.OriginID(plan.ID)}, 38 clock: clock, 39 40 limiter: limiter, 41 } 42 } 43 44 type checkDelegate struct { 45 exec.BuildStepDelegate 46 47 build db.Build 48 plan *atc.CheckPlan 49 eventOrigin event.Origin 50 clock clock.Clock 51 52 // stashed away just so we don't have to query them multiple times 53 cachedPipeline db.Pipeline 54 cachedResource db.Resource 55 cachedResourceType db.ResourceType 56 57 limiter RateLimiter 58 } 59 60 func (d *checkDelegate) FindOrCreateScope(config db.ResourceConfig) (db.ResourceConfigScope, error) { 61 resource, _, err := d.resource() 62 if err != nil { 63 return nil, fmt.Errorf("get resource: %w", err) 64 } 65 66 scope, err := config.FindOrCreateScope(resource) // ignore found, nil is ok 67 if err != nil { 68 return nil, fmt.Errorf("find or create scope: %w", err) 69 } 70 71 return scope, nil 72 } 73 74 func (d *checkDelegate) WaitToRun(ctx context.Context, scope db.ResourceConfigScope) (lock.Lock, bool, error) { 75 logger := lagerctx.FromContext(ctx) 76 77 // rate limit periodic resource checks so worker load (plus load on external 78 // services) isn't too spiky 79 if !d.build.IsManuallyTriggered() && d.plan.Resource != "" { 80 err := d.limiter.Wait(ctx) 81 if err != nil { 82 return nil, false, fmt.Errorf("rate limit: %w", err) 83 } 84 } 85 86 var err error 87 88 var interval time.Duration 89 if d.plan.Interval != "" { 90 interval, err = time.ParseDuration(d.plan.Interval) 91 if err != nil { 92 return nil, false, err 93 } 94 } 95 96 var lock lock.Lock = lock.NoopLock{} 97 if d.plan.Resource != "" { 98 for { 99 var acquired bool 100 lock, acquired, err = scope.AcquireResourceCheckingLock(logger) 101 if err != nil { 102 return nil, false, fmt.Errorf("acquire lock: %w", err) 103 } 104 105 if acquired { 106 break 107 } 108 109 d.clock.Sleep(time.Second) 110 } 111 } 112 113 end, err := scope.LastCheckEndTime() 114 if err != nil { 115 if releaseErr := lock.Release(); releaseErr != nil { 116 logger.Error("failed-to-release-lock", releaseErr) 117 } 118 119 return nil, false, fmt.Errorf("get last check end time: %w", err) 120 } 121 122 runAt := end.Add(interval) 123 124 shouldRun := false 125 if d.build.IsManuallyTriggered() { 126 // ignore interval for manually triggered builds 127 shouldRun = true 128 } else if !d.clock.Now().Before(runAt) { 129 // run if we're past the last check end time 130 shouldRun = true 131 } else { 132 // XXX(check-refactor): we could potentially sleep here until runAt is 133 // reached. 134 // 135 // then the check build queueing logic is to just make sure there's a build 136 // running for every resource, without having to check if intervals have 137 // elapsed. 138 // 139 // this could be expanded upon to short-circuit the waiting with events 140 // triggered by webhooks so that webhooks are super responsive: rather than 141 // queueing a build, it would just wake up a goroutine. 142 } 143 144 if !shouldRun { 145 err := lock.Release() 146 if err != nil { 147 return nil, false, fmt.Errorf("release lock: %w", err) 148 } 149 150 return nil, false, nil 151 } 152 153 return lock, true, nil 154 } 155 156 func (d *checkDelegate) PointToCheckedConfig(scope db.ResourceConfigScope) error { 157 resource, found, err := d.resource() 158 if err != nil { 159 return fmt.Errorf("get resource: %w", err) 160 } 161 162 if found { 163 err := resource.SetResourceConfigScope(scope) 164 if err != nil { 165 return fmt.Errorf("set resource scope: %w", err) 166 } 167 } 168 169 resourceType, found, err := d.resourceType() 170 if err != nil { 171 return fmt.Errorf("get resource type: %w", err) 172 } 173 174 if found { 175 err := resourceType.SetResourceConfigScope(scope) 176 if err != nil { 177 return fmt.Errorf("set resource type scope: %w", err) 178 } 179 } 180 181 return nil 182 } 183 184 func (d *checkDelegate) pipeline() (db.Pipeline, error) { 185 if d.cachedPipeline != nil { 186 return d.cachedPipeline, nil 187 } 188 189 pipeline, found, err := d.build.Pipeline() 190 if err != nil { 191 return nil, fmt.Errorf("get build pipeline: %w", err) 192 } 193 194 if !found { 195 return nil, fmt.Errorf("pipeline not found") 196 } 197 198 d.cachedPipeline = pipeline 199 200 return d.cachedPipeline, nil 201 } 202 203 func (d *checkDelegate) resource() (db.Resource, bool, error) { 204 if d.plan.Resource == "" { 205 return nil, false, nil 206 } 207 208 if d.cachedResource != nil { 209 return d.cachedResource, true, nil 210 } 211 212 pipeline, err := d.pipeline() 213 if err != nil { 214 return nil, false, err 215 } 216 217 resource, found, err := pipeline.Resource(d.plan.Resource) 218 if err != nil { 219 return nil, false, fmt.Errorf("get pipeline resource: %w", err) 220 } 221 222 if !found { 223 return nil, false, fmt.Errorf("resource '%s' deleted", d.plan.Resource) 224 } 225 226 d.cachedResource = resource 227 228 return d.cachedResource, true, nil 229 } 230 231 func (d *checkDelegate) resourceType() (db.ResourceType, bool, error) { 232 if d.plan.ResourceType == "" { 233 return nil, false, nil 234 } 235 236 if d.cachedResourceType != nil { 237 return d.cachedResourceType, true, nil 238 } 239 240 pipeline, err := d.pipeline() 241 if err != nil { 242 return nil, false, err 243 } 244 245 resourceType, found, err := pipeline.ResourceType(d.plan.ResourceType) 246 if err != nil { 247 return nil, false, fmt.Errorf("get pipeline resource type: %w", err) 248 } 249 250 if !found { 251 return nil, false, fmt.Errorf("resource type '%s' deleted", d.plan.ResourceType) 252 } 253 254 d.cachedResourceType = resourceType 255 256 return d.cachedResourceType, true, nil 257 }