go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/appengine/tq/tq_test.go (about) 1 // Copyright 2017 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package tq 16 17 import ( 18 "bytes" 19 "context" 20 "fmt" 21 "net/http/httptest" 22 "sort" 23 "testing" 24 "time" 25 26 "github.com/golang/protobuf/proto" 27 "google.golang.org/protobuf/types/known/durationpb" 28 "google.golang.org/protobuf/types/known/emptypb" 29 30 "go.chromium.org/luci/gae/impl/memory" 31 "go.chromium.org/luci/gae/service/taskqueue" 32 33 "go.chromium.org/luci/common/clock" 34 "go.chromium.org/luci/common/clock/testclock" 35 "go.chromium.org/luci/common/errors" 36 "go.chromium.org/luci/common/retry/transient" 37 "go.chromium.org/luci/server/router" 38 39 . "github.com/smartystreets/goconvey/convey" 40 41 . "go.chromium.org/luci/common/testing/assertions" 42 ) 43 44 var epoch = time.Unix(1500000000, 0).UTC() 45 46 func TestDispatcher(t *testing.T) { 47 t.Parallel() 48 49 Convey("With dispatcher", t, func() { 50 ctx := memory.Use(context.Background()) 51 ctx = clock.Set(ctx, testclock.New(epoch)) 52 taskqueue.GetTestable(ctx).CreateQueue("another-q") 53 54 d := Dispatcher{} 55 r := router.New() 56 57 installRoutes := func() { 58 d.InstallRoutes(r, router.NewMiddlewareChain(func(c *router.Context, next router.Handler) { 59 c.Request = c.Request.WithContext(ctx) 60 next(c) 61 })) 62 } 63 runTasks := func(ctx context.Context) []int { 64 var codes []int 65 for _, tasks := range taskqueue.GetTestable(ctx).GetScheduledTasks() { 66 for _, task := range tasks { 67 // Execute the task. 68 req := httptest.NewRequest("POST", "http://example.com"+task.Path, bytes.NewReader(task.Payload)) 69 rw := httptest.NewRecorder() 70 r.ServeHTTP(rw, req) 71 codes = append(codes, rw.Code) 72 } 73 } 74 return codes 75 } 76 77 Convey("Single task", func() { 78 var calls []proto.Message 79 handler := func(ctx context.Context, payload proto.Message) error { 80 hdr, err := RequestHeaders(ctx) 81 So(err, ShouldBeNil) 82 So(hdr, ShouldResemble, &taskqueue.RequestHeaders{}) 83 calls = append(calls, payload) 84 return nil 85 } 86 87 // Abuse some well-known proto type to simplify the test. It's doesn't 88 // matter what proto type we use here as long as it is registered in 89 // protobuf type registry. 90 d.RegisterTask(&durationpb.Duration{}, handler, "", nil) 91 installRoutes() 92 93 err := d.AddTask(ctx, &Task{ 94 Payload: &durationpb.Duration{Seconds: 123}, 95 DeduplicationKey: "abc", 96 NamePrefix: "prefix", 97 Title: "abc-def", 98 Delay: 30 * time.Second, 99 }) 100 So(err, ShouldBeNil) 101 102 // Added the task. 103 expectedPath := "/internal/tasks/default/abc-def" 104 expectedName := "prefix-afc6f8271b8598ee04e359916e6c584a9bc3c520a11dd5244e3399346ac0d3a7" 105 expectedBody := []byte(`{"type":"google.protobuf.Duration","body":"123s"}`) 106 tasks := taskqueue.GetTestable(ctx).GetScheduledTasks() 107 So(tasks, ShouldResemble, taskqueue.QueueData{ 108 "default": map[string]*taskqueue.Task{ 109 expectedName: { 110 Path: expectedPath, 111 Payload: expectedBody, 112 Name: expectedName, 113 Method: "POST", 114 ETA: epoch.Add(30 * time.Second), 115 }, 116 }, 117 "another-q": {}, 118 }) 119 120 // Read a task with same dedup key. Should be silently ignored. 121 err = d.AddTask(ctx, &Task{ 122 Payload: &durationpb.Duration{Seconds: 123}, 123 DeduplicationKey: "abc", 124 NamePrefix: "prefix", 125 }) 126 So(err, ShouldBeNil) 127 128 // No new tasks. 129 tasks = taskqueue.GetTestable(ctx).GetScheduledTasks() 130 So(len(tasks["default"]), ShouldResemble, 1) 131 132 Convey("Executed", func() { 133 // Execute the task. 134 So(runTasks(ctx), ShouldResemble, []int{200}) 135 So(calls, ShouldResembleProto, []proto.Message{ 136 &durationpb.Duration{Seconds: 123}, 137 }) 138 }) 139 140 Convey("Deleted", func() { 141 So(d.DeleteTask(ctx, &Task{ 142 Payload: &durationpb.Duration{Seconds: 123}, 143 DeduplicationKey: "abc", 144 NamePrefix: "prefix", 145 }), ShouldBeNil) 146 147 // Did not execute any tasks. 148 So(runTasks(ctx), ShouldHaveLength, 0) 149 So(calls, ShouldHaveLength, 0) 150 }) 151 }) 152 153 Convey("Deleting unknown task returns nil", func() { 154 handler := func(ctx context.Context, payload proto.Message) error { return nil } 155 d.RegisterTask(&durationpb.Duration{}, handler, "default", nil) 156 157 So(d.DeleteTask(ctx, &Task{ 158 Payload: &durationpb.Duration{Seconds: 123}, 159 DeduplicationKey: "something", 160 }), ShouldBeNil) 161 }) 162 163 Convey("Many tasks", func() { 164 handler := func(ctx context.Context, payload proto.Message) error { return nil } 165 d.RegisterTask(&durationpb.Duration{}, handler, "default", nil) 166 d.RegisterTask(&emptypb.Empty{}, handler, "another-q", nil) 167 installRoutes() 168 169 t := []*Task{} 170 for i := 0; i < 200; i++ { 171 var task *Task 172 173 if i%2 == 0 { 174 task = &Task{ 175 DeduplicationKey: fmt.Sprintf("%d", i/2), 176 Payload: &durationpb.Duration{}, 177 Delay: time.Duration(i) * time.Second, 178 } 179 } else { 180 task = &Task{ 181 DeduplicationKey: fmt.Sprintf("%d", (i-1)/2), 182 Payload: &emptypb.Empty{}, 183 Delay: time.Duration(i) * time.Second, 184 } 185 } 186 187 t = append(t, task) 188 189 // Mix in some duplicates. 190 if i > 0 && i%100 == 0 { 191 t = append(t, task) 192 } 193 } 194 err := d.AddTask(ctx, t...) 195 So(err, ShouldBeNil) 196 197 // Added all the tasks. 198 allTasks := taskqueue.GetTestable(ctx).GetScheduledTasks() 199 delaysDefault := map[time.Duration]struct{}{} 200 for _, task := range allTasks["default"] { 201 delaysDefault[task.ETA.Sub(epoch)/time.Second] = struct{}{} 202 } 203 delaysAnotherQ := map[time.Duration]struct{}{} 204 for _, task := range allTasks["another-q"] { 205 delaysAnotherQ[task.ETA.Sub(epoch)/time.Second] = struct{}{} 206 } 207 So(len(delaysDefault), ShouldEqual, 100) 208 So(len(delaysAnotherQ), ShouldEqual, 100) 209 210 // Delete the tasks. 211 So(d.DeleteTask(ctx, t...), ShouldBeNil) 212 So(runTasks(ctx), ShouldHaveLength, 0) 213 }) 214 215 Convey("Execution errors", func() { 216 var returnErr error 217 panicNow := false 218 handler := func(ctx context.Context, payload proto.Message) error { 219 if panicNow { 220 panic("must not be called") 221 } 222 return returnErr 223 } 224 225 d.RegisterTask(&durationpb.Duration{}, handler, "", nil) 226 installRoutes() 227 228 goodBody := `{"type":"google.protobuf.Duration","body":"123.000s"}` 229 230 execute := func(body string) *httptest.ResponseRecorder { 231 req := httptest.NewRequest( 232 "POST", 233 "http://example.com/internal/tasks/default/abc-def", 234 bytes.NewReader([]byte(body))) 235 rw := httptest.NewRecorder() 236 r.ServeHTTP(rw, req) 237 return rw 238 } 239 240 // Error conditions inside the task body. 241 242 returnErr = nil 243 rw := execute(goodBody) 244 So(rw.Code, ShouldEqual, 200) 245 So(rw.Body.String(), ShouldEqual, "OK\n") 246 247 returnErr = fmt.Errorf("fatal err") 248 rw = execute(goodBody) 249 So(rw.Code, ShouldEqual, 202) // no retry! 250 So(rw.Body.String(), ShouldEqual, "Fatal error: fatal err\n") 251 252 // 500 for retry on transient errors. 253 returnErr = errors.New("transient err", transient.Tag) 254 rw = execute(goodBody) 255 So(rw.Code, ShouldEqual, 500) 256 So(rw.Body.String(), ShouldEqual, "Transient error: transient err\n") 257 258 // 409 for retry on Retry-tagged errors. Retry tag trumps transient.Tag. 259 returnErr = errors.New("retry me", transient.Tag, Retry) 260 rw = execute(goodBody) 261 So(rw.Code, ShouldEqual, 409) 262 So(rw.Body.String(), ShouldEqual, "The handler asked for retry: retry me\n") 263 264 // Error conditions when routing the task. 265 266 panicNow = true 267 268 rw = execute("not a json") 269 So(rw.Code, ShouldEqual, 202) // no retry! 270 So(rw.Body.String(), ShouldStartWith, "Bad payload, can't deserialize") 271 272 rw = execute(`{"type":"google.protobuf.Duration"}`) 273 So(rw.Code, ShouldEqual, 202) // no retry! 274 So(rw.Body.String(), ShouldStartWith, "Bad payload, can't deserialize") 275 276 rw = execute(`{"type":"google.protobuf.Duration","body":"blah"}`) 277 So(rw.Code, ShouldEqual, 202) // no retry! 278 So(rw.Body.String(), ShouldStartWith, "Bad payload, can't deserialize") 279 280 rw = execute(`{"type":"unknown.proto.type","body":"{}"}`) 281 So(rw.Code, ShouldEqual, 202) // no retry! 282 So(rw.Body.String(), ShouldStartWith, "Bad payload, can't deserialize") 283 }) 284 285 Convey("GetQueues", func() { 286 // Never called. 287 handler := func(ctx context.Context, payload proto.Message) error { 288 panic("handler was called in GetQueues") 289 } 290 291 Convey("empty queue name", func() { 292 d.RegisterTask(&durationpb.Duration{}, handler, "", nil) 293 So(d.GetQueues(), ShouldResemble, []string{"default"}) 294 }) 295 296 Convey("multiple queue names", func() { 297 d.RegisterTask(&durationpb.Duration{}, handler, "default", nil) 298 d.RegisterTask(&emptypb.Empty{}, handler, "another", nil) 299 queues := d.GetQueues() 300 sort.Strings(queues) 301 So(queues, ShouldResemble, []string{"another", "default"}) 302 }) 303 304 Convey("duplicated queue names", func() { 305 d.RegisterTask(&durationpb.Duration{}, handler, "default", nil) 306 d.RegisterTask(&emptypb.Empty{}, handler, "default", nil) 307 queues := d.GetQueues() 308 sort.Strings(queues) 309 So(queues, ShouldResemble, []string{"default"}) 310 }) 311 }) 312 }) 313 }