Merge changes I06a504a9,I54acfc52,I512ddcd0,Ic0288231

* changes:
  scouting: Allow background tasks to cancel themselves
  scouting: Change background_task implementation
  scouting: Make background_task interval configurable
  Make the scouting background scraper more generic
diff --git a/scouting/background_task/BUILD b/scouting/background_task/BUILD
new file mode 100644
index 0000000..bdf7190
--- /dev/null
+++ b/scouting/background_task/BUILD
@@ -0,0 +1,15 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
+
+go_library(
+    name = "background_task",
+    srcs = ["background_task.go"],
+    importpath = "github.com/frc971/971-Robot-Code/scouting/background_task",
+    target_compatible_with = ["@platforms//cpu:x86_64"],
+    visibility = ["//visibility:public"],
+)
+
+go_test(
+    name = "background_task_test",
+    srcs = ["background_task_test.go"],
+    embed = [":background_task"],
+)
diff --git a/scouting/background_task/background_task.go b/scouting/background_task/background_task.go
new file mode 100644
index 0000000..3d7d29a
--- /dev/null
+++ b/scouting/background_task/background_task.go
@@ -0,0 +1,58 @@
+package background_task
+
+import (
+	"time"
+)
+
+// A helper to run a function in the background at a specified interval.
+// Can be used for a lot of different things.
+type backgroundTask struct {
+	ticker        *time.Ticker
+	stopRequested chan bool
+	done          chan bool
+}
+
+func New(interval time.Duration) backgroundTask {
+	return backgroundTask{
+		ticker:        time.NewTicker(interval),
+		stopRequested: make(chan bool, 1),
+		done:          make(chan bool, 1),
+	}
+}
+
+func (task *backgroundTask) Start(taskFunc func()) {
+	go func() {
+		// Signal the Stop() function below when the goroutine has
+		// finished executing.
+		defer func() { task.done <- true }()
+
+		// time.Ticker doesn't perform an immediate invocation.
+		// Instead, it waits for the specified duration before
+		// triggering the first tick. We pretend that there's a tick
+		// here by invoking the callback manually.
+		taskFunc()
+
+		for {
+			select {
+			case <-task.stopRequested:
+				return
+			case <-task.ticker.C:
+				taskFunc()
+			}
+		}
+	}()
+}
+
+// Stops the background task from within the background task. The Stop()
+// function still needs to be called from outside the task.
+func (task *backgroundTask) StopFromWithinTask() {
+	task.stopRequested <- true
+}
+
+func (task *backgroundTask) Stop() {
+	task.stopRequested <- true
+	task.ticker.Stop()
+	<-task.done
+	close(task.stopRequested)
+	close(task.done)
+}
diff --git a/scouting/background_task/background_task_test.go b/scouting/background_task/background_task_test.go
new file mode 100644
index 0000000..d4fb7ee
--- /dev/null
+++ b/scouting/background_task/background_task_test.go
@@ -0,0 +1,50 @@
+package background_task
+
+import (
+	"testing"
+	"time"
+)
+
+func TestBackgroundTask(t *testing.T) {
+	task := New(100 * time.Millisecond)
+	defer task.Stop()
+
+	counter := 0
+	task.Start(func() {
+		counter += 1
+	})
+
+	// Block until we've seeen 10 timer ticks.
+	for counter < 10 {
+		time.Sleep(100 * time.Millisecond)
+	}
+}
+
+func TestSelfCancellation(t *testing.T) {
+	task := New(100 * time.Millisecond)
+
+	done := false
+	counter := 0
+	task.Start(func() {
+		counter += 1
+
+		if done {
+			t.Fatal("callback should not be called after cancellation")
+		}
+
+		if counter == 10 {
+			task.StopFromWithinTask()
+			done = true
+		}
+	})
+
+	// Block until the background task has cancelled itself.
+	for !done {
+		time.Sleep(100 * time.Millisecond)
+	}
+
+	// Then sleep for a little longer to make sure that the task won't
+	// invoke the t.Fatal().
+	time.Sleep(time.Second)
+	task.Stop()
+}
diff --git a/scouting/scraping/background/BUILD b/scouting/scraping/background/BUILD
deleted file mode 100644
index 9aa92c9..0000000
--- a/scouting/scraping/background/BUILD
+++ /dev/null
@@ -1,9 +0,0 @@
-load("@io_bazel_rules_go//go:def.bzl", "go_library")
-
-go_library(
-    name = "background",
-    srcs = ["background.go"],
-    importpath = "github.com/frc971/971-Robot-Code/scouting/scraping/background",
-    target_compatible_with = ["@platforms//cpu:x86_64"],
-    visibility = ["//visibility:public"],
-)
diff --git a/scouting/scraping/background/background.go b/scouting/scraping/background/background.go
deleted file mode 100644
index 5af8c3e..0000000
--- a/scouting/scraping/background/background.go
+++ /dev/null
@@ -1,52 +0,0 @@
-package background
-
-import (
-	"time"
-)
-
-// A helper to run a function in the background every ~10 minutes. Technically
-// can be used for a lot of different things, but is primarily geared towards
-// scraping thebluealliance.com.
-type BackgroundScraper struct {
-	doneChan     chan<- bool
-	checkStopped chan<- bool
-}
-
-func (scraper *BackgroundScraper) Start(scrape func()) {
-	scraper.doneChan = make(chan bool, 1)
-	scraper.checkStopped = make(chan bool, 1)
-
-	go func() {
-		// Setting start time to 11 minutes prior so getRankings called instantly when Start() called
-		startTime := time.Now().Add(-11 * time.Minute)
-		for {
-			curTime := time.Now()
-			diff := curTime.Sub(startTime)
-
-			if diff.Minutes() > 10 {
-				scrape()
-				startTime = curTime
-			}
-
-			if len(scraper.doneChan) != 0 {
-				break
-			}
-
-			time.Sleep(time.Second)
-		}
-
-		scraper.checkStopped <- true
-	}()
-}
-
-func (scraper *BackgroundScraper) Stop() {
-	scraper.doneChan <- true
-
-	for {
-		if len(scraper.checkStopped) != 0 {
-			close(scraper.doneChan)
-			close(scraper.checkStopped)
-			break
-		}
-	}
-}
diff --git a/scouting/webserver/BUILD b/scouting/webserver/BUILD
index 934b50a..f5b0a81 100644
--- a/scouting/webserver/BUILD
+++ b/scouting/webserver/BUILD
@@ -7,8 +7,8 @@
     target_compatible_with = ["@platforms//cpu:x86_64"],
     visibility = ["//visibility:private"],
     deps = [
+        "//scouting/background_task",
         "//scouting/db",
-        "//scouting/scraping/background",
         "//scouting/webserver/driver_ranking",
         "//scouting/webserver/match_list",
         "//scouting/webserver/rankings",
diff --git a/scouting/webserver/main.go b/scouting/webserver/main.go
index 1b5a002..c5bed2c 100644
--- a/scouting/webserver/main.go
+++ b/scouting/webserver/main.go
@@ -14,8 +14,8 @@
 	"syscall"
 	"time"
 
+	"github.com/frc971/971-Robot-Code/scouting/background_task"
 	"github.com/frc971/971-Robot-Code/scouting/db"
-	"github.com/frc971/971-Robot-Code/scouting/scraping/background"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/driver_ranking"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/match_list"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/rankings"
@@ -141,17 +141,17 @@
 	// Since Go doesn't support default arguments, we use 0 and "" to
 	// indicate that we want to source the values from the config.
 
-	matchListScraper := background.BackgroundScraper{}
+	matchListScraper := background_task.New(10 * time.Minute)
 	matchListScraper.Start(func() {
 		match_list.GetMatchList(database, 0, "", *blueAllianceConfigPtr)
 	})
 
-	rankingsScraper := background.BackgroundScraper{}
+	rankingsScraper := background_task.New(10 * time.Minute)
 	rankingsScraper.Start(func() {
 		rankings.GetRankings(database, 0, "", *blueAllianceConfigPtr)
 	})
 
-	driverRankingParser := background.BackgroundScraper{}
+	driverRankingParser := background_task.New(10 * time.Minute)
 	driverRankingParser.Start(func() {
 		// Specify "" as the script path here so that the default is
 		// used.
diff --git a/scouting/webserver/rankings/BUILD b/scouting/webserver/rankings/BUILD
index 4696d26..5192e87 100644
--- a/scouting/webserver/rankings/BUILD
+++ b/scouting/webserver/rankings/BUILD
@@ -20,8 +20,8 @@
     ],
     embed = [":rankings"],
     deps = [
+        "//scouting/background_task",
         "//scouting/db",
-        "//scouting/scraping/background",
         "//scouting/webserver/server",
     ],
 )
diff --git a/scouting/webserver/rankings/rankings_test.go b/scouting/webserver/rankings/rankings_test.go
index 6f8af3b..4ab3c34 100644
--- a/scouting/webserver/rankings/rankings_test.go
+++ b/scouting/webserver/rankings/rankings_test.go
@@ -1,14 +1,15 @@
 package rankings
 
 import (
-	"github.com/frc971/971-Robot-Code/scouting/db"
-	"github.com/frc971/971-Robot-Code/scouting/scraping/background"
-	"github.com/frc971/971-Robot-Code/scouting/webserver/server"
 	"net/http"
 	"reflect"
 	"strings"
 	"testing"
 	"time"
+
+	"github.com/frc971/971-Robot-Code/scouting/background_task"
+	"github.com/frc971/971-Robot-Code/scouting/db"
+	"github.com/frc971/971-Robot-Code/scouting/webserver/server"
 )
 
 type MockDatabase struct {
@@ -37,7 +38,7 @@
 
 func TestGetRankings(t *testing.T) {
 	database := MockDatabase{}
-	scraper := background.BackgroundScraper{}
+	scraper := background_task.New(time.Minute)
 	tbaServer := server.NewScoutingServer()
 	tbaServer.Handle("/", ServeRankings(t, http.FileServer(http.Dir("../../"))))
 	tbaServer.Start(8000)