github.com/nilium/gitlab-runner@v12.5.0+incompatible/commands/builds_helper.go (about)

     1  package commands
     2  
     3  import (
     4  	"fmt"
     5  	"net/http"
     6  	"regexp"
     7  	"strings"
     8  	"sync"
     9  
    10  	"gitlab.com/gitlab-org/gitlab-runner/common"
    11  	"gitlab.com/gitlab-org/gitlab-runner/helpers"
    12  	"gitlab.com/gitlab-org/gitlab-runner/session"
    13  
    14  	"github.com/prometheus/client_golang/prometheus"
    15  )
    16  
    17  var numBuildsDesc = prometheus.NewDesc(
    18  	"gitlab_runner_jobs",
    19  	"The current number of running builds.",
    20  	[]string{"runner", "state", "stage", "executor_stage"},
    21  	nil,
    22  )
    23  
    24  var requestConcurrencyDesc = prometheus.NewDesc(
    25  	"gitlab_runner_request_concurrency",
    26  	"The current number of concurrent requests for a new job",
    27  	[]string{"runner"},
    28  	nil,
    29  )
    30  
    31  var requestConcurrencyExceededDesc = prometheus.NewDesc(
    32  	"gitlab_runner_request_concurrency_exceeded_total",
    33  	"Counter tracking exceeding of request concurrency",
    34  	[]string{"runner"},
    35  	nil,
    36  )
    37  
    38  type statePermutation struct {
    39  	runner        string
    40  	buildState    common.BuildRuntimeState
    41  	buildStage    common.BuildStage
    42  	executorStage common.ExecutorStage
    43  }
    44  
    45  func newStatePermutationFromBuild(build *common.Build) statePermutation {
    46  	return statePermutation{
    47  		runner:        build.Runner.ShortDescription(),
    48  		buildState:    build.CurrentState,
    49  		buildStage:    build.CurrentStage,
    50  		executorStage: build.CurrentExecutorStage(),
    51  	}
    52  }
    53  
    54  type runnerCounter struct {
    55  	builds   int
    56  	requests int
    57  
    58  	requestConcurrencyExceeded int
    59  }
    60  
    61  type buildsHelper struct {
    62  	counters map[string]*runnerCounter
    63  	builds   []*common.Build
    64  	lock     sync.Mutex
    65  
    66  	jobsTotal            *prometheus.CounterVec
    67  	jobDurationHistogram *prometheus.HistogramVec
    68  }
    69  
    70  func (b *buildsHelper) getRunnerCounter(runner *common.RunnerConfig) *runnerCounter {
    71  	if b.counters == nil {
    72  		b.counters = make(map[string]*runnerCounter)
    73  	}
    74  
    75  	counter, _ := b.counters[runner.Token]
    76  	if counter == nil {
    77  		counter = &runnerCounter{}
    78  		b.counters[runner.Token] = counter
    79  	}
    80  	return counter
    81  }
    82  
    83  func (b *buildsHelper) findSessionByURL(url string) *session.Session {
    84  	b.lock.Lock()
    85  	defer b.lock.Unlock()
    86  
    87  	for _, build := range b.builds {
    88  		if strings.HasPrefix(url, build.Session.Endpoint+"/") {
    89  			return build.Session
    90  		}
    91  	}
    92  
    93  	return nil
    94  }
    95  
    96  func (b *buildsHelper) acquireBuild(runner *common.RunnerConfig) bool {
    97  	b.lock.Lock()
    98  	defer b.lock.Unlock()
    99  
   100  	counter := b.getRunnerCounter(runner)
   101  
   102  	if runner.Limit > 0 && counter.builds >= runner.Limit {
   103  		// Too many builds
   104  		return false
   105  	}
   106  
   107  	counter.builds++
   108  	return true
   109  }
   110  
   111  func (b *buildsHelper) releaseBuild(runner *common.RunnerConfig) bool {
   112  	b.lock.Lock()
   113  	defer b.lock.Unlock()
   114  
   115  	counter := b.getRunnerCounter(runner)
   116  	if counter.builds > 0 {
   117  		counter.builds--
   118  		return true
   119  	}
   120  	return false
   121  }
   122  
   123  func (b *buildsHelper) acquireRequest(runner *common.RunnerConfig) bool {
   124  	b.lock.Lock()
   125  	defer b.lock.Unlock()
   126  
   127  	counter := b.getRunnerCounter(runner)
   128  
   129  	if counter.requests >= runner.GetRequestConcurrency() {
   130  		counter.requestConcurrencyExceeded++
   131  
   132  		return false
   133  	}
   134  
   135  	counter.requests++
   136  	return true
   137  }
   138  
   139  func (b *buildsHelper) releaseRequest(runner *common.RunnerConfig) bool {
   140  	b.lock.Lock()
   141  	defer b.lock.Unlock()
   142  
   143  	counter := b.getRunnerCounter(runner)
   144  	if counter.requests > 0 {
   145  		counter.requests--
   146  		return true
   147  	}
   148  	return false
   149  }
   150  
   151  func (b *buildsHelper) addBuild(build *common.Build) {
   152  	if build == nil {
   153  		return
   154  	}
   155  
   156  	b.lock.Lock()
   157  	defer b.lock.Unlock()
   158  
   159  	runners := make(map[int]bool)
   160  	projectRunners := make(map[int]bool)
   161  
   162  	for _, otherBuild := range b.builds {
   163  		if otherBuild.Runner.Token != build.Runner.Token {
   164  			continue
   165  		}
   166  		runners[otherBuild.RunnerID] = true
   167  
   168  		if otherBuild.JobInfo.ProjectID != build.JobInfo.ProjectID {
   169  			continue
   170  		}
   171  		projectRunners[otherBuild.ProjectRunnerID] = true
   172  	}
   173  
   174  	for {
   175  		if !runners[build.RunnerID] {
   176  			break
   177  		}
   178  		build.RunnerID++
   179  	}
   180  
   181  	for {
   182  		if !projectRunners[build.ProjectRunnerID] {
   183  			break
   184  		}
   185  		build.ProjectRunnerID++
   186  	}
   187  
   188  	b.builds = append(b.builds, build)
   189  	b.jobsTotal.WithLabelValues(build.Runner.ShortDescription()).Inc()
   190  
   191  	return
   192  }
   193  
   194  func (b *buildsHelper) removeBuild(deleteBuild *common.Build) bool {
   195  	b.lock.Lock()
   196  	defer b.lock.Unlock()
   197  
   198  	b.jobDurationHistogram.WithLabelValues(deleteBuild.Runner.ShortDescription()).Observe(deleteBuild.Duration().Seconds())
   199  
   200  	for idx, build := range b.builds {
   201  		if build == deleteBuild {
   202  			b.builds = append(b.builds[0:idx], b.builds[idx+1:]...)
   203  
   204  			return true
   205  		}
   206  	}
   207  
   208  	return false
   209  }
   210  
   211  func (b *buildsHelper) buildsCount() int {
   212  	b.lock.Lock()
   213  	defer b.lock.Unlock()
   214  
   215  	return len(b.builds)
   216  }
   217  
   218  func (b *buildsHelper) statesAndStages() map[statePermutation]int {
   219  	b.lock.Lock()
   220  	defer b.lock.Unlock()
   221  
   222  	data := make(map[statePermutation]int)
   223  	for _, build := range b.builds {
   224  		state := newStatePermutationFromBuild(build)
   225  		if _, ok := data[state]; ok {
   226  			data[state]++
   227  		} else {
   228  			data[state] = 1
   229  		}
   230  	}
   231  	return data
   232  }
   233  
   234  func (b *buildsHelper) runnersCounters() map[string]*runnerCounter {
   235  	b.lock.Lock()
   236  	defer b.lock.Unlock()
   237  
   238  	data := make(map[string]*runnerCounter)
   239  	for token, counter := range b.counters {
   240  		data[helpers.ShortenToken(token)] = counter
   241  	}
   242  
   243  	return data
   244  }
   245  
   246  // Describe implements prometheus.Collector.
   247  func (b *buildsHelper) Describe(ch chan<- *prometheus.Desc) {
   248  	ch <- numBuildsDesc
   249  	ch <- requestConcurrencyDesc
   250  	ch <- requestConcurrencyExceededDesc
   251  
   252  	b.jobsTotal.Describe(ch)
   253  	b.jobDurationHistogram.Describe(ch)
   254  }
   255  
   256  // Collect implements prometheus.Collector.
   257  func (b *buildsHelper) Collect(ch chan<- prometheus.Metric) {
   258  	builds := b.statesAndStages()
   259  	for state, count := range builds {
   260  		ch <- prometheus.MustNewConstMetric(
   261  			numBuildsDesc,
   262  			prometheus.GaugeValue,
   263  			float64(count),
   264  			state.runner,
   265  			string(state.buildState),
   266  			string(state.buildStage),
   267  			string(state.executorStage),
   268  		)
   269  	}
   270  
   271  	counters := b.runnersCounters()
   272  	for runner, counter := range counters {
   273  		ch <- prometheus.MustNewConstMetric(
   274  			requestConcurrencyDesc,
   275  			prometheus.GaugeValue,
   276  			float64(counter.requests),
   277  			runner,
   278  		)
   279  
   280  		ch <- prometheus.MustNewConstMetric(
   281  			requestConcurrencyExceededDesc,
   282  			prometheus.CounterValue,
   283  			float64(counter.requestConcurrencyExceeded),
   284  			runner,
   285  		)
   286  	}
   287  
   288  	b.jobsTotal.Collect(ch)
   289  	b.jobDurationHistogram.Collect(ch)
   290  }
   291  
   292  func (b *buildsHelper) ListJobsHandler(w http.ResponseWriter, r *http.Request) {
   293  	version := r.URL.Query().Get("v")
   294  	if version == "" {
   295  		version = "1"
   296  	}
   297  
   298  	handlers := map[string]http.HandlerFunc{
   299  		"1": b.listJobsHandlerV1,
   300  		"2": b.listJobsHandlerV2,
   301  	}
   302  
   303  	handler, ok := handlers[version]
   304  	if !ok {
   305  		w.WriteHeader(http.StatusNotFound)
   306  		fmt.Fprintf(w, "Request version %q not supported", version)
   307  		return
   308  	}
   309  
   310  	w.Header().Add("X-List-Version", version)
   311  	w.Header().Add("Content-Type", "text/plain")
   312  	w.WriteHeader(http.StatusOK)
   313  
   314  	handler(w, r)
   315  }
   316  
   317  func (b *buildsHelper) listJobsHandlerV1(w http.ResponseWriter, r *http.Request) {
   318  	for _, job := range b.builds {
   319  		fmt.Fprintf(
   320  			w,
   321  			"id=%d url=%s state=%s stage=%s executor_stage=%s\n",
   322  			job.ID, job.RepoCleanURL(),
   323  			job.CurrentState, job.CurrentStage, job.CurrentExecutorStage(),
   324  		)
   325  	}
   326  
   327  }
   328  
   329  func (b *buildsHelper) listJobsHandlerV2(w http.ResponseWriter, r *http.Request) {
   330  	for _, job := range b.builds {
   331  		url := CreateJobURL(job.RepoCleanURL(), job.ID)
   332  
   333  		fmt.Fprintf(
   334  			w,
   335  			"url=%s state=%s stage=%s executor_stage=%s duration=%s\n",
   336  			url, job.CurrentState, job.CurrentStage, job.CurrentExecutorStage(), job.Duration(),
   337  		)
   338  	}
   339  }
   340  
   341  func CreateJobURL(projectURL string, jobID int) string {
   342  	r := regexp.MustCompile("(\\.git$)?")
   343  	URL := r.ReplaceAllString(projectURL, "")
   344  
   345  	return fmt.Sprintf("%s/-/jobs/%d", URL, jobID)
   346  }
   347  
   348  func newBuildsHelper() buildsHelper {
   349  	return buildsHelper{
   350  		jobsTotal: prometheus.NewCounterVec(
   351  			prometheus.CounterOpts{
   352  				Name: "gitlab_runner_jobs_total",
   353  				Help: "Total number of handled jobs",
   354  			},
   355  			[]string{"runner"},
   356  		),
   357  		jobDurationHistogram: prometheus.NewHistogramVec(
   358  			prometheus.HistogramOpts{
   359  				Name:    "gitlab_runner_job_duration_seconds",
   360  				Help:    "Histogram of job durations",
   361  				Buckets: []float64{30, 60, 300, 600, 1800, 3600, 7200, 10800, 18000, 36000},
   362  			},
   363  			[]string{"runner"},
   364  		),
   365  	}
   366  }