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  }