internal/worker: support job cancellation

The analysis/scan endpoint checks if the job is canceled and stops
early if it is.

The jobs/cancel endpoint cancels a job.

Change-Id: I59a9b025b7ab0b3703175809bc798f76522fcfb5
Reviewed-on: https://go-review.googlesource.com/c/pkgsite-metrics/+/495796
Reviewed-by: Maceo Thompson <maceothompson@google.com>
Run-TryBot: Jonathan Amsterdam <jba@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
diff --git a/internal/worker/analysis.go b/internal/worker/analysis.go
index 05620c4..a56f991 100644
--- a/internal/worker/analysis.go
+++ b/internal/worker/analysis.go
@@ -74,6 +74,17 @@
 		return fmt.Errorf("%w: %v", derrors.InvalidArgument, err)
 	}
 
+	// If there is a job and it's canceled, return immediately.
+	if req.JobID != "" && s.jobDB != nil {
+		job, err := s.jobDB.GetJob(ctx, req.JobID)
+		if err != nil {
+			log.Errorf(ctx, err, "failed to get job for id %q", req.JobID)
+		} else if job.Canceled {
+			log.Infof(ctx, "job %q canceled; skipping", req.JobID)
+			return nil
+		}
+	}
+
 	// updateJob updates the job for this request if there is one.
 	// If there is an error, it logs it instead of failing.
 	updateJob := func(f func(*jobs.Job)) {
diff --git a/internal/worker/jobs.go b/internal/worker/jobs.go
index 1ec45b8..e9bbf94 100644
--- a/internal/worker/jobs.go
+++ b/internal/worker/jobs.go
@@ -60,6 +60,15 @@
 		enc.Encode(job)
 		return nil
 
+	case "/cancel":
+		if jobID == "" {
+			return fmt.Errorf("missing jobid: %w", derrors.InvalidArgument)
+		}
+		return db.UpdateJob(ctx, jobID, func(j *jobs.Job) error {
+			j.Canceled = true
+			return nil
+		})
+
 	default:
 		return fmt.Errorf("unknown path %q: %w", path, derrors.InvalidArgument)
 	}
diff --git a/internal/worker/jobs_test.go b/internal/worker/jobs_test.go
index a5ea704..806a30e 100644
--- a/internal/worker/jobs_test.go
+++ b/internal/worker/jobs_test.go
@@ -37,6 +37,18 @@
 	if !cmp.Equal(&got, job) {
 		t.Errorf("got\n%+v\nwant\n%+v", got, job)
 	}
+
+	if err := processJobRequest(ctx, &buf, "/cancel", job.ID(), db); err != nil {
+		t.Fatal(err)
+	}
+
+	got2, err := db.GetJob(ctx, job.ID())
+	if err != nil {
+		t.Fatal(err)
+	}
+	if !got2.Canceled {
+		t.Error("got canceled false, want true")
+	}
 }
 
 type testJobDB struct {