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 }