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 }