Avoid fetching the same blob twice.

Change-Id: Ie447d531b24a46256086caf05509da86b9c996c2
diff --git a/fs/gitilesfs.go b/fs/gitilesfs.go
index b192c02..72462c3 100644
--- a/fs/gitilesfs.go
+++ b/fs/gitilesfs.go
@@ -15,6 +15,7 @@
 package fs
 
 import (
+	"fmt"
 	"io"
 	"log"
 	"os"
@@ -51,6 +52,9 @@
 	shaMap map[git.Oid]string
 
 	lazyRepo *cache.LazyRepo
+
+	fetchingCond *sync.Cond
+	fetching     map[git.Oid]bool
 }
 
 type linkNode struct {
@@ -180,42 +184,78 @@
 // given, we may try a clone of the git repository
 func (r *gitilesRoot) openFile(id git.Oid, clone bool) (*os.File, error) {
 	f, ok := r.cache.Blob.Open(id)
-	if !ok {
-		repo := r.lazyRepo.Repository()
-		if clone && repo == nil {
-			r.lazyRepo.Clone()
-		}
+	if ok {
+		return f, nil
+	}
 
-		var content []byte
-		if repo != nil {
-			blob, err := repo.LookupBlob(&id)
-			if err != nil {
-				log.Printf("LookupBlob: %v", err)
-				return nil, syscall.ESPIPE
-			}
-			defer blob.Free()
-			content = blob.Contents()
-		} else {
-			path := r.shaMap[id]
+	f, err := r.fetchFile(id, clone)
+	if err != nil {
+		log.Printf("fetchFile(%s): %v", id, err)
+		return nil, syscall.ESPIPE
+	}
 
-			var err error
-			content, err = r.service.GetBlob(r.opts.Revision, path)
-			if err != nil {
-				log.Printf("GetBlob(%s, %s): %v", r.opts.Revision, path, err)
-				return nil, syscall.EDOM
-			}
-		}
+	return f, nil
+}
 
-		if err := r.cache.Blob.Write(id, content); err != nil {
-			return nil, err
-		}
+func (r *gitilesRoot) fetchFile(id git.Oid, clone bool) (*os.File, error) {
+	r.fetchingCond.L.Lock()
+	defer r.fetchingCond.L.Unlock()
 
+	for r.fetching[id] {
+		r.fetchingCond.Wait()
+	}
+
+	f, ok := r.cache.Blob.Open(id)
+	if ok {
+		return f, nil
+	}
+
+	r.fetching[id] = true
+	defer func() { delete(r.fetching, id) }()
+	r.fetchingCond.L.Unlock()
+	err := r.fetchFileExpensive(id, clone)
+	r.fetchingCond.L.Lock()
+	r.fetchingCond.Broadcast()
+
+	if err == nil {
 		f, ok = r.cache.Blob.Open(id)
 		if !ok {
-			return nil, syscall.EROFS
+			return nil, fmt.Errorf("fetch succeeded, but blob %s not there", id.String())
+		}
+		return f, nil
+	}
+
+	return nil, err
+}
+
+func (r *gitilesRoot) fetchFileExpensive(id git.Oid, clone bool) error {
+	repo := r.lazyRepo.Repository()
+	if clone && repo == nil {
+		r.lazyRepo.Clone()
+	}
+
+	var content []byte
+	if repo != nil {
+		blob, err := repo.LookupBlob(&id)
+		if err != nil {
+			return err
+		}
+		defer blob.Free()
+		content = blob.Contents()
+	} else {
+		path := r.shaMap[id]
+
+		var err error
+		content, err = r.service.GetBlob(r.opts.Revision, path)
+		if err != nil {
+			return fmt.Errorf("GetBlob(%s, %s): %v", r.opts.Revision, path, err)
 		}
 	}
-	return f, nil
+
+	if err := r.cache.Blob.Write(id, content); err != nil {
+		return err
+	}
+	return nil
 }
 
 // dataNode makes arbitrary data available as a file.
@@ -250,14 +290,16 @@
 // NewGitilesRoot returns the root node for a file system.
 func NewGitilesRoot(c *cache.Cache, tree *gitiles.Tree, service *gitiles.RepoService, options GitilesOptions) nodefs.Node {
 	r := &gitilesRoot{
-		Node:      newDirNode(),
-		service:   service,
-		nodeCache: newNodeCache(),
-		cache:     c,
-		shaMap:    map[git.Oid]string{},
-		tree:      tree,
-		opts:      options,
-		lazyRepo:  cache.NewLazyRepo(options.CloneURL, c),
+		Node:         newDirNode(),
+		service:      service,
+		nodeCache:    newNodeCache(),
+		cache:        c,
+		shaMap:       map[git.Oid]string{},
+		tree:         tree,
+		opts:         options,
+		lazyRepo:     cache.NewLazyRepo(options.CloneURL, c),
+		fetchingCond: sync.NewCond(&sync.Mutex{}),
+		fetching:     map[git.Oid]bool{},
 	}
 
 	return r
diff --git a/fs/gitilesfs_test.go b/fs/gitilesfs_test.go
index 728b31e..35f5865 100644
--- a/fs/gitilesfs_test.go
+++ b/fs/gitilesfs_test.go
@@ -27,6 +27,7 @@
 	"regexp"
 	"sort"
 	"strings"
+	"sync"
 	"syscall"
 	"testing"
 	"time"
@@ -157,11 +158,23 @@
 `,
 }
 
-func handleStatic(w http.ResponseWriter, r *http.Request) {
+type testServer struct {
+	listener net.Listener
+	mu       sync.Mutex
+	requests map[string]int
+}
+
+func (s *testServer) handleStatic(w http.ResponseWriter, r *http.Request) {
 	log.Println("handling", r.URL.String())
+
+	s.mu.Lock()
+	s.requests[r.URL.Path]++
+	s.mu.Unlock()
+
 	resp, ok := testGitiles[r.URL.String()]
 	if !ok {
 		http.Error(w, "not found", 404)
+		return
 	}
 
 	out := []byte(resp)
@@ -175,20 +188,27 @@
 	// TODO(hanwen): set content type?
 }
 
-func newTestServer() (net.Listener, error) {
+func newTestServer() (*testServer, error) {
+	listener, err := net.Listen("tcp", ":0")
+	if err != nil {
+		return nil, err
+	}
+
+	ts := &testServer{
+		listener: listener,
+		requests: map[string]int{},
+	}
+
 	mux := http.NewServeMux()
-	mux.HandleFunc("/", handleStatic)
+	mux.HandleFunc("/", ts.handleStatic)
 
 	s := &http.Server{
 		Handler: mux,
 	}
 
-	listener, err := net.Listen("tcp", ":0")
-	if err != nil {
-		return nil, err
-	}
-	go s.Serve(listener)
-	return listener, err
+	go s.Serve(ts.listener)
+
+	return ts, err
 }
 
 func TestGitilesFSSharedNodes(t *testing.T) {
@@ -336,6 +356,47 @@
 	}
 }
 
+func TestGitilesFSMultiFetch(t *testing.T) {
+	fix, err := newTestFixture()
+	if err != nil {
+		t.Fatal("newTestFixture", err)
+	}
+	defer fix.cleanup()
+
+	repoService := fix.service.NewRepoService("platform/build/kati")
+	treeResp, err := repoService.GetTree("ce34badf691d36e8048b63f89d1a86ee5fa4325c", "", true)
+	if err != nil {
+		t.Fatal("Tree:", err)
+	}
+
+	options := GitilesOptions{
+		Revision: "ce34badf691d36e8048b63f89d1a86ee5fa4325c",
+	}
+
+	fs := NewGitilesRoot(fix.cache, treeResp, repoService, options)
+	if err := fix.mount(fs); err != nil {
+		t.Fatal("mount", err)
+	}
+
+	fn := filepath.Join(fix.mntDir, "AUTHORS")
+
+	var wg sync.WaitGroup
+	for i := 0; i < 10; i++ {
+		wg.Add(1)
+		go func() {
+			ioutil.ReadFile(fn)
+			wg.Done()
+		}()
+	}
+	wg.Wait()
+
+	for key, got := range fix.testServer.requests {
+		if got != 1 {
+			t.Errorf("got request count %d for %s, want 1", got, key)
+		}
+	}
+}
+
 func newManifestTestFixture(mf *manifest.Manifest) (*testFixture, error) {
 	fix, err := newTestFixture()
 	if err != nil {
@@ -476,18 +537,18 @@
 }
 
 type testFixture struct {
-	dir      string
-	mntDir   string
-	server   *fuse.Server
-	cache    *cache.Cache
-	listener net.Listener
-	service  *gitiles.Service
-	root     nodefs.Node
+	dir        string
+	mntDir     string
+	server     *fuse.Server
+	cache      *cache.Cache
+	testServer *testServer
+	service    *gitiles.Service
+	root       nodefs.Node
 }
 
 func (f *testFixture) cleanup() {
-	if f.listener != nil {
-		f.listener.Close()
+	if f.testServer != nil {
+		f.testServer.listener.Close()
 	}
 	if f.server != nil {
 		f.server.Unmount()
@@ -508,13 +569,13 @@
 		return nil, err
 	}
 
-	fixture.listener, err = newTestServer()
+	fixture.testServer, err = newTestServer()
 	if err != nil {
 		return nil, err
 	}
 
 	fixture.service, err = gitiles.NewService(
-		fmt.Sprintf("http://%s", fixture.listener.Addr().String()))
+		fmt.Sprintf("http://%s", fixture.testServer.listener.Addr().String()))
 	if err != nil {
 		return nil, err
 	}