diff --git a/repo/fsrepo/fsrepo.go b/repo/fsrepo/fsrepo.go index 626ecb18d..d4ab8c240 100644 --- a/repo/fsrepo/fsrepo.go +++ b/repo/fsrepo/fsrepo.go @@ -50,6 +50,20 @@ var ( dsLock sync.Mutex dsOpenersCounter *counter.Openers datastores map[string]ds2.ThreadSafeDatastoreCloser + + // onlyOne keeps track of open FSRepo instances. + // + // TODO: once command Context / Repo integration is cleaned up, + // this can be removed. Right now, this makes ConfigCmd.Run + // function try to open the repo twice: + // + // $ ipfs daemon & + // $ ipfs config foo + // + // The reason for the above is that in standalone mode without the + // daemon, `ipfs config` tries to save work by not building the + // full IpfsNode, but accessing the Repo directly. + onlyOne repo.OnlyOne ) func init() { @@ -77,7 +91,14 @@ var _ repo.Repo = (*FSRepo)(nil) // Open the FSRepo at path. Returns an error if the repo is not // initialized. -func Open(repoPath string) (*FSRepo, error) { +func Open(repoPath string) (repo.Repo, error) { + fn := func() (repo.Repo, error) { + return open(repoPath) + } + return onlyOne.Open(repoPath, fn) +} + +func open(repoPath string) (repo.Repo, error) { packageLock.Lock() defer packageLock.Unlock() diff --git a/repo/fsrepo/fsrepo_test.go b/repo/fsrepo/fsrepo_test.go index a526a31c9..bcd3af600 100644 --- a/repo/fsrepo/fsrepo_test.go +++ b/repo/fsrepo/fsrepo_test.go @@ -111,7 +111,7 @@ func TestOpenMoreThanOnceInSameProcess(t *testing.T) { assert.Nil(err, t, "first repo should open successfully") r2, err := Open(path) assert.Nil(err, t, "second repo should open successfully") - assert.True(r1.ds == r2.ds, t, "repos should share the datastore") + assert.True(r1 == r2, t, "second open returns same value") assert.Nil(r1.Close(), t) assert.Nil(r2.Close(), t) diff --git a/repo/onlyone.go b/repo/onlyone.go new file mode 100644 index 000000000..860c166bf --- /dev/null +++ b/repo/onlyone.go @@ -0,0 +1,72 @@ +package repo + +import ( + "sync" +) + +// OnlyOne tracks open Repos by arbitrary key and returns the already +// open one. +type OnlyOne struct { + mu sync.Mutex + active map[interface{}]*ref +} + +// Open a Repo identified by key. If Repo is not already open, the +// open function is called, and the result is remember for further +// use. +// +// Key must be comparable, or Open will panic. Make sure to pick keys +// that are unique across different concrete Repo implementations, +// e.g. by creating a local type: +// +// type repoKey string +// r, err := o.Open(repoKey(path), open) +// +// Call Repo.Close when done. +func (o *OnlyOne) Open(key interface{}, open func() (Repo, error)) (Repo, error) { + o.mu.Lock() + defer o.mu.Unlock() + if o.active == nil { + o.active = make(map[interface{}]*ref) + } + + item, found := o.active[key] + if !found { + repo, err := open() + if err != nil { + return nil, err + } + item = &ref{ + parent: o, + key: key, + Repo: repo, + } + o.active[key] = item + } + item.refs++ + return item, nil +} + +type ref struct { + parent *OnlyOne + key interface{} + refs uint32 + Repo +} + +var _ Repo = (*ref)(nil) + +func (r *ref) Close() error { + r.parent.mu.Lock() + defer r.parent.mu.Unlock() + + r.refs-- + if r.refs > 0 { + // others are holding it open + return nil + } + + // last one + delete(r.parent.active, r.key) + return r.Repo.Close() +}