internal/worker: route to list jobs

Provide the "/jobs/list" route, which will write a simple
list of existing jobs.

Change-Id: Ibc16ed65b6209a84d6b95b32ddd6f6b837014a7c
Reviewed-on: https://go-review.googlesource.com/c/pkgsite-metrics/+/496187
Run-TryBot: Jonathan Amsterdam <jba@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
Reviewed-by: Zvonimir Pavlinovic <zpavlinovic@google.com>
diff --git a/internal/worker/jobs.go b/internal/worker/jobs.go
index e9bbf94..79076dc 100644
--- a/internal/worker/jobs.go
+++ b/internal/worker/jobs.go
@@ -15,12 +15,14 @@
 package worker
 
 import (
+	"bytes"
 	"context"
 	"encoding/json"
 	"errors"
 	"fmt"
 	"io"
 	"net/http"
+	"time"
 
 	"golang.org/x/pkgsite-metrics/internal/derrors"
 	"golang.org/x/pkgsite-metrics/internal/jobs"
@@ -43,6 +45,7 @@
 	DeleteJob(ctx context.Context, id string) error
 	GetJob(ctx context.Context, id string) (*jobs.Job, error)
 	UpdateJob(ctx context.Context, id string, f func(*jobs.Job) error) error
+	ListJobs(context.Context, func(*jobs.Job, time.Time) error) error
 }
 
 func processJobRequest(ctx context.Context, w io.Writer, path, jobID string, db jobDB) error {
@@ -69,6 +72,21 @@
 			return nil
 		})
 
+	case "/list":
+		var buf bytes.Buffer
+		fmt.Fprintf(&buf, "%-20s\tEnq\tCompl\tCanceled\n", "ID")
+		err := db.ListJobs(ctx, func(j *jobs.Job, _ time.Time) error {
+			_, err := fmt.Fprintf(&buf, "%-20s\t%3d\t%3d\t%t\n",
+				j.ID(), j.NumEnqueued, j.NumFailed+j.NumErrored+j.NumSucceeded,
+				j.Canceled)
+			return err
+		})
+		if err != nil {
+			return err
+		}
+		buf.WriteTo(w)
+		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 806a30e..c682d71 100644
--- a/internal/worker/jobs_test.go
+++ b/internal/worker/jobs_test.go
@@ -9,10 +9,13 @@
 	"context"
 	"encoding/json"
 	"fmt"
+	"strings"
 	"testing"
 	"time"
 
 	"github.com/google/go-cmp/cmp"
+	"golang.org/x/exp/maps"
+	"golang.org/x/exp/slices"
 	"golang.org/x/pkgsite-metrics/internal/derrors"
 	"golang.org/x/pkgsite-metrics/internal/jobs"
 )
@@ -49,6 +52,17 @@
 	if !got2.Canceled {
 		t.Error("got canceled false, want true")
 	}
+
+	buf.Reset()
+	if err := processJobRequest(ctx, &buf, "/list", "", db); err != nil {
+		t.Fatal(err)
+	}
+	// Don't check for specific output, just make sure there's something
+	// that mentions the job ID.
+	got3 := buf.String()
+	if !strings.Contains(got3, job.ID()) {
+		t.Errorf("got\n%q\nwhich does not contain the job ID %q", got3, job.ID())
+	}
 }
 
 type testJobDB struct {
@@ -90,3 +104,17 @@
 	d.jobs[id] = j
 	return nil
 }
+
+func (d *testJobDB) ListJobs(ctx context.Context, f func(*jobs.Job, time.Time) error) error {
+	jobslice := maps.Values(d.jobs)
+	// Sort by StartedAt descending.
+	slices.SortFunc(jobslice, func(j1, j2 *jobs.Job) bool {
+		return j1.StartedAt.After(j2.StartedAt)
+	})
+	for _, j := range jobslice {
+		if err := f(j, time.Time{}); err != nil {
+			return err
+		}
+	}
+	return nil
+}