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  }