internal/config: remove global

Remove the `cfg` global and the functions that accessed it.

Fixes b/145301722.

Change-Id: I58ab9fbd4fc29f66dbc5b120f04c88ee0703ee57
Reviewed-on: https://go-review.googlesource.com/c/pkgsite/+/238437
Reviewed-by: Julie Qiu <julie@golang.org>
diff --git a/cmd/frontend/main.go b/cmd/frontend/main.go
index 1125f8b..ca92e9e 100644
--- a/cmd/frontend/main.go
+++ b/cmd/frontend/main.go
@@ -100,6 +100,7 @@
 		StaticPath:           *staticPath,
 		ThirdPartyPath:       *thirdPartyPath,
 		DevMode:              *devMode,
+		AppVersionLabel:      cfg.AppVersionLabel(),
 	})
 	if err != nil {
 		log.Fatalf(ctx, "frontend.NewServer: %v", err)
@@ -171,7 +172,7 @@
 			}
 		}
 		return queue.NewInMemory(ctx, proxyClient, sourceClient, db, 10,
-			frontend.FetchAndUpdateState, experiment.NewSet(set))
+			frontend.FetchAndUpdateState, experiment.NewSet(set), cfg.AppVersionLabel())
 	}
 	client, err := cloudtasks.NewClient(ctx)
 	if err != nil {
@@ -190,7 +191,7 @@
 func openDB(ctx context.Context, cfg *config.Config, driver string) (_ *database.DB, err error) {
 	derrors.Wrap(&err, "openDB(ctx, cfg, %q)", driver)
 	log.Infof(ctx, "opening database on host %s", cfg.DBHost)
-	ddb, err := database.Open(driver, cfg.DBConnInfo())
+	ddb, err := database.Open(driver, cfg.DBConnInfo(), cfg.InstanceID)
 	if err == nil {
 		return ddb, nil
 	}
@@ -201,7 +202,7 @@
 	}
 	log.Errorf(ctx, "database.Open for primary host %s failed with %v; trying secondary host %s ",
 		cfg.DBHost, err, cfg.DBSecondaryHost)
-	return database.Open(driver, ci)
+	return database.Open(driver, ci, cfg.InstanceID)
 }
 func getLogger(ctx context.Context, cfg *config.Config) middleware.Logger {
 	if cfg.OnAppEngine() {
diff --git a/cmd/worker/main.go b/cmd/worker/main.go
index 72c6d21..c8a1700 100644
--- a/cmd/worker/main.go
+++ b/cmd/worker/main.go
@@ -68,7 +68,7 @@
 	if err != nil {
 		log.Fatalf(ctx, "unable to register the ocsql driver: %v\n", err)
 	}
-	ddb, err := database.Open(driverName, cfg.DBConnInfo())
+	ddb, err := database.Open(driverName, cfg.DBConnInfo(), cfg.InstanceID)
 	if err != nil {
 		log.Fatalf(ctx, "database.Open: %v", err)
 	}
@@ -157,7 +157,7 @@
 			}
 		}
 		return queue.NewInMemory(ctx, proxyClient, sourceClient, db, *workers,
-			worker.FetchAndUpdateState, experiment.NewSet(set))
+			worker.FetchAndUpdateState, experiment.NewSet(set), cfg.AppVersionLabel())
 	}
 	if queueName == "" {
 		log.Fatal(ctx, "missing queue: must set GO_DISCOVERY_WORKER_TASK_QUEUE env var")
diff --git a/internal/config/config.go b/internal/config/config.go
index cd837fe..9f5ca64 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -38,14 +38,6 @@
 	return fallback
 }
 
-func InstanceID() string {
-	return cfg.InstanceID
-}
-
-func AppVersionLabel() string {
-	return cfg.AppVersionLabel()
-}
-
 // AppVersionFormat is the expected format of the app version timestamp.
 const AppVersionFormat = "20060102t150405"
 
@@ -122,7 +114,6 @@
 // OnAppEngine reports if the current process is running in an AppEngine
 // environment.
 func (c *Config) OnAppEngine() bool {
-	// TODO(rfindley): verify that this works for the go1.12 runtime
 	return c.GaeEnv == "standard"
 }
 
@@ -208,63 +199,51 @@
 	AcceptedURLs []string
 }
 
-var cfg Config
-
 const overrideBucket = "go-discovery"
 
 // Init resolves all configuration values provided by the config package. It
 // must be called before any configuration values are used.
 func Init(ctx context.Context) (_ *Config, err error) {
 	defer derrors.Add(&err, "config.Init(ctx)")
-	cfg2, err := load(ctx)
-	if err != nil {
-		return nil, err
+	// Build a Config from the execution environment, loading some values
+	// from envvars and others from remote services.
+	cfg := &Config{
+		IndexURL:  GetEnv("GO_MODULE_INDEX_URL", "https://index.golang.org/index"),
+		ProxyURL:  GetEnv("GO_MODULE_PROXY_URL", "https://proxy.golang.org"),
+		Port:      os.Getenv("PORT"),
+		DebugPort: os.Getenv("DEBUG_PORT"),
+		// Resolve AppEngine identifiers
+		ProjectID:  os.Getenv("GOOGLE_CLOUD_PROJECT"),
+		ServiceID:  os.Getenv("GAE_SERVICE"),
+		VersionID:  os.Getenv("GAE_VERSION"),
+		InstanceID: os.Getenv("GAE_INSTANCE"),
+		GaeEnv:     os.Getenv("GAE_ENV"),
+		// LocationID is essentially hard-coded until we figure out a good way to
+		// determine it programmatically, but we check an environment variable in
+		// case it needs to be overridden.
+		LocationID: GetEnv("GO_DISCOVERY_GAE_LOCATION_ID", "us-central1"),
+		// This fallback should only be used when developing locally.
+		FallbackVersionLabel: time.Now().Format(AppVersionFormat),
+		DBHost:               chooseOne(GetEnv("GO_DISCOVERY_DATABASE_HOST", "localhost")),
+		DBUser:               GetEnv("GO_DISCOVERY_DATABASE_USER", "postgres"),
+		DBPassword:           os.Getenv("GO_DISCOVERY_DATABASE_PASSWORD"),
+		DBSecondaryHost:      chooseOne(os.Getenv("GO_DISCOVERY_DATABASE_SECONDARY_HOST")),
+		DBPort:               GetEnv("GO_DISCOVERY_DATABASE_PORT", "5432"),
+		DBName:               GetEnv("GO_DISCOVERY_DATABASE_NAME", "discovery-db"),
+		DBSecret:             os.Getenv("GO_DISCOVERY_DATABASE_SECRET"),
+		RedisCacheHost:       os.Getenv("GO_DISCOVERY_REDIS_HOST"),
+		RedisCachePort:       GetEnv("GO_DISCOVERY_REDIS_PORT", "6379"),
+		RedisHAHost:          os.Getenv("GO_DISCOVERY_REDIS_HA_HOST"),
+		RedisHAPort:          GetEnv("GO_DISCOVERY_REDIS_HA_PORT", "6379"),
+		Quota: QuotaSettings{
+			QPS:          10,
+			Burst:        20,
+			MaxEntries:   1000,
+			RecordOnly:   func() *bool { t := true; return &t }(),
+			AcceptedURLs: parseCommaList(GetEnv("GO_DISCOVERY_ACCEPTED_LIST", "")),
+		},
+		UseProfiler: os.Getenv("GO_DISCOVERY_USE_PROFILER") == "TRUE",
 	}
-	cfg = *cfg2
-	return cfg2, nil
-}
-
-// load builds a Config from the execution environment, loading some values
-// from envvars and others from remote services.
-func load(ctx context.Context) (_ *Config, err error) {
-	defer derrors.Add(&err, "config.Load(ctx)")
-
-	// TODO(b/145301722): remove this comment.
-	// This variable shadowing is temporary, as this package is being made
-	// stateless. Init is being incrementally deprecated in favor of an exported
-	// Load function.
-	cfg := &Config{}
-
-	// Resolve client/server configuration from the environment.
-	cfg.IndexURL = GetEnv("GO_MODULE_INDEX_URL", "https://index.golang.org/index")
-	cfg.ProxyURL = GetEnv("GO_MODULE_PROXY_URL", "https://proxy.golang.org")
-	cfg.Port = os.Getenv("PORT")
-	cfg.DebugPort = os.Getenv("DEBUG_PORT")
-
-	// Resolve AppEngine identifiers
-	cfg.ProjectID = os.Getenv("GOOGLE_CLOUD_PROJECT")
-	cfg.ServiceID = os.Getenv("GAE_SERVICE")
-	cfg.VersionID = os.Getenv("GAE_VERSION")
-	cfg.InstanceID = os.Getenv("GAE_INSTANCE")
-	cfg.GaeEnv = os.Getenv("GAE_ENV")
-
-	// locationID is essentially hard-coded until we figure out a good way to
-	// determine it programmatically, but we check an environment variable in
-	// case it needs to be overridden.
-	cfg.LocationID = GetEnv("GO_DISCOVERY_GAE_LOCATION_ID", "us-central1")
-
-	if cfg.GaeEnv != "" {
-		// Zone is not available in the environment but can be queried via the metadata API.
-		zone, err := gceMetadata(ctx, "instance/zone")
-		if err != nil {
-			return nil, err
-		}
-		cfg.ZoneID = zone
-	}
-
-	// this fallback should only be used when developing locally.
-	cfg.FallbackVersionLabel = time.Now().Format(AppVersionFormat)
-
 	cfg.AppMonitoredResource = &mrpb.MonitoredResource{
 		Type: "gae_app",
 		Labels: map[string]string{
@@ -275,17 +254,17 @@
 		},
 	}
 
-	cfg.DBUser = GetEnv("GO_DISCOVERY_DATABASE_USER", "postgres")
-	cfg.DBPassword = os.Getenv("GO_DISCOVERY_DATABASE_PASSWORD")
-	cfg.DBHost = chooseOne(GetEnv("GO_DISCOVERY_DATABASE_HOST", "localhost"))
+	if cfg.GaeEnv != "" {
+		// Zone is not available in the environment but can be queried via the metadata API.
+		zone, err := gceMetadata(ctx, "instance/zone")
+		if err != nil {
+			return nil, err
+		}
+		cfg.ZoneID = zone
+	}
 	if cfg.DBHost == "" {
 		panic("DBHost is empty; impossible")
 	}
-	cfg.DBSecondaryHost = chooseOne(os.Getenv("GO_DISCOVERY_DATABASE_SECONDARY_HOST"))
-	cfg.DBPort = GetEnv("GO_DISCOVERY_DATABASE_PORT", "5432")
-	cfg.DBName = GetEnv("GO_DISCOVERY_DATABASE_NAME", "discovery-db")
-	cfg.DBSecret = os.Getenv("GO_DISCOVERY_DATABASE_SECRET")
-
 	if cfg.DBSecret != "" {
 		var err error
 		cfg.DBPassword, err = secrets.Get(ctx, cfg.DBSecret)
@@ -294,19 +273,6 @@
 		}
 	}
 
-	cfg.RedisCacheHost = os.Getenv("GO_DISCOVERY_REDIS_HOST")
-	cfg.RedisCachePort = GetEnv("GO_DISCOVERY_REDIS_PORT", "6379")
-	cfg.RedisHAHost = os.Getenv("GO_DISCOVERY_REDIS_HA_HOST")
-	cfg.RedisHAPort = GetEnv("GO_DISCOVERY_REDIS_HA_PORT", "6379")
-	cfg.Quota = QuotaSettings{
-		QPS:          10,
-		Burst:        20,
-		MaxEntries:   1000,
-		RecordOnly:   func() *bool { t := true; return &t }(),
-		AcceptedURLs: parseCommaList(GetEnv("GO_DISCOVERY_ACCEPTED_LIST", "")),
-	}
-	cfg.UseProfiler = os.Getenv("GO_DISCOVERY_USE_PROFILER") == "TRUE"
-
 	// If GO_DISCOVERY_CONFIG_OVERRIDE is set, it should point to a file
 	// in overrideBucket which provides overrides for selected configuration.
 	// Use this when you want to fix something in prod quickly, without waiting
@@ -381,7 +347,7 @@
 	fmt.Fprint(w, "config: ")
 	enc := json.NewEncoder(w)
 	enc.SetIndent("", "    ")
-	return enc.Encode(cfg)
+	return enc.Encode(c)
 }
 
 // chooseOne selects one entry at random from a whitespace-separated
diff --git a/internal/database/database.go b/internal/database/database.go
index 86d35d5..d13973d 100644
--- a/internal/database/database.go
+++ b/internal/database/database.go
@@ -20,7 +20,6 @@
 	"unicode"
 
 	"github.com/lib/pq"
-	"golang.org/x/pkgsite/internal/config"
 	"golang.org/x/pkgsite/internal/derrors"
 	"golang.org/x/pkgsite/internal/log"
 )
@@ -33,13 +32,14 @@
 // operate within the transaction.
 type DB struct {
 	db         *sql.DB
+	instanceID string
 	tx         *sql.Tx
 	mu         sync.Mutex
 	maxRetries int // max times a single transaction was retried
 }
 
 // Open creates a new DB  for the given connection string.
-func Open(driverName, dbinfo string) (_ *DB, err error) {
+func Open(driverName, dbinfo, instanceID string) (_ *DB, err error) {
 	defer derrors.Wrap(&err, "database.Open(%q, %q)",
 		driverName, redactPassword(dbinfo))
 
@@ -50,12 +50,12 @@
 	if err := db.Ping(); err != nil {
 		return nil, err
 	}
-	return New(db), nil
+	return New(db, instanceID), nil
 }
 
 // New creates a new DB from a sql.DB.
-func New(db *sql.DB) *DB {
-	return &DB{db: db}
+func New(db *sql.DB, instanceID string) *DB {
+	return &DB{db: db, instanceID: instanceID}
 }
 
 func (db *DB) InTransaction() bool {
@@ -75,7 +75,7 @@
 
 // Exec executes a SQL statement.
 func (db *DB) Exec(ctx context.Context, query string, args ...interface{}) (res sql.Result, err error) {
-	defer logQuery(ctx, query, args)(&err)
+	defer logQuery(ctx, query, args, db.instanceID)(&err)
 
 	if db.tx != nil {
 		return db.tx.ExecContext(ctx, query, args...)
@@ -85,7 +85,7 @@
 
 // Query runs the DB query.
 func (db *DB) Query(ctx context.Context, query string, args ...interface{}) (_ *sql.Rows, err error) {
-	defer logQuery(ctx, query, args)(&err)
+	defer logQuery(ctx, query, args, db.instanceID)(&err)
 	if db.tx != nil {
 		return db.tx.QueryContext(ctx, query, args...)
 	}
@@ -94,7 +94,7 @@
 
 // QueryRow runs the query and returns a single row.
 func (db *DB) QueryRow(ctx context.Context, query string, args ...interface{}) *sql.Row {
-	defer logQuery(ctx, query, args)(nil)
+	defer logQuery(ctx, query, args, db.instanceID)(nil)
 	if db.tx != nil {
 		return db.tx.QueryRowContext(ctx, query, args...)
 	}
@@ -102,7 +102,7 @@
 }
 
 func (db *DB) Prepare(ctx context.Context, query string) (*sql.Stmt, error) {
-	defer logQuery(ctx, "preparing "+query, nil)
+	defer logQuery(ctx, "preparing "+query, nil, db.instanceID)
 	if db.tx != nil {
 		return db.tx.PrepareContext(ctx, query)
 	}
@@ -200,9 +200,9 @@
 		}
 	}()
 
-	dbtx := New(db.db)
+	dbtx := New(db.db, db.instanceID)
 	dbtx.tx = tx
-	defer logTransaction(ctx, opts)(&err)
+	defer dbtx.logTransaction(ctx, opts)(&err)
 	if err := txFunc(dbtx); err != nil {
 		return fmt.Errorf("txFunc(tx): %w", err)
 	}
@@ -451,7 +451,7 @@
 	Error           string `json:",omitempty"`
 }
 
-func logQuery(ctx context.Context, query string, args []interface{}) func(*error) {
+func logQuery(ctx context.Context, query string, args []interface{}, instanceID string) func(*error) {
 	if QueryLoggingDisabled {
 		return func(*error) {}
 	}
@@ -473,7 +473,7 @@
 		query = query[:maxlen] + "..."
 	}
 
-	uid := generateLoggingID()
+	uid := generateLoggingID(instanceID)
 
 	// Construct a short string of the args.
 	const (
@@ -517,11 +517,11 @@
 	}
 }
 
-func logTransaction(ctx context.Context, opts *sql.TxOptions) func(*error) {
+func (db *DB) logTransaction(ctx context.Context, opts *sql.TxOptions) func(*error) {
 	if QueryLoggingDisabled {
 		return func(*error) {}
 	}
-	uid := generateLoggingID()
+	uid := generateLoggingID(db.instanceID)
 	isoLevel := "default"
 	if opts != nil {
 		isoLevel = opts.Isolation.String()
@@ -534,8 +534,7 @@
 	}
 }
 
-func generateLoggingID() string {
-	instanceID := config.InstanceID()
+func generateLoggingID(instanceID string) string {
 	if instanceID == "" {
 		instanceID = "local"
 	} else {
diff --git a/internal/database/database_test.go b/internal/database/database_test.go
index fdba9aa..3e11f6e 100644
--- a/internal/database/database_test.go
+++ b/internal/database/database_test.go
@@ -36,7 +36,7 @@
 		log.Fatal(err)
 	}
 	var err error
-	testDB, err = Open("postgres", dbtest.DBConnURI(dbName))
+	testDB, err = Open("postgres", dbtest.DBConnURI(dbName), "test")
 	if err != nil {
 		log.Fatalf("Open: %v %[1]T", err)
 	}
diff --git a/internal/frontend/fetch.go b/internal/frontend/fetch.go
index 6686bb7..182ef5d 100644
--- a/internal/frontend/fetch.go
+++ b/internal/frontend/fetch.go
@@ -424,7 +424,7 @@
 // worker.FetchAndUpdateState that does not update module_version_states, so that
 // we don't have to import internal/worker here. It is not meant to be used
 // when running on AppEngine.
-func FetchAndUpdateState(ctx context.Context, modulePath, requestedVersion string, proxyClient *proxy.Client, sourceClient *source.Client, db *postgres.DB) (_ int, err error) {
+func FetchAndUpdateState(ctx context.Context, modulePath, requestedVersion string, proxyClient *proxy.Client, sourceClient *source.Client, db *postgres.DB, _ string) (_ int, err error) {
 	defer func() {
 		if err != nil {
 			log.Infof(ctx, "FetchAndUpdateState(%q, %q) completed with err: %v. ", modulePath, requestedVersion, err)
diff --git a/internal/frontend/server.go b/internal/frontend/server.go
index c85a855..acc5bd1 100644
--- a/internal/frontend/server.go
+++ b/internal/frontend/server.go
@@ -19,7 +19,6 @@
 
 	"github.com/go-redis/redis/v7"
 	"golang.org/x/pkgsite/internal"
-	"golang.org/x/pkgsite/internal/config"
 	"golang.org/x/pkgsite/internal/derrors"
 	"golang.org/x/pkgsite/internal/experiment"
 	"golang.org/x/pkgsite/internal/licenses"
@@ -41,6 +40,7 @@
 	templateDir          string
 	devMode              bool
 	errorPage            []byte
+	appVersionLabel      string
 
 	mu        sync.Mutex // Protects all fields below
 	templates map[string]*template.Template
@@ -55,6 +55,7 @@
 	StaticPath           string
 	ThirdPartyPath       string
 	DevMode              bool
+	AppVersionLabel      string
 }
 
 // NewServer creates a new Server for the given database and template directory.
@@ -75,6 +76,7 @@
 		devMode:              scfg.DevMode,
 		templates:            ts,
 		taskIDChangeInterval: scfg.TaskIDChangeInterval,
+		appVersionLabel:      scfg.AppVersionLabel,
 	}
 	errorPageBytes, err := s.renderErrorPage(context.Background(), http.StatusInternalServerError, "error.tmpl", nil)
 	if err != nil {
@@ -186,12 +188,13 @@
 
 // basePage contains fields shared by all pages when rendering templates.
 type basePage struct {
-	HTMLTitle   string
-	Query       string
-	Nonce       string
-	Experiments *experiment.Set
-	GodocURL    string
-	DevMode     bool
+	HTMLTitle       string
+	Query           string
+	Nonce           string
+	Experiments     *experiment.Set
+	GodocURL        string
+	DevMode         bool
+	AppVersionLabel string
 }
 
 // licensePolicyPage is used to generate the static license policy page.
@@ -216,12 +219,13 @@
 // newBasePage returns a base page for the given request and title.
 func (s *Server) newBasePage(r *http.Request, title string) basePage {
 	return basePage{
-		HTMLTitle:   title,
-		Query:       searchQuery(r),
-		Nonce:       middleware.NoncePlaceholder,
-		Experiments: experiment.FromContext(r.Context()),
-		GodocURL:    middleware.GodocURLPlaceholder,
-		DevMode:     s.devMode,
+		HTMLTitle:       title,
+		Query:           searchQuery(r),
+		Nonce:           middleware.NoncePlaceholder,
+		Experiments:     experiment.FromContext(r.Context()),
+		GodocURL:        middleware.GodocURLPlaceholder,
+		DevMode:         s.devMode,
+		AppVersionLabel: s.appVersionLabel,
 	}
 }
 
@@ -230,12 +234,6 @@
 	return "GTM-W8MVQXG"
 }
 
-// AppVersionLabel uniquely identifies the currently running binary. It can be
-// used for cache-busting query parameters.
-func (b basePage) AppVersionLabel() string {
-	return config.AppVersionLabel()
-}
-
 // errorPage contains fields for rendering a HTTP error page.
 type errorPage struct {
 	basePage
diff --git a/internal/frontend/server_test.go b/internal/frontend/server_test.go
index ee3f8c1..012548c 100644
--- a/internal/frontend/server_test.go
+++ b/internal/frontend/server_test.go
@@ -932,13 +932,14 @@
 		exps = append(exps, &internal.Experiment{Name: n, Rollout: 100})
 		set[n] = true
 	}
-	q := queue.NewInMemory(ctx, proxyClient, sourceClient, testDB, 1, FetchAndUpdateState, experiment.NewSet(set))
+	q := queue.NewInMemory(ctx, proxyClient, sourceClient, testDB, 1, FetchAndUpdateState, experiment.NewSet(set), "appVersionLabel")
 	s, err := NewServer(ServerConfig{
 		DataSource:           testDB,
 		Queue:                q,
 		TaskIDChangeInterval: 10 * time.Minute,
 		StaticPath:           "../../content/static",
 		ThirdPartyPath:       "../../third_party",
+		AppVersionLabel:      "",
 	})
 	if err != nil {
 		t.Fatal(err)
diff --git a/internal/postgres/benchmarks_test.go b/internal/postgres/benchmarks_test.go
index 05209e3..74fe266 100644
--- a/internal/postgres/benchmarks_test.go
+++ b/internal/postgres/benchmarks_test.go
@@ -38,7 +38,7 @@
 	if err != nil {
 		b.Fatal(err)
 	}
-	ddb, err := database.Open("pgx", cfg.DBConnInfo())
+	ddb, err := database.Open("pgx", cfg.DBConnInfo(), "bench")
 	if err != nil {
 		b.Fatal(err)
 	}
diff --git a/internal/postgres/test_helper.go b/internal/postgres/test_helper.go
index 378bb7a..5b3b0cd 100644
--- a/internal/postgres/test_helper.go
+++ b/internal/postgres/test_helper.go
@@ -92,7 +92,7 @@
 			return nil, fmt.Errorf("unfixable error migrating database: %v.\nConsider running ./devtools/drop_test_dbs.sh", err)
 		}
 	}
-	db, err := database.Open("postgres", dbtest.DBConnURI(dbName))
+	db, err := database.Open("postgres", dbtest.DBConnURI(dbName), "test")
 	if err != nil {
 		return nil, err
 	}
diff --git a/internal/queue/queue.go b/internal/queue/queue.go
index d775a63..cc13d9f 100644
--- a/internal/queue/queue.go
+++ b/internal/queue/queue.go
@@ -120,29 +120,31 @@
 	sourceClient *source.Client
 	db           *postgres.DB
 
-	queue       chan moduleVersion
-	sem         chan struct{}
-	experiments *experiment.Set
+	queue           chan moduleVersion
+	sem             chan struct{}
+	experiments     *experiment.Set
+	appVersionLabel string
 }
 
 // NewInMemory creates a new InMemory that asynchronously fetches
 // from proxyClient and stores in db. It uses workerCount parallelism to
 // execute these fetches.
 func NewInMemory(ctx context.Context, proxyClient *proxy.Client, sourceClient *source.Client, db *postgres.DB, workerCount int,
-	processFunc func(context.Context, string, string, *proxy.Client, *source.Client, *postgres.DB) (int, error), experiments *experiment.Set) *InMemory {
+	processFunc func(context.Context, string, string, *proxy.Client, *source.Client, *postgres.DB, string) (int, error), experiments *experiment.Set, appVersionLabel string) *InMemory {
 	q := &InMemory{
-		proxyClient:  proxyClient,
-		sourceClient: sourceClient,
-		db:           db,
-		queue:        make(chan moduleVersion, 1000),
-		sem:          make(chan struct{}, workerCount),
-		experiments:  experiments,
+		proxyClient:     proxyClient,
+		sourceClient:    sourceClient,
+		db:              db,
+		queue:           make(chan moduleVersion, 1000),
+		sem:             make(chan struct{}, workerCount),
+		experiments:     experiments,
+		appVersionLabel: appVersionLabel,
 	}
 	go q.process(ctx, processFunc)
 	return q
 }
 
-func (q *InMemory) process(ctx context.Context, processFunc func(context.Context, string, string, *proxy.Client, *source.Client, *postgres.DB) (int, error)) {
+func (q *InMemory) process(ctx context.Context, processFunc func(context.Context, string, string, *proxy.Client, *source.Client, *postgres.DB, string) (int, error)) {
 
 	for v := range q.queue {
 		select {
@@ -162,7 +164,7 @@
 			fetchCtx = experiment.NewContext(fetchCtx, q.experiments)
 			defer cancel()
 
-			if _, err := processFunc(fetchCtx, v.modulePath, v.version, q.proxyClient, q.sourceClient, q.db); err != nil {
+			if _, err := processFunc(fetchCtx, v.modulePath, v.version, q.proxyClient, q.sourceClient, q.db, q.appVersionLabel); err != nil {
 				log.Error(fetchCtx, err)
 			}
 		}(v)
diff --git a/internal/testing/integration/frontend_test.go b/internal/testing/integration/frontend_test.go
index 8866d26..236e3d9 100644
--- a/internal/testing/integration/frontend_test.go
+++ b/internal/testing/integration/frontend_test.go
@@ -157,6 +157,7 @@
 		TaskIDChangeInterval: 10 * time.Minute,
 		StaticPath:           "../../../content/static",
 		ThirdPartyPath:       "../../../third_party",
+		AppVersionLabel:      "",
 	})
 	if err != nil {
 		t.Fatal(err)
diff --git a/internal/testing/integration/integration_test.go b/internal/testing/integration/integration_test.go
index 4e15a8f..afb1327 100644
--- a/internal/testing/integration/integration_test.go
+++ b/internal/testing/integration/integration_test.go
@@ -77,7 +77,7 @@
 	// TODO: it would be better if InMemory made http requests
 	// back to worker, rather than calling fetch itself.
 	queue := queue.NewInMemory(ctx, proxyClient, source.NewClient(1*time.Second), testDB, 10,
-		worker.FetchAndUpdateState, nil)
+		worker.FetchAndUpdateState, nil, "test")
 
 	workerServer, err := worker.NewServer(&config.Config{}, worker.ServerConfig{
 		DB:                   testDB,
@@ -104,6 +104,7 @@
 		TaskIDChangeInterval: 10 * time.Minute,
 		StaticPath:           "../../../content/static",
 		ThirdPartyPath:       "../../../third_party",
+		AppVersionLabel:      "",
 	})
 	if err != nil {
 		t.Fatal(err)
diff --git a/internal/worker/fetch.go b/internal/worker/fetch.go
index cf837cb..546a97b 100644
--- a/internal/worker/fetch.go
+++ b/internal/worker/fetch.go
@@ -15,7 +15,6 @@
 	"go.opencensus.io/trace"
 	"golang.org/x/mod/semver"
 	"golang.org/x/pkgsite/internal"
-	"golang.org/x/pkgsite/internal/config"
 	"golang.org/x/pkgsite/internal/derrors"
 	"golang.org/x/pkgsite/internal/experiment"
 	"golang.org/x/pkgsite/internal/fetch"
@@ -45,7 +44,7 @@
 // the module_version_states table according to the result. It returns an HTTP
 // status code representing the result of the fetch operation, and a non-nil
 // error if this status code is not 200.
-func FetchAndUpdateState(ctx context.Context, modulePath, requestedVersion string, proxyClient *proxy.Client, sourceClient *source.Client, db *postgres.DB) (_ int, err error) {
+func FetchAndUpdateState(ctx context.Context, modulePath, requestedVersion string, proxyClient *proxy.Client, sourceClient *source.Client, db *postgres.DB, appVersionLabel string) (_ int, err error) {
 	defer derrors.Wrap(&err, "FetchAndUpdateState(%q, %q)", modulePath, requestedVersion)
 
 	tctx, span := trace.StartSpan(ctx, "FetchAndUpdateState")
@@ -75,7 +74,7 @@
 	// TODO(golang/go#39628): Split UpsertModuleVersionState into
 	// InsertModuleVersionState and UpdateModuleVersionState.
 	start := time.Now()
-	err = db.UpsertModuleVersionState(ctx, ft.ModulePath, ft.ResolvedVersion, config.AppVersionLabel(),
+	err = db.UpsertModuleVersionState(ctx, ft.ModulePath, ft.ResolvedVersion, appVersionLabel,
 		time.Time{}, ft.Status, ft.GoModPath, ft.Error, ft.PackageVersionStates)
 	ft.timings["db.UpsertModuleVersionState"] = time.Since(start)
 	if err != nil {
diff --git a/internal/worker/fetch_test.go b/internal/worker/fetch_test.go
index 73922f2..bf3fb91 100644
--- a/internal/worker/fetch_test.go
+++ b/internal/worker/fetch_test.go
@@ -90,7 +90,7 @@
 	}
 
 	// Fetch a module@version that the proxy serves successfully.
-	if _, err := FetchAndUpdateState(ctx, modulePath, version, proxyClient, sourceClient, testDB); err != nil {
+	if _, err := FetchAndUpdateState(ctx, modulePath, version, proxyClient, sourceClient, testDB, "appVersionLabel"); err != nil {
 		t.Fatal(err)
 	}
 
@@ -127,7 +127,7 @@
 	defer teardownProxy2()
 
 	// Now fetch it again.
-	if code, _ := FetchAndUpdateState(ctx, modulePath, version, proxyClient, sourceClient, testDB); code != http.StatusNotFound {
+	if code, _ := FetchAndUpdateState(ctx, modulePath, version, proxyClient, sourceClient, testDB, "appVersionLabel"); code != http.StatusNotFound {
 		t.Fatalf("FetchAndUpdateState(ctx, %q, %q, proxyClient, sourceClient, testDB): got code %d, want 404/410", modulePath, version, code)
 	}
 
@@ -188,7 +188,7 @@
 
 func checkModuleNotFound(t *testing.T, ctx context.Context, modulePath, version string, proxyClient *proxy.Client, sourceClient *source.Client, wantCode int, wantErr error) {
 	t.Helper()
-	code, err := FetchAndUpdateState(ctx, modulePath, version, proxyClient, sourceClient, testDB)
+	code, err := FetchAndUpdateState(ctx, modulePath, version, proxyClient, sourceClient, testDB, "appVersionLabel")
 	if code != wantCode || !errors.Is(err, wantErr) {
 		t.Fatalf("got %d, %v; want %d, Is(err, %v)", code, err, wantCode, wantErr)
 	}
@@ -228,7 +228,7 @@
 		want       = http.StatusNotFound
 	)
 
-	code, _ := FetchAndUpdateState(ctx, modulePath, version, proxyClient, sourceClient, testDB)
+	code, _ := FetchAndUpdateState(ctx, modulePath, version, proxyClient, sourceClient, testDB, "appVersionLabel")
 	if code != want {
 		t.Fatalf("got code %d, want %d", code, want)
 	}
@@ -262,7 +262,7 @@
 		want       = hasIncompletePackagesCode
 	)
 
-	code, err := FetchAndUpdateState(ctx, modulePath, version, proxyClient, sourceClient, testDB)
+	code, err := FetchAndUpdateState(ctx, modulePath, version, proxyClient, sourceClient, testDB, "appVersionLabel")
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -334,7 +334,7 @@
 	defer teardownProxy()
 	sourceClient := source.NewClient(sourceTimeout)
 
-	code, err := FetchAndUpdateState(ctx, modulePath, version, proxyClient, sourceClient, testDB)
+	code, err := FetchAndUpdateState(ctx, modulePath, version, proxyClient, sourceClient, testDB, "appVersionLabel")
 	wantErr := derrors.AlternativeModule
 	wantCode := derrors.ToHTTPStatus(wantErr)
 	if code != wantCode || !errors.Is(err, wantErr) {
@@ -402,7 +402,7 @@
 	defer teardownProxy()
 	sourceClient := source.NewClient(sourceTimeout)
 
-	if _, err := FetchAndUpdateState(ctx, modulePath, olderVersion, proxyClient, sourceClient, testDB); err != nil {
+	if _, err := FetchAndUpdateState(ctx, modulePath, olderVersion, proxyClient, sourceClient, testDB, "appVersionLabel"); err != nil {
 		t.Fatal(err)
 	}
 	gotModule, gotVersion, gotFound := postgres.GetFromSearchDocuments(ctx, t, testDB, modulePath+"/foo")
@@ -410,7 +410,7 @@
 		t.Fatalf("got (%q, %q, %t), want (%q, %q, true)", gotModule, gotVersion, gotFound, modulePath, olderVersion)
 	}
 
-	code, _ := FetchAndUpdateState(ctx, modulePath, mismatchVersion, proxyClient, sourceClient, testDB)
+	code, _ := FetchAndUpdateState(ctx, modulePath, mismatchVersion, proxyClient, sourceClient, testDB, "appVersionLabel")
 	if want := derrors.ToHTTPStatus(derrors.AlternativeModule); code != want {
 		t.Fatalf("got %d, want %d", code, want)
 	}
@@ -450,7 +450,7 @@
 	defer teardownProxy()
 	sourceClient := source.NewClient(sourceTimeout)
 
-	code, err := FetchAndUpdateState(ctx, modulePath, version, proxyClient, sourceClient, testDB)
+	code, err := FetchAndUpdateState(ctx, modulePath, version, proxyClient, sourceClient, testDB, "appVersionLabel")
 	if err != nil {
 		t.Fatalf("FetchAndUpdateState(%q, %q, %v, %v, %v): %v", modulePath, version, proxyClient, sourceClient, testDB, err)
 	}
@@ -518,7 +518,7 @@
 	defer teardownProxy()
 	sourceClient := source.NewClient(sourceTimeout)
 
-	code, err := FetchAndUpdateState(ctx, modulePath, version, proxyClient, sourceClient, testDB)
+	code, err := FetchAndUpdateState(ctx, modulePath, version, proxyClient, sourceClient, testDB, "appVersionLabel")
 	if err != nil {
 		t.Fatalf("FetchAndUpdateState(%q, %q, %v, %v, %v): %v", modulePath, version, proxyClient, sourceClient, testDB, err)
 	}
@@ -557,7 +557,7 @@
 	})
 	defer tearDown()
 	sourceClient := source.NewClient(sourceTimeout)
-	if _, err := FetchAndUpdateState(ctx, "my.mod/foo", "v1.0.0", proxyClient, sourceClient, testDB); err != nil {
+	if _, err := FetchAndUpdateState(ctx, "my.mod/foo", "v1.0.0", proxyClient, sourceClient, testDB, "appVersionLabel"); err != nil {
 		t.Fatalf("FetchAndUpdateState: %v", err)
 	}
 	pkg, err := testDB.LegacyGetPackage(ctx, "my.mod/foo", internal.UnknownModulePath, "v1.0.0")
@@ -607,7 +607,7 @@
 	})
 	defer teardownProxy()
 	sourceClient := source.NewClient(sourceTimeout)
-	if _, err := FetchAndUpdateState(ctx, modulePath, version, proxyClient, sourceClient, testDB); err != nil {
+	if _, err := FetchAndUpdateState(ctx, modulePath, version, proxyClient, sourceClient, testDB, "appVersionLabel"); err != nil {
 		t.Fatalf("FetchAndUpdateState(%q, %q, %v, %v, %v): %v", modulePath, version, proxyClient, sourceClient, testDB, err)
 	}
 
@@ -625,7 +625,7 @@
 	})
 	defer teardownProxy()
 
-	if _, err := FetchAndUpdateState(ctx, modulePath, version, proxyClient, sourceClient, testDB); err != nil {
+	if _, err := FetchAndUpdateState(ctx, modulePath, version, proxyClient, sourceClient, testDB, "appVersionLabel"); err != nil {
 		t.Fatalf("FetchAndUpdateState(%q, %q, %v, %v, %v): %v", modulePath, version, proxyClient, sourceClient, testDB, err)
 	}
 	want := &internal.LegacyVersionedPackage{
@@ -673,7 +673,7 @@
 		},
 	})
 	defer teardownProxy()
-	if _, err := FetchAndUpdateState(ctx, modulePath, version, proxyClient, sourceClient, testDB); !errors.Is(err, derrors.DBModuleInsertInvalid) {
+	if _, err := FetchAndUpdateState(ctx, modulePath, version, proxyClient, sourceClient, testDB, "appVersionLabel"); !errors.Is(err, derrors.DBModuleInsertInvalid) {
 		t.Fatalf("FetchAndUpdateState(%q, %q, %v, %v, %v): %v", modulePath, version, proxyClient, sourceClient, testDB, err)
 	}
 }
@@ -1049,7 +1049,7 @@
 
 			sourceClient := source.NewClient(sourceTimeout)
 
-			if _, err := FetchAndUpdateState(ctx, test.modulePath, test.version, proxyClient, sourceClient, testDB); err != nil {
+			if _, err := FetchAndUpdateState(ctx, test.modulePath, test.version, proxyClient, sourceClient, testDB, "appVersionLabel"); err != nil {
 				t.Fatalf("FetchAndUpdateState(%q, %q, %v, %v, %v): %v", test.modulePath, test.version, proxyClient, sourceClient, testDB, err)
 			}
 
@@ -1104,7 +1104,7 @@
 
 	ctx, cancel := context.WithTimeout(context.Background(), 0)
 	defer cancel()
-	_, err := FetchAndUpdateState(ctx, name, version, proxyClient, sourceClient, testDB)
+	_, err := FetchAndUpdateState(ctx, name, version, proxyClient, sourceClient, testDB, "appVersionLabel")
 	if err == nil || !strings.Contains(err.Error(), wantErrString) {
 		t.Fatalf("FetchAndUpdateState(%q, %q, %v, %v, %v) returned error %v, want error containing %q",
 			name, version, proxyClient, sourceClient, testDB, err, wantErrString)
diff --git a/internal/worker/server.go b/internal/worker/server.go
index 1203466..3d18cfa 100644
--- a/internal/worker/server.go
+++ b/internal/worker/server.go
@@ -239,7 +239,7 @@
 		return err.Error(), http.StatusBadRequest
 	}
 
-	code, err := FetchAndUpdateState(r.Context(), modulePath, version, s.proxyClient, s.sourceClient, s.db)
+	code, err := FetchAndUpdateState(r.Context(), modulePath, version, s.proxyClient, s.sourceClient, s.db, s.cfg.AppVersionLabel())
 	if err != nil {
 		return err.Error(), code
 	}
diff --git a/internal/worker/server_test.go b/internal/worker/server_test.go
index c227c78..372428a 100644
--- a/internal/worker/server_test.go
+++ b/internal/worker/server_test.go
@@ -162,7 +162,7 @@
 			defer postgres.ResetTestDB(testDB, t)
 
 			// Use 10 workers to have parallelism consistent with the worker binary.
-			q := queue.NewInMemory(ctx, proxyClient, sourceClient, testDB, 10, FetchAndUpdateState, nil)
+			q := queue.NewInMemory(ctx, proxyClient, sourceClient, testDB, 10, FetchAndUpdateState, nil, "")
 
 			s, err := NewServer(&config.Config{}, ServerConfig{
 				DB:                   testDB,