golang.org/x/build@v0.0.0-20240506185731-218518f32b70/internal/relui/listener.go (about) 1 // Copyright 2021 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package relui 6 7 import ( 8 "bytes" 9 "context" 10 "database/sql" 11 "encoding/json" 12 "fmt" 13 "html/template" 14 "log" 15 "net/url" 16 "time" 17 18 "github.com/google/uuid" 19 "github.com/jackc/pgx/v4" 20 "golang.org/x/build/internal/relui/db" 21 "golang.org/x/build/internal/task" 22 "golang.org/x/build/internal/workflow" 23 ) 24 25 // PGListener implements workflow.Listener for recording workflow state. 26 type PGListener struct { 27 DB db.PGDBTX 28 29 BaseURL *url.URL 30 31 ScheduleFailureMailHeader task.MailHeader 32 SendMail func(task.MailHeader, task.MailContent) error 33 34 templ *template.Template 35 } 36 37 // WorkflowStalled is called when no tasks are runnable. 38 func (l *PGListener) WorkflowStalled(workflowID uuid.UUID) error { 39 wf, err := db.New(l.DB).Workflow(context.Background(), workflowID) 40 if err != nil || wf.ScheduleID.Int32 == 0 { 41 return err 42 } 43 var buf bytes.Buffer 44 body := scheduledFailureEmailBody{Workflow: wf} 45 if err := l.template("scheduled_workflow_failure_email.txt").Execute(&buf, body); err != nil { 46 log.Printf("WorkflowFinished: Execute(_, %v) = %q", body, err) 47 return err 48 } 49 return l.SendMail(l.ScheduleFailureMailHeader, task.MailContent{ 50 Subject: fmt.Sprintf("[relui] Scheduled workflow %q failed", wf.Name.String), 51 BodyText: buf.String(), 52 }) 53 } 54 55 // TaskStateChanged is called whenever a task is updated by the 56 // workflow. The workflow.TaskState is persisted as a db.Task, 57 // creating or updating a row as necessary. 58 func (l *PGListener) TaskStateChanged(workflowID uuid.UUID, taskName string, state *workflow.TaskState) error { 59 log.Printf("TaskStateChanged(%q, %q, %#v)", workflowID, taskName, state) 60 ctx, cancel := context.WithCancel(context.Background()) 61 defer cancel() 62 result, err := json.Marshal(state.Result) 63 if err != nil { 64 return err 65 } 66 err = l.DB.BeginFunc(ctx, func(tx pgx.Tx) error { 67 q := db.New(tx) 68 updated := time.Now() 69 _, err := q.UpsertTask(ctx, db.UpsertTaskParams{ 70 WorkflowID: workflowID, 71 Name: taskName, 72 Started: state.Started, 73 Finished: state.Finished, 74 Result: sql.NullString{String: string(result), Valid: len(result) > 0}, 75 Error: sql.NullString{String: state.Error, Valid: state.Error != ""}, 76 CreatedAt: updated, 77 UpdatedAt: updated, 78 RetryCount: int32(state.RetryCount), 79 }) 80 return err 81 }) 82 if err != nil { 83 log.Printf("TaskStateChanged(%q, %q, %#v) = %v", workflowID, taskName, state, err) 84 } 85 return err 86 } 87 88 // WorkflowStarted persists a new workflow execution in the database. 89 func (l *PGListener) WorkflowStarted(ctx context.Context, workflowID uuid.UUID, name string, params map[string]interface{}, scheduleID int) error { 90 q := db.New(l.DB) 91 m, err := json.Marshal(params) 92 if err != nil { 93 return err 94 } 95 updated := time.Now() 96 wfp := db.CreateWorkflowParams{ 97 ID: workflowID, 98 Name: sql.NullString{String: name, Valid: true}, 99 Params: sql.NullString{String: string(m), Valid: len(m) > 0}, 100 ScheduleID: sql.NullInt32{Int32: int32(scheduleID), Valid: scheduleID != 0}, 101 CreatedAt: updated, 102 UpdatedAt: updated, 103 } 104 _, err = q.CreateWorkflow(ctx, wfp) 105 return err 106 } 107 108 type scheduledFailureEmailBody struct { 109 Workflow db.Workflow 110 Err error 111 } 112 113 // WorkflowFinished saves the final state of a workflow after its run 114 // has completed. 115 func (l *PGListener) WorkflowFinished(ctx context.Context, workflowID uuid.UUID, outputs map[string]interface{}, workflowErr error) error { 116 log.Printf("WorkflowFinished(%q, %v, %q)", workflowID, outputs, workflowErr) 117 q := db.New(l.DB) 118 m, err := json.Marshal(outputs) 119 if err != nil { 120 return err 121 } 122 wp := db.WorkflowFinishedParams{ 123 ID: workflowID, 124 Finished: true, 125 Output: string(m), 126 UpdatedAt: time.Now(), 127 } 128 if workflowErr != nil { 129 wp.Error = workflowErr.Error() 130 } 131 _, err = q.WorkflowFinished(ctx, wp) 132 return err 133 } 134 135 func (l *PGListener) template(name string) *template.Template { 136 if l.templ == nil { 137 helpers := map[string]any{"baseLink": l.baseLink} 138 l.templ = template.Must(template.New("").Funcs(helpers).ParseFS(templates, "templates/*.txt")) 139 } 140 return l.templ.Lookup(name) 141 } 142 143 func (l *PGListener) baseLink(target string, extras ...string) string { 144 return BaseLink(l.BaseURL)(target, extras...) 145 } 146 147 func (l *PGListener) Logger(workflowID uuid.UUID, taskName string) workflow.Logger { 148 return &postgresLogger{ 149 db: l.DB, 150 workflowID: workflowID, 151 taskName: taskName, 152 } 153 } 154 155 // postgresLogger logs task output to the database. It implements workflow.Logger. 156 type postgresLogger struct { 157 db db.PGDBTX 158 workflowID uuid.UUID 159 taskName string 160 } 161 162 func (l *postgresLogger) Printf(format string, v ...interface{}) { 163 ctx := context.Background() 164 err := l.db.BeginFunc(ctx, func(tx pgx.Tx) error { 165 q := db.New(tx) 166 body := fmt.Sprintf(format, v...) 167 _, err := q.CreateTaskLog(ctx, db.CreateTaskLogParams{ 168 WorkflowID: l.workflowID, 169 TaskName: l.taskName, 170 Body: body, 171 }) 172 if err != nil { 173 log.Printf("q.CreateTaskLog(%v, %v, %q) = %v", l.workflowID, l.taskName, body, err) 174 } 175 return err 176 }) 177 if err != nil { 178 log.Printf("l.Printf(%q, %v) = %v", format, v, err) 179 } 180 } 181 182 func LogOnlyMailer(header task.MailHeader, content task.MailContent) error { 183 log.Println("Logging but not sending mail:", header, content) 184 return nil 185 }