internal/postgres: limit imported-by-count updates

Add a query parameter that limits the total number of counts updated.
The intent is to limit the work that the endpoint does, to avoid
overloading the system.

Change-Id: I28b0031f171192870e84111714c72d996a6a6964
Reviewed-on: https://go-review.googlesource.com/c/pkgsite/+/854068
Reviewed-by: Nicholas Husin <nsh@golang.org>
Reviewed-by: Ethan Lee <ethanalee@google.com>
LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Nicholas Husin <husin@google.com>
diff --git a/internal/postgres/import_counts.go b/internal/postgres/import_counts.go
index 89f1652..64a8da2 100644
--- a/internal/postgres/import_counts.go
+++ b/internal/postgres/import_counts.go
@@ -21,6 +21,9 @@
 	// (in a single transaction, with the rows locked).
 	// If zero, a reasonable default is used.
 	BatchSize int
+	// A hard limit on the number of rows to update (per kind: applies separately to modules
+	// and packages). If zero, then no limit.
+	Limit int
 	// Maximum time to spend updating. If zero, then no max.
 	MaxTime time.Duration
 	// How old a count has to be before updating it.
@@ -221,9 +224,17 @@
 	cutoff := time.Now().Add(-opts.Staleness)
 	deadline := time.Now().Add(opts.MaxTime)
 	for time.Now().Before(deadline) {
+		batchSize := opts.BatchSize
+		if opts.Limit > 0 {
+			rem := opts.Limit - int(nUpdated)
+			if rem <= 0 {
+				break
+			}
+			batchSize = min(batchSize, rem)
+		}
 		var nu int64
 		err = db.db.Transact(ctx, sql.LevelDefault, func(tx *database.DB) error {
-			if _, err := tx.Exec(ctx, query, cutoff, opts.BatchSize); err != nil {
+			if _, err := tx.Exec(ctx, query, cutoff, batchSize); err != nil {
 				return fmt.Errorf("creating temp table: %w", err)
 			}
 			var err error
diff --git a/internal/postgres/import_counts_test.go b/internal/postgres/import_counts_test.go
index ec23d42..6a72e5f 100644
--- a/internal/postgres/import_counts_test.go
+++ b/internal/postgres/import_counts_test.go
@@ -235,6 +235,24 @@
 		// counted in imported_by_module_count.
 		_ = validateImportedByCountAndGetSearchDocument(t, testDB, "fmt", 1, 2)
 	})
+	t.Run("limit", func(t *testing.T) {
+		testDB, release := acquire(t)
+		defer release()
+
+		m := sample.Module("mod.com/A", "v1.0.0", "p1", "p2", "p3", "p4")
+		testDB.MustInsertModule(t, m)
+
+		np, nm, err := testDB.UpdateSearchDocumentsImportedByCount(ctx, &ImporterCountOptions{
+			BatchSize: 2,
+			Limit:     3,
+		})
+		if err != nil {
+			t.Fatal(err)
+		}
+		if np != 3 || nm != 3 {
+			t.Errorf("got (%d, %d) updated, want (3, 3)", np, nm)
+		}
+	})
 }
 
 func TestUpdateSearchDocumentsImportedByCountWithCounts(t *testing.T) {
diff --git a/internal/worker/server.go b/internal/worker/server.go
index eec758b..1d1d915 100644
--- a/internal/worker/server.go
+++ b/internal/worker/server.go
@@ -278,15 +278,19 @@
 	// The Cloud Scheduler max timeout is 30 minutes.
 	// Default to less for wiggle room.
 	defaultMaxTime = 26 * time.Minute
+	// Update as many counts as possible in the given time.
+	defaultLimit = 0
 )
 
 // handleUpdateImportedByCount updates imported_by_count for all packages.
 func (s *Server) handleUpdateImportedByCount(w http.ResponseWriter, r *http.Request) error {
 	batchSize := parseIntParam(r, "batch", defaultBatchSize)
+	limit := parseIntParam(r, "limit", defaultLimit)
 	np, nm, err := s.db.UpdateSearchDocumentsImportedByCount(r.Context(), &postgres.ImporterCountOptions{
 		BatchSize: batchSize,
 		Staleness: defaultStaleness,
 		MaxTime:   defaultMaxTime,
+		Limit:     limit,
 	})
 	if err != nil {
 		return err