go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/examples/appengine/tq/main.go (about) 1 // Copyright 2020 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 // Demo of server/tq module on Appengine. 16 // 17 // It can also run locally, but it needs real Cloud Datastore, so you'll need 18 // to provide some Cloud Project name. Note that it will create real entities 19 // there which may interfere with the production server/tq deployment in this 20 // project, so use some experimental Cloud Project for this: 21 // 22 // $ go run . -cloud-project your-experimental-project 23 // $ curl http://127.0.0.1:8800/count-down/10 24 // <observe logs> 25 // $ curl http://127.0.0.1:8800/internal/tasks/c/sweep 26 // <observe logs> 27 // 28 // To test this for real, deploy the GAE app: 29 // 30 // $ gae.py upload -A your-experimental-project --switch 31 // $ curl https://<your-experimental-project>.appspot.com/count-down/10 32 // <observe logs> 33 // 34 // Again, be careful with what Cloud Project you are updating. 35 package main 36 37 import ( 38 "context" 39 "fmt" 40 "net/http" 41 "strconv" 42 "time" 43 44 "go.chromium.org/luci/common/clock" 45 "go.chromium.org/luci/common/logging" 46 "go.chromium.org/luci/gae/service/datastore" 47 "go.chromium.org/luci/server" 48 "go.chromium.org/luci/server/gaeemulation" 49 "go.chromium.org/luci/server/module" 50 "go.chromium.org/luci/server/router" 51 "go.chromium.org/luci/server/tq" 52 "google.golang.org/protobuf/proto" 53 54 // Enable datastore transactional tasks support. 55 _ "go.chromium.org/luci/server/tq/txn/datastore" 56 57 "go.chromium.org/luci/examples/appengine/tq/taskspb" 58 ) 59 60 // ExampleEntity is just some test entity to update in a transaction. 61 type ExampleEntity struct { 62 _kind string `gae:"$kind,ExampleEntity"` 63 ID int64 `gae:"$id"` 64 LastUpdate time.Time `gae:",noindex"` 65 } 66 67 func init() { 68 // RegisterTaskClass tells the TQ module how to serialize, route and execute 69 // a task of a particular proto type (*taskspb.CountDownTask in this case). 70 // 71 // It can be called any time before the serving loop (e.g. in an init, 72 // in main, in server.Main callback, etc). init() is the preferred place. 73 tq.RegisterTaskClass(tq.TaskClass{ 74 // This is a stable ID that identifies this particular kind of tasks. 75 // Changing it will essentially "break" all inflight tasks. 76 ID: "count-down-task", 77 78 // This is used for deserialization and also for discovery of what ID to use 79 // when submitting tasks. Changing it is safe as long as the JSONPB 80 // representation of in-flight tasks still matches the new proto. 81 Prototype: (*taskspb.CountDownTask)(nil), 82 83 // This controls how AddTask calls behave with respect to transactions. 84 // FollowsContext means "enqueue transactionally if the context is 85 // transactional, or non-transactionally otherwise". Other possibilities are 86 // Transactional (always require a transaction) and NonTransactional 87 // (fail if called from a transaction). 88 Kind: tq.FollowsContext, 89 90 // What Cloud Tasks queue to use for these tasks. See queue.yaml. 91 Queue: "countdown-tasks", 92 93 // Handler will be called to handle a previously submitted task. It can also 94 // be attached later (perhaps even in from a different package) via 95 // AttachHandler. 96 Handler: func(ctx context.Context, payload proto.Message) error { 97 task := payload.(*taskspb.CountDownTask) 98 99 logging.Infof(ctx, "Got %d", task.Number) 100 if task.Number <= 0 { 101 return nil // stop counting 102 } 103 104 return datastore.RunInTransaction(ctx, func(ctx context.Context) error { 105 // Update some entity. 106 err := datastore.Put(ctx, &ExampleEntity{ 107 ID: task.Number, 108 LastUpdate: clock.Now(ctx).UTC(), 109 }) 110 if err != nil { 111 return err 112 } 113 // And transactionally enqueue the next task. Note if you need to submit 114 // more tasks, it is fine to call multiple Enqueue (or AddTask) in 115 // parallel. 116 return EnqueueCountDown(ctx, task.Number-1) 117 }, nil) 118 }, 119 }) 120 } 121 122 // EnqueueCountDown enqueues a count down task. 123 func EnqueueCountDown(ctx context.Context, num int64) error { 124 return tq.AddTask(ctx, &tq.Task{ 125 // The body of the task. Also identifies what TaskClass to use. 126 Payload: &taskspb.CountDownTask{Number: num}, 127 // Title appears in logs and URLs, useful for debugging. 128 Title: fmt.Sprintf("count-%d", num), 129 // How long to wait before executing this task. Not super precise. 130 Delay: 100 * time.Millisecond, 131 }) 132 } 133 134 func main() { 135 modules := []module.Module{ 136 gaeemulation.NewModuleFromFlags(), // to use Cloud Datastore 137 tq.NewModuleFromFlags(), // to transactionally submit Cloud Tasks 138 } 139 140 server.Main(nil, modules, func(srv *server.Server) error { 141 srv.Routes.GET("/count-down/:From", nil, func(c *router.Context) { 142 num, err := strconv.ParseInt(c.Params.ByName("From"), 10, 32) 143 if err != nil { 144 http.Error(c.Writer, "Not a number", http.StatusBadRequest) 145 return 146 } 147 // Kick off the chain by enqueuing the starting task. 148 if err = EnqueueCountDown(c.Request.Context(), num); err != nil { 149 http.Error(c.Writer, err.Error(), http.StatusInternalServerError) 150 } else { 151 c.Writer.Write([]byte("OK\n")) 152 } 153 }) 154 return nil 155 }) 156 }