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  }