| 209 | } |
| 210 | |
| 211 | func (r *Runner) reportTaskStatus(ctx context.Context) error { |
| 212 | defer func() { |
| 213 | r.mu.Lock() |
| 214 | defer r.mu.Unlock() |
| 215 | r.doneReporting = true |
| 216 | }() |
| 217 | |
| 218 | select { |
| 219 | case <-ctx.Done(): |
| 220 | return ctx.Err() |
| 221 | case <-r.cfg.StartReporting: |
| 222 | r.logger.Info(ctx, "starting to report task status") |
| 223 | } |
| 224 | startedReporting := r.clock.Now("reportTaskStatus", "startedReporting") |
| 225 | msgNo := 0 |
| 226 | |
| 227 | getRandPeriod := func() time.Duration { |
| 228 | // vary the period by +-50% so that updates are not synchronized across runners, which would create |
| 229 | // artificially large instantaneous stress on Coder and the database. |
| 230 | p := (r.randFloat64() + 0.5) * r.cfg.ReportStatusPeriod.Seconds() |
| 231 | return time.Duration(p * float64(time.Second)) |
| 232 | } |
| 233 | tmr := r.clock.NewTimer(getRandPeriod(), "reportTaskStatus") |
| 234 | for { |
| 235 | select { |
| 236 | case <-ctx.Done(): |
| 237 | return ctx.Err() |
| 238 | case <-tmr.C: |
| 239 | tmr.Reset(getRandPeriod(), "reportTaskStatus", "tick") |
| 240 | } |
| 241 | r.mu.Lock() |
| 242 | now := r.clock.Now("reportTaskStatus", "tick") |
| 243 | r.reportTimes[msgNo] = now |
| 244 | // It's important that we set doneReporting along with a final report, since the watchWorkspaceUpdates goroutine |
| 245 | // needs an update to wake up and check if we're done. We could introduce a secondary signaling channel, but |
| 246 | // it adds a lot of complexity and will be hard to test. We expect the tick period to be much smaller than the |
| 247 | // report status duration, so one extra tick is not a big deal. |
| 248 | if now.After(startedReporting.Add(r.cfg.ReportStatusDuration)) { |
| 249 | r.doneReporting = true |
| 250 | } |
| 251 | r.mu.Unlock() |
| 252 | |
| 253 | err := r.updater.updateAppStatus(ctx, &agentproto.UpdateAppStatusRequest{ |
| 254 | Slug: r.cfg.AppSlug, |
| 255 | Message: statusUpdatePrefix + strconv.Itoa(msgNo), |
| 256 | State: agentproto.UpdateAppStatusRequest_WORKING, |
| 257 | Uri: "https://example.com/example-status/", |
| 258 | }) |
| 259 | if err != nil { |
| 260 | r.logger.Error(ctx, "failed to report task status", slog.Error(err)) |
| 261 | r.cfg.Metrics.ReportTaskStatusErrorsTotal.WithLabelValues(r.cfg.MetricLabelValues...).Inc() |
| 262 | } |
| 263 | msgNo++ |
| 264 | // note that it's safe to read r.doneReporting here without a lock because we're the only goroutine that sets |
| 265 | // it. |
| 266 | if r.doneReporting { |
| 267 | return nil |
| 268 | } |