golang.org/x/build@v0.0.0-20240506185731-218518f32b70/internal/relui/worker_test.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  	"context"
     9  	"database/sql"
    10  	"errors"
    11  	"fmt"
    12  	"strings"
    13  	"sync"
    14  	"testing"
    15  	"time"
    16  
    17  	"github.com/google/go-cmp/cmp"
    18  	"github.com/google/go-cmp/cmp/cmpopts"
    19  	"github.com/google/uuid"
    20  	"golang.org/x/build/internal/relui/db"
    21  	"golang.org/x/build/internal/workflow"
    22  )
    23  
    24  func TestWorkerStartWorkflow(t *testing.T) {
    25  	ctx, cancel := context.WithCancel(context.Background())
    26  	defer cancel()
    27  	dbp := testDB(ctx, t)
    28  	q := db.New(dbp)
    29  	wg := sync.WaitGroup{}
    30  	dh := NewDefinitionHolder()
    31  	w := NewWorker(dh, dbp, &testWorkflowListener{
    32  		Listener:   &PGListener{DB: dbp},
    33  		onFinished: wg.Done,
    34  	})
    35  
    36  	wd := newTestEchoWorkflow()
    37  	dh.RegisterDefinition(t.Name(), wd)
    38  	params := map[string]interface{}{"greeting": "greetings", "names": []string{"alice", "bob"}}
    39  
    40  	wg.Add(1)
    41  	wfid, err := w.StartWorkflow(ctx, t.Name(), params, 0)
    42  	if err != nil {
    43  		t.Fatalf("w.StartWorkflow(_, %v, %v) = %v, %v, wanted no error", wd, params, wfid, err)
    44  	}
    45  	go w.Run(ctx)
    46  	wg.Wait()
    47  
    48  	wfs, err := q.Workflows(ctx)
    49  	if err != nil {
    50  		t.Fatalf("q.Workflows() = %v, %v, wanted no error", wfs, err)
    51  	}
    52  	wantWfs := []db.Workflow{{
    53  		ID: wfid,
    54  		// Params ignored: nondeterministic serialization
    55  		Name:      nullString(t.Name()),
    56  		Output:    `{"echo": "greetings alice bob"}`,
    57  		Finished:  true,
    58  		CreatedAt: time.Now(), // cmpopts.EquateApproxTime
    59  		UpdatedAt: time.Now(), // cmpopts.EquateApproxTime
    60  	}}
    61  	if diff := cmp.Diff(wantWfs, wfs, cmpopts.EquateApproxTime(time.Minute), cmpopts.IgnoreFields(db.Workflow{}, "Params")); diff != "" {
    62  		t.Fatalf("q.Workflows() mismatch (-want +got):\n%s", diff)
    63  	}
    64  	tasks, err := q.TasksForWorkflow(ctx, wfid)
    65  	if err != nil {
    66  		t.Fatalf("q.TasksForWorkflow(_, %v) = %v, %v, wanted no error", wfid, tasks, err)
    67  	}
    68  	want := []db.Task{
    69  		{
    70  			WorkflowID: wfid,
    71  			Name:       "echo",
    72  			Started:    true,
    73  			Finished:   true,
    74  			Result:     nullString(`"greetings alice bob"`),
    75  			Error:      sql.NullString{},
    76  			CreatedAt:  time.Now(), // cmpopts.EquateApproxTime
    77  			UpdatedAt:  time.Now(), // cmpopts.EquateApproxTime
    78  		},
    79  	}
    80  	if diff := cmp.Diff(want, tasks, cmpopts.EquateApproxTime(time.Minute)); diff != "" {
    81  		t.Errorf("q.TasksForWorkflow(_, %q) mismatch (-want +got):\n%s", wfid, diff)
    82  	}
    83  }
    84  
    85  func TestWorkerResume(t *testing.T) {
    86  	ctx, cancel := context.WithCancel(context.Background())
    87  	defer cancel()
    88  	dbp := testDB(ctx, t)
    89  	q := db.New(dbp)
    90  	wg := sync.WaitGroup{}
    91  	dh := NewDefinitionHolder()
    92  	w := NewWorker(dh, dbp, &testWorkflowListener{
    93  		Listener:   &PGListener{DB: dbp},
    94  		onFinished: wg.Done,
    95  	})
    96  
    97  	wd := newTestEchoWorkflow()
    98  	dh.RegisterDefinition(t.Name(), wd)
    99  	wfid := createUnfinishedEchoWorkflow(t, ctx, q)
   100  
   101  	wg.Add(1)
   102  	go w.Run(ctx)
   103  	if err := w.Resume(ctx, wfid); err != nil {
   104  		t.Fatalf("w.Resume(_, %v) = %v, wanted no error", wfid, err)
   105  	}
   106  	wg.Wait()
   107  
   108  	tasks, err := q.TasksForWorkflow(ctx, wfid)
   109  	if err != nil {
   110  		t.Fatalf("q.TasksForWorkflow(_, %v) = %v, %v, wanted no error", wfid, tasks, err)
   111  	}
   112  	want := []db.Task{{
   113  		WorkflowID: wfid,
   114  		Name:       "echo",
   115  		Started:    true,
   116  		Finished:   true,
   117  		Result:     nullString(`"hello alice bob"`),
   118  		Error:      sql.NullString{},
   119  		CreatedAt:  time.Now(), // cmpopts.EquateApproxTime
   120  		UpdatedAt:  time.Now(), // cmpopts.EquateApproxTime
   121  	}}
   122  	if diff := cmp.Diff(want, tasks, cmpopts.EquateApproxTime(time.Minute)); diff != "" {
   123  		t.Errorf("q.TasksForWorkflow(_, %q) mismatch (-want +got):\n%s", wfid, diff)
   124  	}
   125  }
   126  
   127  func TestWorkerResumeMissingDefinition(t *testing.T) {
   128  	ctx, cancel := context.WithCancel(context.Background())
   129  	defer cancel()
   130  	dbp := testDB(ctx, t)
   131  	q := db.New(dbp)
   132  	w := NewWorker(NewDefinitionHolder(), dbp, &PGListener{DB: dbp})
   133  
   134  	cwp := db.CreateWorkflowParams{ID: uuid.New(), Name: nullString(t.Name()), Params: nullString("{}")}
   135  	if wf, err := q.CreateWorkflow(ctx, cwp); err != nil {
   136  		t.Fatalf("q.CreateWorkflow(_, %v) = %v, %v, wanted no error", cwp, wf, err)
   137  	}
   138  
   139  	if err := w.Resume(ctx, cwp.ID); err == nil {
   140  		t.Fatalf("w.Resume(_, %q) = %v, wanted error", cwp.ID, err)
   141  	}
   142  }
   143  
   144  func TestWorkflowResumeAll(t *testing.T) {
   145  	ctx, cancel := context.WithCancel(context.Background())
   146  	defer cancel()
   147  	dbp := testDB(ctx, t)
   148  	q := db.New(dbp)
   149  	wg := sync.WaitGroup{}
   150  	dh := NewDefinitionHolder()
   151  	w := NewWorker(dh, dbp, &testWorkflowListener{
   152  		Listener:   &PGListener{DB: dbp},
   153  		onFinished: wg.Done,
   154  	})
   155  
   156  	wd := newTestEchoWorkflow()
   157  	dh.RegisterDefinition(t.Name(), wd)
   158  	wfid1 := createUnfinishedEchoWorkflow(t, ctx, q)
   159  	wfid2 := createUnfinishedEchoWorkflow(t, ctx, q)
   160  
   161  	wg.Add(2)
   162  	go w.Run(ctx)
   163  	if err := w.ResumeAll(ctx); err != nil {
   164  		t.Fatalf("w.ResumeAll() = %v, wanted no error", err)
   165  	}
   166  	wg.Wait()
   167  
   168  	tasks, err := q.Tasks(ctx)
   169  	if err != nil {
   170  		t.Fatalf("q.Tasks() = %v, %v, wanted no error", tasks, err)
   171  	}
   172  	want := []db.TasksRow{
   173  		{
   174  			WorkflowID:       wfid1,
   175  			Name:             "echo",
   176  			Started:          true,
   177  			Finished:         true,
   178  			Result:           nullString(`"hello alice bob"`),
   179  			Error:            sql.NullString{},
   180  			CreatedAt:        time.Now(), // cmpopts.EquateApproxTime
   181  			UpdatedAt:        time.Now(), // cmpopts.EquateApproxTime
   182  			MostRecentUpdate: time.Now(),
   183  		},
   184  		{
   185  			WorkflowID:       wfid2,
   186  			Name:             "echo",
   187  			Started:          true,
   188  			Finished:         true,
   189  			Result:           nullString(`"hello alice bob"`),
   190  			Error:            sql.NullString{},
   191  			CreatedAt:        time.Now(), // cmpopts.EquateApproxTime
   192  			UpdatedAt:        time.Now(), // cmpopts.EquateApproxTime
   193  			MostRecentUpdate: time.Now(),
   194  		},
   195  	}
   196  	sort := cmpopts.SortSlices(func(x, y db.TasksRow) bool {
   197  		return x.WorkflowID.String() < y.WorkflowID.String()
   198  	})
   199  	if diff := cmp.Diff(want, tasks, cmpopts.EquateApproxTime(time.Minute), sort); diff != "" {
   200  		t.Errorf("q.Tasks() mismatch (-want +got):\n%s", diff)
   201  	}
   202  }
   203  
   204  func TestWorkflowResumeRetry(t *testing.T) {
   205  	ctx, cancel := context.WithCancel(context.Background())
   206  	defer cancel()
   207  	dbp := testDB(ctx, t)
   208  	dh := NewDefinitionHolder()
   209  	w := NewWorker(dh, dbp, &PGListener{DB: dbp})
   210  
   211  	counter := 0
   212  	blockingChan := make(chan bool)
   213  	wd := workflow.New()
   214  	nothing := workflow.Task0(wd, "needs retry", func(ctx context.Context) (string, error) {
   215  		// Send twice so that the test can stop us mid-execution.
   216  		for i := 0; i < 2; i++ {
   217  			select {
   218  			case <-ctx.Done():
   219  				return "", ctx.Err()
   220  			case blockingChan <- true:
   221  				counter++
   222  			}
   223  		}
   224  		if counter > 4 {
   225  			return "", nil
   226  		}
   227  		return "", errors.New("expected")
   228  	})
   229  	workflow.Output(wd, "nothing", nothing)
   230  	dh.RegisterDefinition(t.Name(), wd)
   231  
   232  	// Run the workflow. It will try the task up to 3 times. Stop the worker
   233  	// during its second run, then resume it and verify the task retries.
   234  	go func() {
   235  		for i := 0; i < 3; i++ {
   236  			<-blockingChan
   237  		}
   238  		cancel()
   239  	}()
   240  	wfid, err := w.StartWorkflow(ctx, t.Name(), nil, 0)
   241  	if err != nil {
   242  		t.Fatalf("w.StartWorkflow(_, %v, %v) = %v, %v, wanted no error", wd, nil, wfid, err)
   243  	}
   244  	w.Run(ctx)
   245  	if counter != 3 {
   246  		t.Fatalf("task sent %v times, wanted 3", counter)
   247  	}
   248  
   249  	t.Log("Restarting worker")
   250  	ctx, cancel = context.WithCancel(context.Background())
   251  	defer cancel()
   252  	wfDone := make(chan bool, 1)
   253  	w = NewWorker(dh, dbp, &testWorkflowListener{
   254  		Listener:   &PGListener{DB: dbp},
   255  		onFinished: func() { wfDone <- true },
   256  	})
   257  
   258  	go func() {
   259  		for {
   260  			select {
   261  			case <-blockingChan:
   262  			case <-ctx.Done():
   263  				return
   264  			}
   265  		}
   266  	}()
   267  	go w.Run(ctx)
   268  	if err := w.Resume(ctx, wfid); err != nil {
   269  		t.Fatalf("w.Resume(_, %v) = %v, wanted no error", wfid, err)
   270  	}
   271  	<-wfDone
   272  }
   273  
   274  func newTestEchoWorkflow() *workflow.Definition {
   275  	wd := workflow.New()
   276  	echo := func(ctx context.Context, greeting string, names []string) (string, error) {
   277  		return fmt.Sprintf("%v %v", greeting, strings.Join(names, " ")), nil
   278  	}
   279  	greeting := workflow.Param(wd, workflow.ParamDef[string]{Name: "greeting"})
   280  	names := workflow.Param(wd, workflow.ParamDef[[]string]{Name: "names", ParamType: workflow.SliceShort})
   281  	workflow.Output(wd, "echo", workflow.Task2(wd, "echo", echo, greeting, names))
   282  	return wd
   283  }
   284  
   285  func createUnfinishedEchoWorkflow(t *testing.T, ctx context.Context, q *db.Queries) uuid.UUID {
   286  	t.Helper()
   287  	cwp := db.CreateWorkflowParams{ID: uuid.New(), Name: nullString(t.Name()), Params: nullString(`{"greeting": "hello", "names": ["alice", "bob"]}`)}
   288  	if wf, err := q.CreateWorkflow(ctx, cwp); err != nil {
   289  		t.Fatalf("q.CreateWorkflow(_, %v) = %v, %v, wanted no error", cwp, wf, err)
   290  	}
   291  	cwt := db.CreateTaskParams{WorkflowID: cwp.ID, Name: "echo", Result: nullString("null"), CreatedAt: time.Now()}
   292  	if wt, err := q.CreateTask(ctx, cwt); err != nil {
   293  		t.Fatalf("q.CreateWorkflowTask(_, %v) = %v, %v, wanted no error", cwt, wt, err)
   294  	}
   295  	return cwp.ID
   296  }
   297  
   298  type testWorkflowListener struct {
   299  	Listener
   300  
   301  	onFinished func()
   302  	onStalled  func()
   303  }
   304  
   305  func (t *testWorkflowListener) WorkflowFinished(ctx context.Context, wfid uuid.UUID, outputs map[string]interface{}, wferr error) error {
   306  	err := t.Listener.WorkflowFinished(ctx, wfid, outputs, wferr)
   307  	if t.onFinished != nil {
   308  		t.onFinished()
   309  	}
   310  	return err
   311  }
   312  
   313  func (t *testWorkflowListener) WorkflowStalled(workflowID uuid.UUID) error {
   314  	err := t.Listener.WorkflowStalled(workflowID)
   315  	if t.onStalled != nil {
   316  		t.onStalled()
   317  	}
   318  	return err
   319  }