| 108 | } |
| 109 | |
| 110 | func reportFailedWorkspaceBuilds(ctx context.Context, logger slog.Logger, db database.Store, enqueuer notifications.Enqueuer, clk quartz.Clock) error { |
| 111 | now := clk.Now() |
| 112 | since := now.Add(-failedWorkspaceBuildsReportFrequency) |
| 113 | |
| 114 | // Firstly, check if this is the first run of the job ever |
| 115 | reportLog, err := db.GetNotificationReportGeneratorLogByTemplate(ctx, notifications.TemplateWorkspaceBuildsFailedReport) |
| 116 | if err != nil && !xerrors.Is(err, sql.ErrNoRows) { |
| 117 | return xerrors.Errorf("unable to read report generator log: %w", err) |
| 118 | } |
| 119 | if xerrors.Is(err, sql.ErrNoRows) { |
| 120 | // First run? Check-in the job, and get back after one week. |
| 121 | logger.Info(ctx, "report generator is executing the job for the first time", slog.F("notification_template_id", notifications.TemplateWorkspaceBuildsFailedReport)) |
| 122 | |
| 123 | err = db.UpsertNotificationReportGeneratorLog(ctx, database.UpsertNotificationReportGeneratorLogParams{ |
| 124 | NotificationTemplateID: notifications.TemplateWorkspaceBuildsFailedReport, |
| 125 | LastGeneratedAt: dbtime.Time(now).UTC(), |
| 126 | }) |
| 127 | if err != nil { |
| 128 | return xerrors.Errorf("unable to update report generator logs (first time execution): %w", err) |
| 129 | } |
| 130 | return nil |
| 131 | } |
| 132 | |
| 133 | // Secondly, check if the job has not been running recently |
| 134 | if !reportLog.LastGeneratedAt.IsZero() && reportLog.LastGeneratedAt.Add(failedWorkspaceBuildsReportFrequency).After(now) { |
| 135 | return nil // reports sent recently, no need to send them now |
| 136 | } |
| 137 | |
| 138 | // Thirdly, fetch workspace build stats by templates |
| 139 | templateStatsRows, err := db.GetWorkspaceBuildStatsByTemplates(ctx, dbtime.Time(since).UTC()) |
| 140 | if err != nil { |
| 141 | return xerrors.Errorf("unable to fetch failed workspace builds: %w", err) |
| 142 | } |
| 143 | |
| 144 | reports := make(map[uuid.UUID][]adminReport) |
| 145 | |
| 146 | for _, stats := range templateStatsRows { |
| 147 | select { |
| 148 | case <-ctx.Done(): |
| 149 | logger.Debug(ctx, "context is canceled, quitting", slog.Error(ctx.Err())) |
| 150 | break |
| 151 | default: |
| 152 | } |
| 153 | |
| 154 | if stats.FailedBuilds == 0 { |
| 155 | logger.Info(ctx, "no failed workspace builds found for template", slog.F("template_id", stats.TemplateID), slog.Error(err)) |
| 156 | continue |
| 157 | } |
| 158 | |
| 159 | // Fetch template admins with org access to the templates |
| 160 | templateAdmins, err := findTemplateAdmins(ctx, db, stats) |
| 161 | if err != nil { |
| 162 | logger.Error(ctx, "unable to find template admins for template", slog.F("template_id", stats.TemplateID), slog.Error(err)) |
| 163 | continue |
| 164 | } |
| 165 | |
| 166 | // Fetch failed builds by the template |
| 167 | failedBuilds, err := db.GetFailedWorkspaceBuildsByTemplateID(ctx, database.GetFailedWorkspaceBuildsByTemplateIDParams{ |