[gold] Preparing to remove datastore-backed ExpectationsStore

Notes to the reviewer:
 - PS 1-2 removes IssueExpStoreFactory and replace it with a method on
   ExpectationsStore.
 - PS 3+ fills out the IssueExpectations implementation on Firestore.
 - PS 3+ also makes the Firestore impl use eventbus like the DS
   version did

Bug: skia:9090
Change-Id: I32e90bb3d301164344fd9f3a99e50a8978a8a8fb
Reviewed-on: https://skia-review.googlesource.com/c/buildbot/+/218557
Reviewed-by: Joe Gregorio <jcgregorio@google.com>
Commit-Queue: Kevin Lubick <kjlubick@google.com>
diff --git a/golden/cmd/baseline_server/main.go b/golden/cmd/baseline_server/main.go
index 0541180..09205af 100644
--- a/golden/cmd/baseline_server/main.go
+++ b/golden/cmd/baseline_server/main.go
@@ -95,12 +95,12 @@
 	}
 
 	// Set up the cloud expectations store
-	expStore, issueExpStoreFactory, err := ds_expstore.New(ds.DS, evt)
+	expStore, err := ds_expstore.DeprecatedNew(ds.DS, evt)
 	if err != nil {
 		sklog.Fatalf("Unable to configure cloud expectations store: %s", err)
 	}
 
-	tryjobStore, err := tryjobstore.NewCloudTryjobStore(ds.DS, issueExpStoreFactory, evt)
+	tryjobStore, err := tryjobstore.NewCloudTryjobStore(ds.DS, evt)
 	if err != nil {
 		sklog.Fatalf("Unable to instantiate tryjob store: %s", err)
 	}
@@ -127,7 +127,7 @@
 	}
 
 	// Initialize the Baseliner instance from the values set above.
-	baseliner, err := gcs_baseliner.New(gsClient, expStore, issueExpStoreFactory, tryjobStore, vcs)
+	baseliner, err := gcs_baseliner.New(gsClient, expStore, tryjobStore, vcs)
 	if err != nil {
 		sklog.Fatalf("Error initializing baseliner: %s", err)
 	}
diff --git a/golden/cmd/sampler/main.go b/golden/cmd/sampler/main.go
index d6b2d36..ab04903 100644
--- a/golden/cmd/sampler/main.go
+++ b/golden/cmd/sampler/main.go
@@ -177,7 +177,7 @@
 	var expStore expstorage.ExpectationsStore
 	var err error
 
-	expStore, _, err = ds_expstore.New(ds.DS, evt)
+	expStore, err = ds_expstore.DeprecatedNew(ds.DS, evt)
 	if err != nil {
 		sklog.Fatalf("Unable to create cloud expectations store: %s", err)
 	}
diff --git a/golden/cmd/skiacorrectness/main.go b/golden/cmd/skiacorrectness/main.go
index 4d3531f..dec4fd8 100644
--- a/golden/cmd/skiacorrectness/main.go
+++ b/golden/cmd/skiacorrectness/main.go
@@ -361,41 +361,40 @@
 		}
 
 		// Set up the cloud expectations store
-		expStore, issueExpStoreFactory, err := ds_expstore.New(ds.DS, evt)
+		expStore, err := ds_expstore.DeprecatedNew(ds.DS, evt)
 		if err != nil {
 			sklog.Fatalf("Unable to configure cloud expectations store: %s", err)
 		}
 
-		tryjobStore, err := tryjobstore.NewCloudTryjobStore(ds.DS, issueExpStoreFactory, evt)
+		tryjobStore, err := tryjobstore.NewCloudTryjobStore(ds.DS, evt)
 		if err != nil {
 			sklog.Fatalf("Unable to instantiate tryjob store: %s", err)
 		}
-		tryjobMonitor := gerrit_tryjob_monitor.New(tryjobStore, expStore, issueExpStoreFactory, gerritAPI, *siteURL, evt, *authoritative)
+		tryjobMonitor := gerrit_tryjob_monitor.New(tryjobStore, expStore, gerritAPI, *siteURL, evt, *authoritative)
 
 		// Initialize the Baseliner instance from the values set above.
-		baseliner, err := gcs_baseliner.New(gsClient, expStore, issueExpStoreFactory, tryjobStore, vcs)
+		baseliner, err := gcs_baseliner.New(gsClient, expStore, tryjobStore, vcs)
 		if err != nil {
 			sklog.Fatalf("Error initializing baseliner: %s", err)
 		}
 
 		// Extract the site URL
 		storages := &storage.Storage{
-			DiffStore:            diffStore,
-			ExpectationsStore:    expStore,
-			IssueExpStoreFactory: issueExpStoreFactory,
-			TraceDB:              db,
-			MasterTileBuilder:    masterTileBuilder,
-			NCommits:             *nCommits,
-			EventBus:             evt,
-			TryjobStore:          tryjobStore,
-			TryjobMonitor:        tryjobMonitor,
-			GerritAPI:            gerritAPI,
-			GCSClient:            gsClient,
-			VCS:                  vcs,
-			IsAuthoritative:      *authoritative,
-			SiteURL:              *siteURL,
-			IsSparseTile:         *sparseInput,
-			Baseliner:            baseliner,
+			DiffStore:         diffStore,
+			ExpectationsStore: expStore,
+			TraceDB:           db,
+			MasterTileBuilder: masterTileBuilder,
+			NCommits:          *nCommits,
+			EventBus:          evt,
+			TryjobStore:       tryjobStore,
+			TryjobMonitor:     tryjobMonitor,
+			GerritAPI:         gerritAPI,
+			GCSClient:         gsClient,
+			VCS:               vcs,
+			IsAuthoritative:   *authoritative,
+			SiteURL:           *siteURL,
+			IsSparseTile:      *sparseInput,
+			Baseliner:         baseliner,
 		}
 
 		// Load the whitelist if there is one and disable querying for issues.
diff --git a/golden/go/baseline/gcs_baseliner/baseliner.go b/golden/go/baseline/gcs_baseliner/baseliner.go
index 1f07a32..624c7aa 100644
--- a/golden/go/baseline/gcs_baseliner/baseliner.go
+++ b/golden/go/baseline/gcs_baseliner/baseliner.go
@@ -37,11 +37,10 @@
 // GCS and retrieve them. Other packages use it to continuously write expectations to GCS
 // as they become available.
 type BaselinerImpl struct {
-	gStorageClient       storage.GCSClient
-	expectationsStore    expstorage.ExpectationsStore
-	issueExpStoreFactory expstorage.IssueExpStoreFactory
-	tryjobStore          tryjobstore.TryjobStore
-	vcs                  vcsinfo.VCS
+	gStorageClient    storage.GCSClient
+	expectationsStore expstorage.ExpectationsStore
+	tryjobStore       tryjobstore.TryjobStore
+	vcs               vcsinfo.VCS
 
 	// mutex protects lastWrittenBaselines, baselineCache and currentTile
 	mutex sync.RWMutex
@@ -62,7 +61,7 @@
 }
 
 // New creates a new instance of baseliner.Baseliner that interacts with baselines in GCS.
-func New(gStorageClient storage.GCSClient, expectationsStore expstorage.ExpectationsStore, issueExpStoreFactory expstorage.IssueExpStoreFactory, tryjobStore tryjobstore.TryjobStore, vcs vcsinfo.VCS) (*BaselinerImpl, error) {
+func New(gStorageClient storage.GCSClient, expectationsStore expstorage.ExpectationsStore, tryjobStore tryjobstore.TryjobStore, vcs vcsinfo.VCS) (*BaselinerImpl, error) {
 	c, err := lru.New(issueCacheSize)
 	if err != nil {
 		return nil, skerr.Fmt("Error allocating cache: %s", err)
@@ -71,7 +70,6 @@
 	return &BaselinerImpl{
 		gStorageClient:       gStorageClient,
 		expectationsStore:    expectationsStore,
-		issueExpStoreFactory: issueExpStoreFactory,
 		tryjobStore:          tryjobStore,
 		vcs:                  vcs,
 		issueBaselineCache:   c,
@@ -180,7 +178,7 @@
 
 // PushIssueBaseline implements the baseline.Baseliner interface.
 func (b *BaselinerImpl) PushIssueBaseline(issueID int64, tileInfo baseline.TileInfo, dCounter digest_counter.DigestCounter) error {
-	issueExpStore := b.issueExpStoreFactory(issueID)
+	issueExpStore := b.expectationsStore.ForIssue(issueID)
 	exp, err := issueExpStore.Get()
 	if err != nil {
 		return skerr.Fmt("Unable to get issue expectations: %s", err)
diff --git a/golden/go/baseline/gcs_baseliner/baseliner_test.go b/golden/go/baseline/gcs_baseliner/baseliner_test.go
index 6ed1fcb..d886fa0 100644
--- a/golden/go/baseline/gcs_baseliner/baseliner_test.go
+++ b/golden/go/baseline/gcs_baseliner/baseliner_test.go
@@ -26,7 +26,7 @@
 
 	mgs.On("ReadBaseline", testCommitHash, baseline.MasterBranch).Return(three_devices.MakeTestBaseline(), nil).Once()
 
-	baseliner, err := New(mgs, nil, nil, nil, nil)
+	baseliner, err := New(mgs, nil, nil, nil)
 	assert.NoError(t, err)
 
 	b, err := baseliner.FetchBaseline(testCommitHash, baseline.MasterBranch, false)
@@ -78,7 +78,7 @@
 	// are not live on master yet).
 	mgs.On("ReadBaseline", "", testIssueID).Return(additionalTriages, nil).Once()
 
-	baseliner, err := New(mgs, nil, nil, nil, nil)
+	baseliner, err := New(mgs, nil, nil, nil)
 	assert.NoError(t, err)
 
 	b, err := baseliner.FetchBaseline(testCommitHash, testIssueID, false)
@@ -124,7 +124,7 @@
 	// ReadBaseline should only be called once despite multiple requests below
 	mgs.On("ReadBaseline", testCommitHash, baseline.MasterBranch).Return(three_devices.MakeTestBaseline(), nil).Once()
 
-	baseliner, err := New(mgs, nil, nil, nil, nil)
+	baseliner, err := New(mgs, nil, nil, nil)
 	assert.NoError(t, err)
 
 	for i := 0; i < 10; i++ {
@@ -186,7 +186,7 @@
 		}
 	}).Return("gs://test-bucket/baselines/foo-baseline.json", nil).Times(3) // once per commit
 
-	baseliner, err := New(mgs, mes, nil, nil, nil)
+	baseliner, err := New(mgs, mes, nil, nil)
 	assert.NoError(t, err)
 
 	b, err := baseliner.PushMasterBaselines(mcs, "")
diff --git a/golden/go/bbstate/bbstate_test.go b/golden/go/bbstate/bbstate_test.go
index 4675417..e0dec3a 100644
--- a/golden/go/bbstate/bbstate_test.go
+++ b/golden/go/bbstate/bbstate_test.go
@@ -14,7 +14,6 @@
 	"go.skia.org/infra/go/httputils"
 	"go.skia.org/infra/go/sklog"
 	"go.skia.org/infra/go/testutils/unittest"
-	"go.skia.org/infra/golden/go/expstorage/ds_expstore"
 	"go.skia.org/infra/golden/go/tryjobstore"
 	gstorage "google.golang.org/api/storage/v1"
 )
@@ -49,9 +48,7 @@
 	assert.NoError(t, err)
 
 	evt := eventbus.New()
-	_, expStoreFactory, err := ds_expstore.New(ds.DS, evt)
-	assert.NoError(t, err)
-	tjStore, err := tryjobstore.NewCloudTryjobStore(dsClient, expStoreFactory, evt)
+	tjStore, err := tryjobstore.NewCloudTryjobStore(dsClient, evt)
 	assert.NoError(t, err)
 
 	gerritAPI, err := gerrit.NewGerrit(gerrit.GERRIT_SKIA_URL, "", httpClient)
diff --git a/golden/go/expstorage/ds_expstore/ds_expstore.go b/golden/go/expstorage/ds_expstore/ds_expstore.go
index fe226e7..d885e33 100644
--- a/golden/go/expstorage/ds_expstore/ds_expstore.go
+++ b/golden/go/expstorage/ds_expstore/ds_expstore.go
@@ -101,13 +101,13 @@
 	ExpectationsBlob *datastore.Key // key of the blob that stores expectations
 }
 
-// NewCloudExpectationsStore returns an ExpectationsStore implementation based on
+// DeprecatedNew returns an ExpectationsStore implementation based on
 // Cloud Datastore for the master branch and a factory to create ExpectationsStore
 // instances for Gerrit issues. The factory uses the same datastore client as the
 // master store.
-func New(client *datastore.Client, eventBus eventbus.EventBus) (*DSExpStore, expstorage.IssueExpStoreFactory, error) {
+func DeprecatedNew(client *datastore.Client, eventBus eventbus.EventBus) (*DSExpStore, error) {
 	if client == nil {
-		return nil, nil, sklog.FmtErrorf("Received nil for datastore client.")
+		return nil, sklog.FmtErrorf("Received nil for datastore client.")
 	}
 
 	// Create the instance for the master and set the target entities for the
@@ -119,7 +119,7 @@
 	blobStore := dsutil.NewBlobStore(client, ds.EXPECTATIONS_BLOB_ROOT, ds.EXPECTATIONS_BLOB)
 
 	store := &DSExpStore{
-		issueID:         expstorage.MasterIssueID,
+		issueID:         expstorage.MasterBranch,
 		changeKind:      ds.MASTER_EXP_CHANGE,
 		eventExpChange:  expstorage.EV_EXPSTORAGE_CHANGED,
 		globalEvent:     true,
@@ -131,34 +131,33 @@
 		blobStore:       blobStore,
 	}
 
-	// The factory allows to create an isolated ExpectationStore instance for the
-	// given issue.
-	factory := func(issueID int64) expstorage.ExpectationsStore {
-		summaryKey := ds.NewKey(ds.HELPER_RECENT_KEYS)
-		summaryKey.Name = fmt.Sprintf("expstorage-issue-%d", issueID)
-		expectationsKey := ds.NewKey(ds.EXPECTATIONS_BLOB_ROOT)
-		expectationsKey.Name = fmt.Sprintf("expstorage-expectations-issue-%d", issueID)
-		return &DSExpStore{
-			issueID:         issueID,
-			changeKind:      ds.TRYJOB_EXP_CHANGE,
-			eventExpChange:  expstorage.EV_TRYJOB_EXP_CHANGED,
-			globalEvent:     false,
-			client:          client,
-			eventBus:        eventBus,
-			summaryKey:      summaryKey,
-			expectationsKey: expectationsKey,
-			recentKeysList:  dsutil.NewRecentKeysList(client, summaryKey, dsutil.DefaultConsistencyDelta),
-			blobStore:       blobStore,
-		}
-	}
-
 	// Check the connection to the cloud datastore and if we could load the
 	// expectations successfully.
 	_, _, err := store.loadCurrentExpectations(nil)
 	if err != nil {
-		return nil, nil, sklog.FmtErrorf("Error in test call to the cloud datastore: %s", err)
+		return nil, sklog.FmtErrorf("Error in test call to the cloud datastore: %s", err)
 	}
-	return store, factory, nil
+	return store, nil
+}
+
+// ForIssue implements the ExpectationsStore interface.
+func (c *DSExpStore) ForIssue(issueID int64) expstorage.ExpectationsStore {
+	summaryKey := ds.NewKey(ds.HELPER_RECENT_KEYS)
+	summaryKey.Name = fmt.Sprintf("expstorage-issue-%d", issueID)
+	expectationsKey := ds.NewKey(ds.EXPECTATIONS_BLOB_ROOT)
+	expectationsKey.Name = fmt.Sprintf("expstorage-expectations-issue-%d", issueID)
+	return &DSExpStore{
+		issueID:         issueID,
+		changeKind:      ds.TRYJOB_EXP_CHANGE,
+		eventExpChange:  expstorage.EV_TRYJOB_EXP_CHANGED,
+		globalEvent:     false,
+		client:          c.client,
+		eventBus:        c.eventBus,
+		summaryKey:      summaryKey,
+		expectationsKey: expectationsKey,
+		recentKeysList:  dsutil.NewRecentKeysList(c.client, summaryKey, dsutil.DefaultConsistencyDelta),
+		blobStore:       c.blobStore,
+	}
 }
 
 // Get implements the ExpectationsStore interface.
diff --git a/golden/go/expstorage/ds_expstore/ds_expstore_test.go b/golden/go/expstorage/ds_expstore/ds_expstore_test.go
index b85b1b2..ac79fab 100644
--- a/golden/go/expstorage/ds_expstore/ds_expstore_test.go
+++ b/golden/go/expstorage/ds_expstore/ds_expstore_test.go
@@ -34,7 +34,7 @@
 
 	// Test the DS backed store for master.
 	masterEventBus := eventbus.New()
-	cloudStore, _, err := New(ds.DS, masterEventBus)
+	cloudStore, err := DeprecatedNew(ds.DS, masterEventBus)
 	assert.NoError(t, err)
 	testExpectationStore(t, cloudStore, masterEventBus, 0, expstorage.EV_EXPSTORAGE_CHANGED)
 	testCloudExpstoreClear(t, cloudStore)
@@ -64,10 +64,10 @@
 
 	// Test the expectation store for an individual issue.
 	masterEventBus := eventbus.New()
-	_, issueStoreFactory, err := New(ds.DS, masterEventBus)
+	e, err := DeprecatedNew(ds.DS, masterEventBus)
 	assert.NoError(t, err)
 	issueID := int64(1234567)
-	issueStore := issueStoreFactory(issueID)
+	issueStore := e.ForIssue(issueID)
 	testExpectationStore(t, issueStore, masterEventBus, issueID, expstorage.EV_TRYJOB_EXP_CHANGED)
 	testCloudExpstoreClear(t, issueStore.(*DSExpStore))
 }
diff --git a/golden/go/expstorage/fs_expstore/FIRESTORE.md b/golden/go/expstorage/fs_expstore/FIRESTORE.md
index 2218bb7..7ca66c8 100644
--- a/golden/go/expstorage/fs_expstore/FIRESTORE.md
+++ b/golden/go/expstorage/fs_expstore/FIRESTORE.md
@@ -11,12 +11,12 @@
 it is assumed to be Untriaged.
 
 There is the idea of the MasterExpectations, which is the Expectations belonging to the
-git branch "master". Additionally, there can be smaller BranchExpectations that belong
+git branch "master". Additionally, there can be smaller IssueExpectations that belong
 to a ChangeList (CL) and stay separate from the MasterExpectations until the CL lands.
 
 We'd like to be able to do the following:
 
-  - Store and retrieve Expectations (both MasterExpectations and BranchExpectations).
+  - Store and retrieve Expectations (both MasterExpectations and IssueExpectations).
   - Update the Label for a (Grouping, Digest).
   - Keep an audit record of what user updated the Label for a given (Grouping, Digest).
   - Undo a previous change.
@@ -82,7 +82,7 @@
 Documents with Issue==0 and assemble them together. The implementation will have an Expectations
 map in RAM that acts as a write-through cache.
 
-BranchExpectations will have their changed Expectations (essentially their delta from the
+IssueExpectations will have their changed Expectations (essentially their delta from the
 MasterExpectations) stored in the `expectations` Collection with nonzero
 Issue fields. When the tryjob monitor notes that a CL has landed, it can make a transaction
 to change all the Issue fields of the associated Documents in the `expectations` Collection to 0.
diff --git a/golden/go/expstorage/fs_expstore/fs_expstore.go b/golden/go/expstorage/fs_expstore/fs_expstore.go
index f19f393..80a0b56 100644
--- a/golden/go/expstorage/fs_expstore/fs_expstore.go
+++ b/golden/go/expstorage/fs_expstore/fs_expstore.go
@@ -6,11 +6,13 @@
 	"context"
 	"errors"
 	"fmt"
+	"strconv"
 	"sync"
 	"time"
 
 	"cloud.google.com/go/firestore"
 	"github.com/cenkalti/backoff"
+	"go.skia.org/infra/go/eventbus"
 	ifirestore "go.skia.org/infra/go/firestore"
 	"go.skia.org/infra/go/metrics2"
 	"go.skia.org/infra/go/skerr"
@@ -27,10 +29,6 @@
 	ReadWrite
 )
 
-const (
-	MasterBranch = int64(0)
-)
-
 var (
 	ReadOnlyErr = errors.New("expectationStore is in read-only mode")
 )
@@ -62,6 +60,16 @@
 	mode   AccessMode
 	issue  int64 // Gerrit or GitHub issue, or MasterBranch
 
+	// eventBus allows this Store to communicate with the outside world when
+	// expectations change.
+	eventBus eventbus.EventBus
+	// globalEvent keeps track whether we want to send events within this instance
+	// or on the global eventbus.
+	globalEvent bool
+	// eventExpChange keeps track of which event to fire when the expectations change.
+	// This will be for either the MasterExpectations or for an IssueExpectations.
+	eventExpChange string
+
 	// cacheMutex protects the write-through cache object.
 	cacheMutex sync.RWMutex
 	cache      types.Expectations
@@ -99,40 +107,99 @@
 	LabelAfter  types.Label    `firestore:"after"`
 }
 
-// New returns a new Store using the given firestore client. The issue param is used
-// to indicate if this Store is configured to read/write the baselines for a given CL
-// or if it is on MasterBranch.
-func New(client *ifirestore.Client, issue int64, mode AccessMode) *Store {
+// New returns a new Store using the given firestore client. The Store will track
+// MasterBranch- see ForIssue() for getting Stores that track ChangeLists.
+func New(client *ifirestore.Client, eventBus eventbus.EventBus, mode AccessMode) *Store {
 	return &Store{
-		client: client,
-		issue:  issue,
-		mode:   mode,
+		client:         client,
+		eventBus:       eventBus,
+		eventExpChange: expstorage.EV_EXPSTORAGE_CHANGED,
+		globalEvent:    true,
+		issue:          expstorage.MasterBranch,
+		mode:           mode,
+	}
+}
+
+// ForIssue implements the ExpectationsStore interface.
+func (f *Store) ForIssue(id int64) expstorage.ExpectationsStore {
+	if id == expstorage.MasterBranch {
+		// This should likely never happen, but if it does, lock the master branch
+		// down to read-only to prevent multiple threads writing to it at once.
+		// One of the core assumptions of this implementation is that there is only
+		// ever one process writing to the master branch.
+		return New(f.client, f.eventBus, ReadOnly)
+	}
+	return &Store{
+		client:         f.client,
+		eventBus:       f.eventBus,
+		eventExpChange: expstorage.EV_TRYJOB_EXP_CHANGED,
+		globalEvent:    false,
+		issue:          id,
+		mode:           f.mode,
 	}
 }
 
 // Get implements the ExpectationsStore interface.
 func (f *Store) Get() (types.Expectations, error) {
-	defer metrics2.FuncTimer().Stop()
-	f.cacheMutex.RLock()
-	defer f.cacheMutex.RUnlock()
-	if f.cache == nil {
-		c, err := f.loadExpectations()
+	if f.issue == expstorage.MasterBranch {
+		defer metrics2.NewTimer("gold_get_expectations", map[string]string{"master_branch": "true"}).Stop()
+		// On the initial load, we need to make sure we have write-access
+		// to the mutex. Otherwise, read access is sufficient.
+		if f.cache == nil {
+			f.cacheMutex.Lock()
+			defer f.cacheMutex.Unlock()
+		} else {
+			f.cacheMutex.RLock()
+			defer f.cacheMutex.RUnlock()
+		}
+		return f.getMasterExpectations(true)
+	}
+	defer metrics2.NewTimer("gold_get_expectations", map[string]string{"master_branch": "false"}).Stop()
+	return f.getIssueExpectations()
+}
+
+// getMasterExpectations returns an Expectations object which is safe to mutate
+// based on the current state. In cases where caching is fine (e.g. the MasterBranch).
+// If caching it is used, it is expected the caller has taken care of any mutex grabbing.
+func (f *Store) getMasterExpectations(useCache bool) (types.Expectations, error) {
+	if f.cache == nil || !useCache {
+		c, err := f.loadExpectations(expstorage.MasterBranch)
 		if err != nil {
-			return nil, skerr.Fmt("could not load expectations from firestore: %s", err)
+			return nil, skerr.Fmt("could not load master expectations from firestore: %s", err)
+		}
+		if !useCache {
+			return c, nil
 		}
 		f.cache = c
 	}
 	return f.cache.DeepCopy(), nil
 }
 
-// loadExpectations reads the entire Expectations from the expectationsCollection,
-// matching the configured branch.
-func (f *Store) loadExpectations() (types.Expectations, error) {
+// getIssueExpectations returns an Expectations object which is safe to mutate
+// that has all issue-specific Expectations applied to the master Expectations.
+// It fetches everything from firestore every time, as there could be multiple
+// readers and writers and thus caching isn't safe.
+func (f *Store) getIssueExpectations() (types.Expectations, error) {
+	masterExp, err := f.getMasterExpectations(false)
+	if err != nil {
+		return nil, skerr.Fmt("could not load master expectations: %s", err)
+	}
+	issueExp, err := f.loadExpectations(f.issue)
+	if err != nil {
+		return nil, skerr.Fmt("could not load expectations delta for issue %d from firestore: %s", f.issue, err)
+	}
+	masterExp.MergeExpectations(issueExp)
+	return masterExp, nil
+}
+
+// loadExpectations returns an Expectations object from the expectationsCollection,
+// with all Expectations belonging to the passed in issue (can be MasterBranch).
+func (f *Store) loadExpectations(issue int64) (types.Expectations, error) {
 	defer metrics2.FuncTimer().Stop()
 	e := types.Expectations{}
-	q := f.client.Collection(expectationsCollection).Where(issueCol, "==", MasterBranch)
+	q := f.client.Collection(expectationsCollection).Where(issueCol, "==", issue)
 	maxRetries := 3
-	err := f.client.IterDocs("loadExpectations", "", q, maxRetries, maxOperationTime, func(doc *firestore.DocumentSnapshot) error {
+	err := f.client.IterDocs("loadExpectations", strconv.FormatInt(issue, 10), q, maxRetries, maxOperationTime, func(doc *firestore.DocumentSnapshot) error {
 		if doc == nil {
 			return nil
 		}
@@ -170,6 +237,13 @@
 		return now, entries, changes
 	}()
 
+	if f.eventBus != nil {
+		f.eventBus.Publish(f.eventExpChange, &expstorage.EventExpectationChange{
+			TestChanges: newExp,
+			IssueID:     f.issue,
+		}, f.globalEvent)
+	}
+
 	// Nothing to add
 	if len(entries) == 0 {
 		return nil
@@ -272,7 +346,7 @@
 
 // QueryLog implements the ExpectationsStore interface.
 func (f *Store) QueryLog(ctx context.Context, offset, size int, details bool) ([]expstorage.TriageLogEntry, int, error) {
-	if offset <= 0 || size <= 0 {
+	if offset < 0 || size <= 0 {
 		return nil, 0, fmt.Errorf("offset: %d and size: %d must be positive", offset, size)
 	}
 	tags := map[string]string{
@@ -285,7 +359,7 @@
 
 	// Fetch the records, which have everything except the details.
 	q := f.client.Collection(triageRecordsCollection).OrderBy(tsCol, firestore.Desc).Offset(offset).Limit(size)
-	q = q.Where(issueCol, "==", MasterBranch).Where(committedCol, "==", true)
+	q = q.Where(issueCol, "==", f.issue).Where(committedCol, "==", true)
 	var rv []expstorage.TriageLogEntry
 	d := fmt.Sprintf("offset: %d, size %d", offset, size)
 	err := f.client.IterDocs("query_log", d, q, 3, maxOperationTime, func(doc *firestore.DocumentSnapshot) error {
diff --git a/golden/go/expstorage/fs_expstore/fs_expstore_test.go b/golden/go/expstorage/fs_expstore/fs_expstore_test.go
index 71b89d6..61c07d5 100644
--- a/golden/go/expstorage/fs_expstore/fs_expstore_test.go
+++ b/golden/go/expstorage/fs_expstore/fs_expstore_test.go
@@ -10,6 +10,7 @@
 
 	"github.com/google/uuid"
 	assert "github.com/stretchr/testify/require"
+	"go.skia.org/infra/go/eventbus/mocks"
 	"go.skia.org/infra/go/firestore"
 	"go.skia.org/infra/go/testutils/unittest"
 	"go.skia.org/infra/golden/go/expstorage"
@@ -29,7 +30,7 @@
 
 	c := getTestFirestoreInstance(t)
 
-	f := New(c, MasterBranch, ReadWrite)
+	f := New(c, nil, ReadWrite)
 
 	// Brand new instance should have no expectations
 	e, err := f.Get()
@@ -73,7 +74,7 @@
 
 	// Make sure that if we create a new view, we can read the results
 	// from the table to make the expectations
-	fr := New(c, MasterBranch, ReadOnly)
+	fr := New(c, nil, ReadOnly)
 	e, err = fr.Get()
 	assert.NoError(t, err)
 	assert.Equal(t, expected, e)
@@ -87,7 +88,7 @@
 
 	c := getTestFirestoreInstance(t)
 
-	f := New(c, MasterBranch, ReadWrite)
+	f := New(c, nil, ReadWrite)
 
 	type entry struct {
 		Grouping types.TestName
@@ -171,7 +172,7 @@
 
 	c := getTestFirestoreInstance(t)
 
-	f := New(c, MasterBranch, ReadWrite)
+	f := New(c, nil, ReadWrite)
 
 	// Write the expectations in two, non-overlapping blocks.
 	exp1 := makeBigExpectations(0, 16)
@@ -203,7 +204,7 @@
 
 	// Make sure that if we create a new view, we can read the results
 	// from the table to make the expectations
-	fr := New(c, MasterBranch, ReadOnly)
+	fr := New(c, nil, ReadOnly)
 	e, err = fr.Get()
 	assert.NoError(t, err)
 	assert.Equal(t, expected, e)
@@ -213,7 +214,7 @@
 func TestReadOnly(t *testing.T) {
 	unittest.SmallTest(t)
 
-	f := New(nil, MasterBranch, ReadOnly)
+	f := New(nil, nil, ReadOnly)
 
 	err := f.AddChange(context.Background(), types.Expectations{
 		data.AlphaTest: {
@@ -230,11 +231,11 @@
 	unittest.RequiresFirestoreEmulator(t)
 
 	c := getTestFirestoreInstance(t)
-	f := New(c, MasterBranch, ReadWrite)
-	ctx := context.Background()
+	f := New(c, nil, ReadWrite)
 
 	fillWith4Entries(t, f)
 
+	ctx := context.Background()
 	entries, n, err := f.QueryLog(ctx, 0, 100, false)
 	assert.NoError(t, err)
 	assert.Equal(t, 4, n) // 4 operations
@@ -306,11 +307,11 @@
 	unittest.RequiresFirestoreEmulator(t)
 
 	c := getTestFirestoreInstance(t)
-	f := New(c, MasterBranch, ReadWrite)
-	ctx := context.Background()
+	f := New(c, nil, ReadWrite)
 
 	fillWith4Entries(t, f)
 
+	ctx := context.Background()
 	entries, n, err := f.QueryLog(ctx, 0, 100, true)
 	assert.NoError(t, err)
 	assert.Equal(t, 4, n) // 4 operations
@@ -356,10 +357,11 @@
 	unittest.RequiresFirestoreEmulator(t)
 
 	c := getTestFirestoreInstance(t)
-	f := New(c, MasterBranch, ReadWrite)
-	ctx := context.Background()
+	f := New(c, nil, ReadWrite)
 
 	fillWith4Entries(t, f)
+
+	ctx := context.Background()
 	entries, n, err := f.QueryLog(ctx, 0, 4, false)
 	assert.NoError(t, err)
 	assert.Equal(t, 4, n)
@@ -401,7 +403,7 @@
 
 	// Make sure that if we create a new view, we can read the results
 	// from the table to make the expectations
-	fr := New(c, MasterBranch, ReadOnly)
+	fr := New(c, nil, ReadOnly)
 	exp, err = fr.Get()
 	assert.NoError(t, err)
 	assert.Equal(t, expected, exp)
@@ -413,44 +415,299 @@
 	unittest.RequiresFirestoreEmulator(t)
 
 	c := getTestFirestoreInstance(t)
-	f := New(c, MasterBranch, ReadWrite)
-	ctx := context.Background()
+	f := New(c, nil, ReadWrite)
 
-	_, err := f.UndoChange(ctx, "doesnotexist", "userTwo")
+	_, err := f.UndoChange(context.Background(), "doesnotexist", "userTwo")
 	assert.Error(t, err)
 	assert.Contains(t, err.Error(), "not find change")
 }
 
-// TODO(kjlubick): implement tests for branch expectations.
-// func TestBranchExpectationsGet(t *testing.T) {
-// 	unittest.ManualTest(t)
-// 	unittest.RequiresFirestoreEmulator(t)
+// TestEventBusAddMaster makes sure proper eventbus signals are sent
+// when changes are made to the master branch.
+func TestEventBusAddMaster(t *testing.T) {
+	unittest.ManualTest(t)
+	unittest.RequiresFirestoreEmulator(t)
 
-// 	c := getTestFirestoreInstance(t)
-// 	m := New(c, MasterBranch, ReadWrite)
-// 	b := New(c, 117, ReadWrite) // arbitrary branch id
-// 	ctx := context.Background()
+	meb := &mocks.EventBus{}
+	defer meb.AssertExpectations(t)
 
-// }
+	c := getTestFirestoreInstance(t)
+	f := New(c, meb, ReadWrite)
 
-// fillWith4Entries fills a given Store with 4 triaged records of a few digests.
-func fillWith4Entries(t *testing.T, f *Store) {
-	assert.NoError(t, f.AddChange(context.Background(), types.Expectations{
+	change1 := types.Expectations{
+		data.AlphaTest: {
+			data.AlphaGood1Digest: types.POSITIVE,
+		},
+	}
+	change2 := types.Expectations{
+		data.AlphaTest: {
+			data.AlphaBad1Digest: types.NEGATIVE,
+		},
+		data.BetaTest: {
+			data.BetaGood1Digest: types.POSITIVE,
+		},
+	}
+
+	meb.On("Publish", expstorage.EV_EXPSTORAGE_CHANGED, &expstorage.EventExpectationChange{
+		TestChanges: change1,
+		IssueID:     expstorage.MasterBranch,
+	}, /*global=*/ true).Once()
+	meb.On("Publish", expstorage.EV_EXPSTORAGE_CHANGED, &expstorage.EventExpectationChange{
+		TestChanges: change2,
+		IssueID:     expstorage.MasterBranch,
+	}, /*global=*/ true).Once()
+
+	ctx := context.Background()
+	assert.NoError(t, f.AddChange(ctx, change1, userOne))
+	assert.NoError(t, f.AddChange(ctx, change2, userTwo))
+}
+
+// TestEventBusAddIssue makes sure proper eventbus signals are sent
+// when changes are made to an IssueExpectations.
+func TestEventBusAddIssue(t *testing.T) {
+	unittest.ManualTest(t)
+	unittest.RequiresFirestoreEmulator(t)
+
+	meb := &mocks.EventBus{}
+	defer meb.AssertExpectations(t)
+
+	c := getTestFirestoreInstance(t)
+	e := New(c, meb, ReadWrite)
+	issue := int64(117)
+	f := e.ForIssue(issue) // arbitrary issue
+
+	change1 := types.Expectations{
+		data.AlphaTest: {
+			data.AlphaGood1Digest: types.POSITIVE,
+		},
+	}
+	change2 := types.Expectations{
+		data.AlphaTest: {
+			data.AlphaBad1Digest: types.NEGATIVE,
+		},
+		data.BetaTest: {
+			data.BetaGood1Digest: types.POSITIVE,
+		},
+	}
+
+	meb.On("Publish", expstorage.EV_TRYJOB_EXP_CHANGED, &expstorage.EventExpectationChange{
+		TestChanges: change1,
+		IssueID:     issue,
+	}, /*global=*/ false).Once()
+	meb.On("Publish", expstorage.EV_TRYJOB_EXP_CHANGED, &expstorage.EventExpectationChange{
+		TestChanges: change2,
+		IssueID:     issue,
+	}, /*global=*/ false).Once()
+
+	ctx := context.Background()
+	assert.NoError(t, f.AddChange(ctx, change1, userOne))
+	assert.NoError(t, f.AddChange(ctx, change2, userTwo))
+}
+
+// TestEventBusUndo tests that eventbus signals are properly sent during Undo.
+func TestEventBusUndo(t *testing.T) {
+	unittest.ManualTest(t)
+	unittest.RequiresFirestoreEmulator(t)
+
+	meb := &mocks.EventBus{}
+	defer meb.AssertExpectations(t)
+
+	c := getTestFirestoreInstance(t)
+	f := New(c, meb, ReadWrite)
+
+	change := types.Expectations{
 		data.AlphaTest: {
 			data.AlphaGood1Digest: types.NEGATIVE,
 		},
-	}, userOne))
-	assert.NoError(t, f.AddChange(context.Background(), types.Expectations{
+	}
+	expectedUndo := types.Expectations{
 		data.AlphaTest: {
-			data.AlphaGood1Digest: types.POSITIVE, // overwrites previous value
+			data.AlphaGood1Digest: types.UNTRIAGED,
+		},
+	}
+
+	meb.On("Publish", expstorage.EV_EXPSTORAGE_CHANGED, &expstorage.EventExpectationChange{
+		TestChanges: change,
+		IssueID:     expstorage.MasterBranch,
+	}, /*global=*/ true).Once()
+	meb.On("Publish", expstorage.EV_EXPSTORAGE_CHANGED, &expstorage.EventExpectationChange{
+		TestChanges: expectedUndo,
+		IssueID:     expstorage.MasterBranch,
+	}, /*global=*/ true).Once()
+
+	ctx := context.Background()
+	assert.NoError(t, f.AddChange(ctx, change, userOne))
+
+	entries, n, err := f.QueryLog(ctx, 0, 1, false)
+	assert.NoError(t, err)
+	assert.Equal(t, 1, n)
+
+	exp, err := f.UndoChange(ctx, entries[0].ID, userOne)
+	assert.NoError(t, err)
+	assert.Equal(t, expectedUndo, exp)
+}
+
+// TestIssueExpectationsAddGet tests the separation of the MasterExpectations
+// and the IssueExpectations. It starts with a shared history, then
+// adds some expectations to both, before asserting that they are properly dealt
+// with. Specifically, the IssueExpectations should be applied as a delta to
+// the MasterExpectations.
+func TestIssueExpectationsAddGet(t *testing.T) {
+	unittest.ManualTest(t)
+	unittest.RequiresFirestoreEmulator(t)
+
+	c := getTestFirestoreInstance(t)
+	mb := New(c, nil, ReadWrite)
+
+	ctx := context.Background()
+	assert.NoError(t, mb.AddChange(ctx, types.Expectations{
+		data.AlphaTest: {
+			data.AlphaGood1Digest: types.NEGATIVE,
 		},
 	}, userTwo))
-	assert.NoError(t, f.AddChange(context.Background(), types.Expectations{
+
+	ib := mb.ForIssue(117) // arbitrary issue id
+
+	masterE, err := mb.Get()
+	assert.NoError(t, err)
+	issueE, err := ib.Get()
+	assert.NoError(t, err)
+	assert.Equal(t, masterE, issueE)
+
+	// Add to the IssueExpectations
+	assert.NoError(t, ib.AddChange(ctx, types.Expectations{
+		data.AlphaTest: {
+			data.AlphaGood1Digest: types.POSITIVE, // overwrites previous
+		},
 		data.BetaTest: {
 			data.BetaGood1Digest: types.POSITIVE,
 		},
 	}, userOne))
-	assert.NoError(t, f.AddChange(context.Background(), types.Expectations{
+
+	// Add to the MasterExpectations
+	assert.NoError(t, mb.AddChange(ctx, types.Expectations{
+		data.AlphaTest: {
+			data.AlphaBad1Digest: types.NEGATIVE,
+		},
+	}, userOne))
+
+	masterE, err = mb.Get()
+	assert.NoError(t, err)
+	issueE, err = ib.Get()
+	assert.NoError(t, err)
+
+	// Make sure the IssueExpectations did not leak to the MasterExpectations
+	assert.Equal(t, types.Expectations{
+		data.AlphaTest: {
+			data.AlphaGood1Digest: types.NEGATIVE,
+			data.AlphaBad1Digest:  types.NEGATIVE,
+		},
+	}, masterE)
+
+	// Make sure the IssueExpectations are applied on top of the updated
+	// MasterExpectations.
+	assert.Equal(t, types.Expectations{
+		data.AlphaTest: {
+			data.AlphaGood1Digest: types.POSITIVE,
+			data.AlphaBad1Digest:  types.NEGATIVE,
+		},
+		data.BetaTest: {
+			data.BetaGood1Digest: types.POSITIVE,
+		},
+	}, issueE)
+}
+
+// TestIssueExpectationsQueryLog makes sure the QueryLogs interacts
+// with the IssueExpectations as expected. Which is to say, the two
+// logs are separate.
+func TestIssueExpectationsQueryLog(t *testing.T) {
+	unittest.ManualTest(t)
+	unittest.RequiresFirestoreEmulator(t)
+
+	c := getTestFirestoreInstance(t)
+	mb := New(c, nil, ReadWrite)
+
+	ctx := context.Background()
+	assert.NoError(t, mb.AddChange(ctx, types.Expectations{
+		data.AlphaTest: {
+			data.AlphaGood1Digest: types.POSITIVE,
+		},
+	}, userTwo))
+
+	ib := mb.ForIssue(117) // arbitrary issue id
+
+	assert.NoError(t, ib.AddChange(ctx, types.Expectations{
+		data.BetaTest: {
+			data.BetaGood1Digest: types.POSITIVE,
+		},
+	}, userOne))
+
+	// Make sure the master logs are separate from the issue logs.
+	// request up to 10 to make sure we would get the issue
+	// change (if the filtering was wrong).
+	entries, n, err := mb.QueryLog(ctx, 0, 10, true)
+	assert.NoError(t, err)
+	assert.Equal(t, 1, n)
+
+	now := time.Now()
+	normalizeEntries(t, now, entries)
+	assert.Equal(t, expstorage.TriageLogEntry{
+		ID:          "was_random_0",
+		Name:        userTwo,
+		TS:          now.Unix(),
+		ChangeCount: 1,
+		Details: []expstorage.TriageDetail{
+			{
+				TestName: data.AlphaTest,
+				Digest:   data.AlphaGood1Digest,
+				Label:    types.POSITIVE.String(),
+			},
+		},
+	}, entries[0])
+
+	// Make sure the issue logs are separate from the master logs.
+	// Unlike when getting the expectations, the issue logs are
+	// *only* those logs that affected this issue. Not, for example,
+	// all the master logs with the issue logs tacked on.
+	entries, n, err = ib.QueryLog(ctx, 0, 10, true)
+	assert.NoError(t, err)
+	assert.Equal(t, 1, n) // only one change on this branch
+
+	normalizeEntries(t, now, entries)
+	assert.Equal(t, expstorage.TriageLogEntry{
+		ID:          "was_random_0",
+		Name:        userOne,
+		TS:          now.Unix(),
+		ChangeCount: 1,
+		Details: []expstorage.TriageDetail{
+			{
+				TestName: data.BetaTest,
+				Digest:   data.BetaGood1Digest,
+				Label:    types.POSITIVE.String(),
+			},
+		},
+	}, entries[0])
+}
+
+// fillWith4Entries fills a given Store with 4 triaged records of a few digests.
+func fillWith4Entries(t *testing.T, f *Store) {
+	ctx := context.Background()
+	assert.NoError(t, f.AddChange(ctx, types.Expectations{
+		data.AlphaTest: {
+			data.AlphaGood1Digest: types.NEGATIVE,
+		},
+	}, userOne))
+	assert.NoError(t, f.AddChange(ctx, types.Expectations{
+		data.AlphaTest: {
+			data.AlphaGood1Digest: types.POSITIVE, // overwrites previous value
+		},
+	}, userTwo))
+	assert.NoError(t, f.AddChange(ctx, types.Expectations{
+		data.BetaTest: {
+			data.BetaGood1Digest: types.POSITIVE,
+		},
+	}, userOne))
+	assert.NoError(t, f.AddChange(ctx, types.Expectations{
 		data.AlphaTest: {
 			data.AlphaBad1Digest: types.NEGATIVE,
 		},
diff --git a/golden/go/expstorage/mem_expstore/mem_expstore.go b/golden/go/expstorage/mem_expstore/mem_expstore.go
index f603d49..1143963 100644
--- a/golden/go/expstorage/mem_expstore/mem_expstore.go
+++ b/golden/go/expstorage/mem_expstore/mem_expstore.go
@@ -30,6 +30,12 @@
 	return ret
 }
 
+// See ExpectationsStore interface.
+func (m *MemExpectationsStore) ForIssue(id int64) expstorage.ExpectationsStore {
+	sklog.Fatal("MemExpectation store does not support ForIssue.")
+	return nil
+}
+
 // Get fulfills the ExpectationsStore interface.
 func (m *MemExpectationsStore) Get() (types.Expectations, error) {
 	m.mutex.RLock()
@@ -47,7 +53,7 @@
 	if m.eventBus != nil {
 		m.eventBus.Publish(expstorage.EV_EXPSTORAGE_CHANGED, &expstorage.EventExpectationChange{
 			TestChanges: changedTests,
-			IssueID:     expstorage.MasterIssueID,
+			IssueID:     expstorage.MasterBranch,
 		}, true)
 	}
 
@@ -75,7 +81,7 @@
 	if m.eventBus != nil {
 		m.eventBus.Publish(expstorage.EV_EXPSTORAGE_CHANGED, &expstorage.EventExpectationChange{
 			TestChanges: changedDigests,
-			IssueID:     expstorage.MasterIssueID,
+			IssueID:     expstorage.MasterBranch,
 		}, true)
 	}
 
diff --git a/golden/go/expstorage/types.go b/golden/go/expstorage/types.go
index c540458..7060133 100644
--- a/golden/go/expstorage/types.go
+++ b/golden/go/expstorage/types.go
@@ -18,9 +18,9 @@
 	// for an issue change. It sends an instance of *TryjobExpChange.
 	EV_TRYJOB_EXP_CHANGED = "expstorage:tryjob-exp-change"
 
-	// MasterIssueID is the value used for IssueID when we dealing with the
-	// master branch. Any IssueID < 0 should be ignored.
-	MasterIssueID = -1
+	// MasterBranch is the value used for IssueID when we dealing with the
+	// master branch. Any other IssueID < 0 should be ignored.
+	MasterBranch = int64(-1)
 )
 
 func init() {
@@ -53,6 +53,12 @@
 	// undone. The expectations returned are the expectations that were changed,
 	// with the newly reverted values.
 	UndoChange(ctx context.Context, changeID, userID string) (types.Expectations, error)
+
+	// Returns a new ExpectationStore that will deal with the Expectations for an issue
+	// with the given id (aka an IssueExpectations). Any expectations sent to the returned
+	// ExpectationStore will be kept separate from the master branch.
+	// This issue id is the Gerrit id or GitHub id.
+	ForIssue(id int64) ExpectationsStore
 }
 
 // TriageDetails represents one changed digest and the label that was
@@ -79,6 +85,3 @@
 	IssueID     int64
 	TestChanges types.Expectations
 }
-
-// IssueExpStoreFactory creates an ExpectationsStore instance for the given issue id.
-type IssueExpStoreFactory func(issueID int64) ExpectationsStore
diff --git a/golden/go/goldingestion/tryjob_ingestion.go b/golden/go/goldingestion/tryjob_ingestion.go
index f15ddca..85dbd4d 100644
--- a/golden/go/goldingestion/tryjob_ingestion.go
+++ b/golden/go/goldingestion/tryjob_ingestion.go
@@ -25,7 +25,6 @@
 	"go.skia.org/infra/go/vcsinfo"
 	"go.skia.org/infra/golden/go/bbstate"
 	"go.skia.org/infra/golden/go/config"
-	"go.skia.org/infra/golden/go/expstorage/ds_expstore"
 	"go.skia.org/infra/golden/go/tryjobstore"
 	"go.skia.org/infra/golden/go/types"
 	gstorage "google.golang.org/api/storage/v1"
@@ -95,14 +94,8 @@
 	// bot uploads results. Currently only applies to the Skia repo.
 	cfgFile := config.ExtraParams[CONFIG_JOB_CFG_FILE]
 
-	_, expStoreFactory, err := ds_expstore.New(ds.DS, eventBus)
-	if err != nil {
-		return nil, sklog.FmtErrorf("Unable to create cloud expectations store: %s", err)
-	}
-	sklog.Infof("Cloud Expectations Store created")
-
 	// Create the cloud tryjob store.
-	tryjobStore, err := tryjobstore.NewCloudTryjobStore(ds.DS, expStoreFactory, eventBus)
+	tryjobStore, err := tryjobstore.NewCloudTryjobStore(ds.DS, eventBus)
 	if err != nil {
 		return nil, fmt.Errorf("Error creating tryjob store: %s", err)
 	}
diff --git a/golden/go/goldingestion/tryjob_ingestion_test.go b/golden/go/goldingestion/tryjob_ingestion_test.go
index b1654e7..689366f 100644
--- a/golden/go/goldingestion/tryjob_ingestion_test.go
+++ b/golden/go/goldingestion/tryjob_ingestion_test.go
@@ -15,7 +15,6 @@
 	"go.skia.org/infra/go/testutils/unittest"
 	"go.skia.org/infra/go/util"
 	"go.skia.org/infra/go/vcsinfo"
-	"go.skia.org/infra/golden/go/expstorage/ds_expstore"
 	"go.skia.org/infra/golden/go/tryjobstore"
 )
 
@@ -75,9 +74,7 @@
 
 	// Set up the TryjobStore.
 	eventBus := eventbus.New()
-	_, expStoreFactory, err := ds_expstore.New(ds.DS, eventBus)
-	assert.NoError(t, err)
-	tryjobStore, err := tryjobstore.NewCloudTryjobStore(ds.DS, expStoreFactory, eventBus)
+	tryjobStore, err := tryjobstore.NewCloudTryjobStore(ds.DS, eventBus)
 	assert.NoError(t, err)
 
 	// Map the path of the file to it's content
diff --git a/golden/go/mocks/ExpectationsStore.go b/golden/go/mocks/ExpectationsStore.go
index b71e66c..4baa29d 100644
--- a/golden/go/mocks/ExpectationsStore.go
+++ b/golden/go/mocks/ExpectationsStore.go
@@ -26,6 +26,22 @@
 	return r0
 }
 
+// ForIssue provides a mock function with given fields: id
+func (_m *ExpectationsStore) ForIssue(id int64) expstorage.ExpectationsStore {
+	ret := _m.Called(id)
+
+	var r0 expstorage.ExpectationsStore
+	if rf, ok := ret.Get(0).(func(int64) expstorage.ExpectationsStore); ok {
+		r0 = rf(id)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).(expstorage.ExpectationsStore)
+		}
+	}
+
+	return r0
+}
+
 // Get provides a mock function with given fields:
 func (_m *ExpectationsStore) Get() (types.Expectations, error) {
 	ret := _m.Called()
diff --git a/golden/go/search/new_search.go b/golden/go/search/new_search.go
index eb876fd..4cf8891 100644
--- a/golden/go/search/new_search.go
+++ b/golden/go/search/new_search.go
@@ -275,7 +275,7 @@
 	ret := make(ExpSlice, 0, 2)
 
 	if (q != nil) && (q.Issue > 0) {
-		issueExpStore := s.storages.IssueExpStoreFactory(q.Issue)
+		issueExpStore := s.storages.ExpectationsStore.ForIssue(q.Issue)
 		tjExp, err := issueExpStore.Get()
 		if err != nil {
 			return nil, sklog.FmtErrorf("Unable to load expectations for issue %d from tryjobstore: %s", q.Issue, err)
diff --git a/golden/go/search/utils_test.go b/golden/go/search/utils_test.go
index ed1f38b..e03c24c 100644
--- a/golden/go/search/utils_test.go
+++ b/golden/go/search/utils_test.go
@@ -154,7 +154,7 @@
 	tileBuilder := mocks.NewMockTileBuilderFromTile(t, sample.Tile)
 	eventBus := eventbus.New()
 
-	baseliner, err := gcs_baseliner.New(nil, mes, nil, nil, nil)
+	baseliner, err := gcs_baseliner.New(nil, mes, nil, nil)
 	assert.NoError(t, err)
 
 	storages := &storage.Storage{
diff --git a/golden/go/storage/storage.go b/golden/go/storage/storage.go
index af72248..6d27d30 100644
--- a/golden/go/storage/storage.go
+++ b/golden/go/storage/storage.go
@@ -45,22 +45,21 @@
 // Storage is a container struct for the various storage objects we are using.
 // It is intended to reduce parameter lists as we pass around storage objects.
 type Storage struct {
-	DiffStore            diff.DiffStore
-	ExpectationsStore    expstorage.ExpectationsStore
-	IssueExpStoreFactory expstorage.IssueExpStoreFactory
-	IgnoreStore          ignore.IgnoreStore
-	TraceDB              tracedb.DB
-	MasterTileBuilder    TileSource
-	EventBus             eventbus.EventBus
-	TryjobStore          tryjobstore.TryjobStore
-	TryjobMonitor        tryjobs.TryjobMonitor
-	GerritAPI            gerrit.GerritInterface
-	GCSClient            GCSClient
-	Baseliner            baseline.Baseliner
-	VCS                  vcsinfo.VCS
-	WhiteListQuery       paramtools.ParamSet
-	IsAuthoritative      bool
-	SiteURL              string
+	DiffStore         diff.DiffStore
+	ExpectationsStore expstorage.ExpectationsStore
+	IgnoreStore       ignore.IgnoreStore
+	TraceDB           tracedb.DB
+	MasterTileBuilder TileSource
+	EventBus          eventbus.EventBus
+	TryjobStore       tryjobstore.TryjobStore
+	TryjobMonitor     tryjobs.TryjobMonitor
+	GerritAPI         gerrit.GerritInterface
+	GCSClient         GCSClient
+	Baseliner         baseline.Baseliner
+	VCS               vcsinfo.VCS
+	WhiteListQuery    paramtools.ParamSet
+	IsAuthoritative   bool
+	SiteURL           string
 
 	// IsSparseTile indicates that new tiles should be condensed by removing commits that have no data.
 	IsSparseTile bool
diff --git a/golden/go/tryjobs/gerrit_tryjob_monitor/gerrit_tryjob_monitor.go b/golden/go/tryjobs/gerrit_tryjob_monitor/gerrit_tryjob_monitor.go
index a18cf25..3cbc40a 100644
--- a/golden/go/tryjobs/gerrit_tryjob_monitor/gerrit_tryjob_monitor.go
+++ b/golden/go/tryjobs/gerrit_tryjob_monitor/gerrit_tryjob_monitor.go
@@ -17,29 +17,27 @@
 // GerritTryjobMonitor offers a higher level api to handle tryjob-related tasks on top
 // of the tryjobstore package.
 type GerritTryjobMonitor struct {
-	expStore             expstorage.ExpectationsStore
-	issueExpStoreFactory expstorage.IssueExpStoreFactory
-	gerritAPI            gerrit.GerritInterface
-	tryjobStore          tryjobstore.TryjobStore
-	siteURL              string
-	eventBus             eventbus.EventBus
-	writeGerritMonitor   *util.CondMonitor
-	isAuthoritative      bool
+	expStore           expstorage.ExpectationsStore
+	gerritAPI          gerrit.GerritInterface
+	tryjobStore        tryjobstore.TryjobStore
+	siteURL            string
+	eventBus           eventbus.EventBus
+	writeGerritMonitor *util.CondMonitor
+	isAuthoritative    bool
 }
 
 // New creates a new instance of GerritTryjobMonitor.
 // siteURL is URL under which the current site it served. It is used to
 // generate URLs that are written to Gerrit CLs.
-func New(tryjobStore tryjobstore.TryjobStore, expStore expstorage.ExpectationsStore, iesFactory expstorage.IssueExpStoreFactory, gerritAPI gerrit.GerritInterface, siteURL string, eventBus eventbus.EventBus, isAuthoritative bool) *GerritTryjobMonitor {
+func New(tryjobStore tryjobstore.TryjobStore, expStore expstorage.ExpectationsStore, gerritAPI gerrit.GerritInterface, siteURL string, eventBus eventbus.EventBus, isAuthoritative bool) *GerritTryjobMonitor {
 	ret := &GerritTryjobMonitor{
-		expStore:             expStore,
-		issueExpStoreFactory: iesFactory,
-		tryjobStore:          tryjobStore,
-		gerritAPI:            gerritAPI,
-		siteURL:              strings.TrimRight(siteURL, "/"),
-		eventBus:             eventBus,
-		writeGerritMonitor:   util.NewCondMonitor(1),
-		isAuthoritative:      isAuthoritative,
+		expStore:           expStore,
+		tryjobStore:        tryjobStore,
+		gerritAPI:          gerritAPI,
+		siteURL:            strings.TrimRight(siteURL, "/"),
+		eventBus:           eventBus,
+		writeGerritMonitor: util.NewCondMonitor(1),
+		isAuthoritative:    isAuthoritative,
 	}
 
 	// Subscribe to events that a tryjob has been updated.
@@ -122,7 +120,7 @@
 // CommitIssueBaseline commits the expectations for the given issue to the master baseline.
 func (t *GerritTryjobMonitor) CommitIssueBaseline(issueID int64, user string) error {
 	// Get the issue expecations.
-	issueExpStore := t.issueExpStoreFactory(issueID)
+	issueExpStore := t.expStore.ForIssue(issueID)
 	issueChanges, err := issueExpStore.Get()
 	if err != nil {
 		return sklog.FmtErrorf("Unable to retrieve expecations for issue %d: %s", issueID, err)
diff --git a/golden/go/tryjobs/gerrit_tryjob_monitor/gerrit_tryjob_monitor_test.go b/golden/go/tryjobs/gerrit_tryjob_monitor/gerrit_tryjob_monitor_test.go
index d0e5e24..e4f229b 100644
--- a/golden/go/tryjobs/gerrit_tryjob_monitor/gerrit_tryjob_monitor_test.go
+++ b/golden/go/tryjobs/gerrit_tryjob_monitor/gerrit_tryjob_monitor_test.go
@@ -49,7 +49,7 @@
 		assert.True(t, storeIssue.CommentAdded)
 	}).Return(nil)
 
-	tryjobMonitor := New(mtjs, nil, nil, mg, siteURL, meb, isAuthoritative)
+	tryjobMonitor := New(mtjs, nil, mg, siteURL, meb, isAuthoritative)
 
 	assert.NoError(t, tryjobMonitor.WriteGoldLinkAsComment(mockIssueID))
 }
@@ -71,7 +71,7 @@
 
 	mtjs.On("GetIssue", mockIssueID, false).Return(nil, nil)
 
-	tryjobMonitor := New(mtjs, nil, nil, mg, siteURL, meb, isAuthoritative)
+	tryjobMonitor := New(mtjs, nil, mg, siteURL, meb, isAuthoritative)
 
 	err := tryjobMonitor.WriteGoldLinkAsComment(mockIssueID)
 	assert.Error(t, err)
diff --git a/golden/go/tryjobstore/tryjobstore.go b/golden/go/tryjobstore/tryjobstore.go
index 5494f83..f4413ef 100644
--- a/golden/go/tryjobstore/tryjobstore.go
+++ b/golden/go/tryjobstore/tryjobstore.go
@@ -14,7 +14,6 @@
 	"go.skia.org/infra/go/skerr"
 	"go.skia.org/infra/go/sklog"
 	"go.skia.org/infra/go/util"
-	"go.skia.org/infra/golden/go/expstorage"
 	"go.skia.org/infra/golden/go/types"
 	"golang.org/x/sync/errgroup"
 )
@@ -111,13 +110,12 @@
 
 // cloudTryjobStore implements the TryjobStore interface on top of cloud datastore.
 type cloudTryjobStore struct {
-	client          *datastore.Client
-	eventBus        eventbus.EventBus
-	expStoreFactory expstorage.IssueExpStoreFactory
+	client   *datastore.Client
+	eventBus eventbus.EventBus
 }
 
 // NewCloudTryjobStore creates a new instance of TryjobStore based on cloud datastore.
-func NewCloudTryjobStore(client *datastore.Client, expStoreFactory expstorage.IssueExpStoreFactory, eventBus eventbus.EventBus) (TryjobStore, error) {
+func NewCloudTryjobStore(client *datastore.Client, eventBus eventbus.EventBus) (TryjobStore, error) {
 	if client == nil {
 		return nil, sklog.FmtErrorf("Received nil for datastore client.")
 	}
@@ -127,9 +125,8 @@
 	}
 
 	return &cloudTryjobStore{
-		client:          client,
-		eventBus:        eventBus,
-		expStoreFactory: expStoreFactory,
+		client:   client,
+		eventBus: eventBus,
 	}, nil
 }
 
diff --git a/golden/go/tryjobstore/tryjobstore_test.go b/golden/go/tryjobstore/tryjobstore_test.go
index 87de591..deebf10 100644
--- a/golden/go/tryjobstore/tryjobstore_test.go
+++ b/golden/go/tryjobstore/tryjobstore_test.go
@@ -34,15 +34,15 @@
 	defer cleanup()
 
 	eventBus := eventbus.New()
-	_, expStoreFactory, err := ds_expstore.New(ds.DS, eventBus)
+	estore, err := ds_expstore.DeprecatedNew(ds.DS, eventBus)
 	assert.NoError(t, err)
-	store, err := NewCloudTryjobStore(ds.DS, expStoreFactory, eventBus)
+	store, err := NewCloudTryjobStore(ds.DS, eventBus)
 	assert.NoError(t, err)
 
-	testTryjobStore(t, store, expStoreFactory)
+	testTryjobStore(t, store, estore)
 }
 
-func testTryjobStore(t *testing.T, store TryjobStore, expStoreFactory expstorage.IssueExpStoreFactory) {
+func testTryjobStore(t *testing.T, store TryjobStore, estore expstorage.ExpectationsStore) {
 	// Add the issue and two tryjobs to the store.
 	issueID := int64(99)
 	patchsetID := int64(1099)
@@ -205,7 +205,7 @@
 	// TODO(kjlubick): assert something with expLogEntries - it is only added to.
 	expLogEntries := []expstorage.TriageLogEntry{}
 	userName := "jdoe@example.com"
-	expStore := expStoreFactory(issueID)
+	expStore := estore.ForIssue(issueID)
 	for i := 0; i < 5; i++ {
 		triageDetails := []expstorage.TriageDetail{}
 		changes := types.Expectations{}
diff --git a/golden/go/web/web.go b/golden/go/web/web.go
index 0315e8c..3d8bb0c 100644
--- a/golden/go/web/web.go
+++ b/golden/go/web/web.go
@@ -581,7 +581,7 @@
 	// in the request, then get the expectations store for the issue.
 	expStore := wh.Storages.ExpectationsStore
 	if req.Issue > 0 {
-		expStore = wh.Storages.IssueExpStoreFactory(req.Issue)
+		expStore = wh.Storages.ExpectationsStore.ForIssue(req.Issue)
 	}
 
 	// Add the change.
@@ -901,7 +901,7 @@
 		details := q.Get("details") == "true"
 		expStore := wh.Storages.ExpectationsStore
 		if issue > 0 {
-			expStore = wh.Storages.IssueExpStoreFactory(issue)
+			expStore = wh.Storages.ExpectationsStore.ForIssue(issue)
 		}
 
 		logEntries, total, err = expStore.QueryLog(r.Context(), offset, size, details)