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  }