cmd/coordinator/internal/dashboard: filter ErrNoSuchEntity

The AppEngine dashboard filters out datastore.ErrNoSuchEntity, which
happens when we fetch a commit that has no build information yet. This
change ports that functionality to the coordinator dashboard.

Updates golang/go#34744

Change-Id: I38f6b2e1a1805fb24b9e387a5737ff8c2057b625
Reviewed-on: https://go-review.googlesource.com/c/build/+/222197
Run-TryBot: Alexander Rakoczy <alex@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Carlos Amedee <carlos@golang.org>
diff --git a/cmd/coordinator/internal/dashboard/datastore.go b/cmd/coordinator/internal/dashboard/datastore.go
index 2658d94..0d9305c 100644
--- a/cmd/coordinator/internal/dashboard/datastore.go
+++ b/cmd/coordinator/internal/dashboard/datastore.go
@@ -9,6 +9,7 @@
 
 import (
 	"context"
+	"errors"
 	"log"
 
 	"cloud.google.com/go/datastore"
@@ -25,7 +26,8 @@
 		keys = append(keys, key)
 	}
 	out := make([]*Commit, len(keys))
-	if err := cl.GetMulti(ctx, keys, out); err != nil {
+	// datastore.ErrNoSuchEntity is returned when we ask for a commit that we do not yet have test data.
+	if err := cl.GetMulti(ctx, keys, out); err != nil && filterMultiError(err, ignoreNoSuchEntity) != nil {
 		log.Printf("getResults: error fetching %d results: %v", len(keys), err)
 		return
 	}
@@ -42,3 +44,45 @@
 	}
 	return
 }
+
+type ignoreFunc func(err error) error
+
+// ignoreNoSuchEntity ignores datastore.ErrNoSuchEntity, which is returned when
+// we ask for a commit that we do not yet have test data.
+func ignoreNoSuchEntity(err error) error {
+	if !errors.Is(err, datastore.ErrNoSuchEntity) {
+		return err
+	}
+	return nil
+}
+
+// filterMultiError loops over datastore.MultiError, skipping errors ignored by
+// the specified ignoreFuncs. Any unfiltered errors will be returned as a
+// datastore.MultiError error. If no errors are left, nil will be returned.
+// Errors that are not datastore.MultiError will be returned as-is.
+func filterMultiError(err error, ignores ...ignoreFunc) error {
+	if err == nil {
+		return nil
+	}
+	me := datastore.MultiError{}
+	if ok := errors.As(err, &me); !ok {
+		return err
+	}
+	ret := datastore.MultiError{}
+	for _, err := range me {
+		var skip bool
+		for _, ignore := range ignores {
+			if err := ignore(err); err == nil {
+				skip = true
+				break
+			}
+		}
+		if !skip {
+			ret = append(ret, err)
+		}
+	}
+	if len(ret) > 0 {
+		return ret
+	}
+	return nil
+}
diff --git a/cmd/coordinator/internal/dashboard/datastore_test.go b/cmd/coordinator/internal/dashboard/datastore_test.go
new file mode 100644
index 0000000..0a5507c
--- /dev/null
+++ b/cmd/coordinator/internal/dashboard/datastore_test.go
@@ -0,0 +1,91 @@
+// Copyright 2020 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// +build go1.13
+// +build linux darwin
+
+package dashboard
+
+import (
+	"errors"
+	"testing"
+
+	"cloud.google.com/go/datastore"
+)
+
+var testError = errors.New("this is a test error for cmd/coordinator")
+
+func ignoreTestError(err error) error {
+	if !errors.Is(err, testError) {
+		return err
+	}
+	return nil
+}
+
+func ignoreNothing(err error) error {
+	return err
+}
+
+func TestFilterMultiError(t *testing.T) {
+	cases := []struct {
+		desc    string
+		err     error
+		ignores []ignoreFunc
+		wantErr bool
+	}{
+		{
+			desc:    "single ignored error",
+			err:     datastore.MultiError{testError},
+			ignores: []ignoreFunc{ignoreTestError},
+		},
+		{
+			desc:    "multiple ignored errors",
+			err:     datastore.MultiError{testError, testError},
+			ignores: []ignoreFunc{ignoreTestError},
+		},
+		{
+			desc:    "non-ignored error",
+			err:     datastore.MultiError{testError, errors.New("this should fail")},
+			ignores: []ignoreFunc{ignoreTestError},
+			wantErr: true,
+		},
+		{
+			desc:    "nil error",
+			ignores: []ignoreFunc{ignoreTestError},
+		},
+		{
+			desc:    "non-multistore error",
+			err:     errors.New("this should fail"),
+			ignores: []ignoreFunc{ignoreTestError},
+			wantErr: true,
+		},
+		{
+			desc:    "no ignoreFuncs",
+			err:     errors.New("this should fail"),
+			ignores: []ignoreFunc{},
+			wantErr: true,
+		},
+		{
+			desc:    "if any ignoreFunc ignores, error is ignored.",
+			err:     datastore.MultiError{testError, testError},
+			ignores: []ignoreFunc{ignoreNothing, ignoreTestError},
+		},
+	}
+	for _, c := range cases {
+		t.Run(c.desc, func(t *testing.T) {
+			if err := filterMultiError(c.err, c.ignores...); (err != nil) != c.wantErr {
+				t.Errorf("filterMultiError(%v, %v) = %v, wantErr = %v", c.err, c.ignores, err, c.wantErr)
+			}
+		})
+	}
+}
+
+func TestIgnoreNoSuchEntity(t *testing.T) {
+	if err := ignoreNoSuchEntity(datastore.ErrNoSuchEntity); err != nil {
+		t.Errorf("ignoreNoSuchEntity(%v) = %v, wanted no error", datastore.ErrNoSuchEntity, err)
+	}
+	if err := ignoreNoSuchEntity(datastore.ErrInvalidKey); err == nil {
+		t.Errorf("ignoreNoSuchEntity(%v) = %v, wanted error", datastore.ErrInvalidKey, err)
+	}
+}