feat(fsrepo): handle safe Removal
Brian Tiger Chow committed
Jan 12, 2015 at 20:03 UTC
4aba282379c961e91dd2bd6622e21334af911dc0
6 files changed
+180
-5
cmd/ipfs/init.go
+3
@@ -101,6 +101,9 @@ func doInit(repoRoot string, force bool, nBitsForKeypair int) (interface{}, erro
101
return nil, err
102
}
103
} else {
104
+ if err := fsrepo.Remove(repoRoot); err != nil {
105
+ return nil, err
106
+ }
107
r := fsrepo.At(repoRoot)
108
if err := r.Open(); err != nil {
109
return nil, err
repo/fsrepo/fsrepo.go
+55
-5
@@ -1,6 +1,7 @@
1
package fsrepo
2
3
import (
4
+ "errors"
5
"fmt"
6
"io"
7
"os"
@@ -13,7 +14,22 @@ import (
14
debugerror "github.com/jbenet/go-ipfs/util/debugerror"
15
)
16
16
-// FSRepo represents an IPFS FileSystem Repo
17
+var (
18
+ // pkgLock prevents the fsrepo from being removed while there exist open
19
+ // FSRepo handles. It also ensures that the Init is atomic.
20
+ //
21
+ // packageLock also protects numOpenedRepos
22
+ //
23
+ // If an operation is used when repo is Open and the operation does not
24
+ // change the repo's state, the package lock does not need to be acquired.
25
+ pkgLock *packageLock
26
+)
27
+
28
+func init() {
29
+ pkgLock = makePackageLock()
30
+}
31
+
32
+// FSRepo represents an IPFS FileSystem Repo. It is not thread-safe.
33
type FSRepo struct {
34
state state
35
path string
@@ -22,6 +38,7 @@ type FSRepo struct {
38
39
// At returns a handle to an FSRepo at the provided |path|.
40
func At(path string) *FSRepo {
41
+ // This method must not have side-effects.
42
return &FSRepo{
43
path: path,
44
state: unopened, // explicitly set for clarity
@@ -30,7 +47,10 @@ func At(path string) *FSRepo {
47
48
// Init initializes a new FSRepo at the given path with the provided config.
49
func Init(path string, conf *config.Config) error {
33
- if IsInitialized(path) {
50
+ pkgLock.Lock() // lock must be held to ensure atomicity (prevent Removal)
51
+ defer pkgLock.Unlock()
52
+
53
+ if isInitializedUnsynced(path) {
54
return nil
55
}
56
configFilename, err := config.Filename(path)
@@ -43,12 +63,24 @@ func Init(path string, conf *config.Config) error {
63
return nil
64
}
65
66
+// Remove recursively removes the FSRepo at |path|.
67
+func Remove(path string) error {
68
+ pkgLock.Lock()
69
+ defer pkgLock.Unlock()
70
+ if pkgLock.NumOpeners(path) != 0 {
71
+ return errors.New("repo in use")
72
+ }
73
+ return os.RemoveAll(path)
74
+}
75
+
76
// Open returns an error if the repo is not initialized.
77
func (r *FSRepo) Open() error {
78
+ pkgLock.Lock()
79
+ defer pkgLock.Unlock()
80
if r.state != unopened {
81
return debugerror.Errorf("repo is %s", r.state)
82
}
51
- if !IsInitialized(r.path) {
83
+ if !isInitializedUnsynced(r.path) {
84
return debugerror.New("ipfs not initialized, please run 'ipfs init'")
85
}
86
// check repo path, then check all constituent parts.
@@ -86,12 +118,17 @@ func (r *FSRepo) Open() error {
118
}
119
120
r.state = opened
121
+ pkgLock.AddOpener(r.path)
122
return nil
123
}
124
92
-// Config returns the FSRepo's config. Result is undefined if the Repo is not
93
-// Open.
125
+// Config returns the FSRepo's config. This method must not be called if the
126
+// repo is not open.
127
+//
128
+// Result when not Open is undefined. The method may panic if it pleases.
129
func (r *FSRepo) Config() *config.Config {
130
+ // no lock necessary because repo is either Open (and thus protected from
131
+ // Removal) or has no side-effect
132
if r.state != opened {
133
panic(fmt.Sprintln("repo is", r.state))
134
}
@@ -100,6 +137,7 @@ func (r *FSRepo) Config() *config.Config {
137
138
// SetConfig updates the FSRepo's config.
139
func (r *FSRepo) SetConfig(updated *config.Config) error {
140
+ // no lock required because repo should be Open
141
if r.state != opened {
142
panic(fmt.Sprintln("repo is", r.state))
143
}
@@ -146,6 +184,7 @@ func (r *FSRepo) GetConfigKey(key string) (interface{}, error) {
184
185
// SetConfigKey writes the value of a particular key.
186
func (r *FSRepo) SetConfigKey(key string, value interface{}) error {
187
+ // no lock required because repo should be Open
188
if r.state != opened {
189
return debugerror.Errorf("repo is %s", r.state)
190
}
@@ -172,9 +211,12 @@ func (r *FSRepo) SetConfigKey(key string, value interface{}) error {
211
212
// Close closes the FSRepo, releasing held resources.
213
func (r *FSRepo) Close() error {
214
+ pkgLock.Lock()
215
+ defer pkgLock.Unlock()
216
if r.state != opened {
217
return debugerror.Errorf("repo is %s", r.state)
218
}
219
+ pkgLock.RemoveOpener(r.path)
220
return nil // TODO release repo lock
221
}
222
@@ -183,6 +225,14 @@ var _ repo.Interface = &FSRepo{}
225
226
// IsInitialized returns true if the repo is initialized at provided |path|.
227
func IsInitialized(path string) bool {
228
+ pkgLock.Lock()
229
+ defer pkgLock.Unlock()
230
+ return isInitializedUnsynced(path)
231
+}
232
+
233
+// isInitializedUnsynced reports whether the repo is initialized. Caller must
234
+// hold pkgLock.
235
+func isInitializedUnsynced(path string) bool {
236
configFilename, err := config.Filename(path)
237
if err != nil {
238
return false
repo/fsrepo/fsrepo_test.go
new
+67
@@ -0,0 +1,67 @@
1
+package fsrepo
2
+
3
+import (
4
+ "os"
5
+ "path"
6
+ "testing"
7
+
8
+ "github.com/jbenet/go-ipfs/repo/config"
9
+)
10
+
11
+// NB: These tests cannot be run in parallel
12
+
13
+func init() {
14
+ // ensure tests begin in clean state
15
+ os.RemoveAll(testRepoDir)
16
+}
17
+
18
+const testRepoDir = "./fsrepo_test/repos"
19
+
20
+func testRepoPath(p string) string {
21
+ return path.Join(testRepoDir, p)
22
+}
23
+
24
+func TestCannotRemoveIfOpen(t *testing.T) {
25
+ path := testRepoPath("TestCannotRemoveIfOpen")
26
+ AssertNil(Init(path, &config.Config{}), t, "should initialize successfully")
27
+ r := At(path)
28
+ AssertNil(r.Open(), t)
29
+ AssertErr(Remove(path), t, "should not be able to remove while open")
30
+ AssertNil(r.Close(), t)
31
+ AssertNil(Remove(path), t, "should be able to remove after closed")
32
+}
33
+
34
+func TestCanManageReposIndependently(t *testing.T) {
35
+ pathA := testRepoPath("a")
36
+ pathB := testRepoPath("b")
37
+
38
+ t.Log("initialize two repos")
39
+ AssertNil(Init(pathA, &config.Config{}), t, "should initialize successfully")
40
+ AssertNil(Init(pathB, &config.Config{}), t, "should initialize successfully")
41
+
42
+ t.Log("open the two repos")
43
+ repoA := At(pathA)
44
+ repoB := At(pathB)
45
+ AssertNil(repoA.Open(), t)
46
+ AssertNil(repoB.Open(), t)
47
+
48
+ t.Log("close and remove b while a is open")
49
+ AssertNil(repoB.Close(), t, "close b")
50
+ AssertNil(Remove(pathB), t, "remove b")
51
+
52
+ t.Log("close and remove a")
53
+ AssertNil(repoA.Close(), t)
54
+ AssertNil(Remove(pathA), t)
55
+}
56
+
57
+func AssertNil(err error, t *testing.T, msgs ...string) {
58
+ if err != nil {
59
+ t.Error(msgs, "error:", err)
60
+ }
61
+}
62
+
63
+func AssertErr(err error, t *testing.T, msgs ...string) {
64
+ if err == nil {
65
+ t.Error(msgs, "error:", err)
66
+ }
67
+}
repo/fsrepo/fsrepo_test/.gitkeep
repo/fsrepo/fsrepo_test/README.md
new
+1
@@ -0,0 +1 @@
1
+This directory is used to store FSRepos generated during go tests.
repo/fsrepo/lock.go
new
+54
@@ -0,0 +1,54 @@
1
+package fsrepo
2
+
3
+import (
4
+ "path"
5
+ "sync"
6
+)
7
+
8
+type packageLock struct {
9
+ // lock protects repos
10
+ lock sync.Mutex
11
+ // repos maps repo paths to the number of openers holding an FSRepo handle
12
+ // to it
13
+ repos map[string]int
14
+}
15
+
16
+func makePackageLock() *packageLock {
17
+ return &packageLock{
18
+ repos: make(map[string]int),
19
+ }
20
+}
21
+
22
+// Lock must be held to while performing any operation that modifies an
23
+// FSRepo's state field. This includes Init, Open, Close, and Remove.
24
+func (l *packageLock) Lock() {
25
+ l.lock.Lock()
26
+}
27
+
28
+func (l *packageLock) Unlock() {
29
+ l.lock.Unlock()
30
+}
31
+
32
+// NumOpeners returns the number of FSRepos holding a handle to the repo at
33
+// this path. This method is not thread-safe. The caller must have this object
34
+// locked.
35
+func (l *packageLock) NumOpeners(repoPath string) int {
36
+ return l.repos[key(repoPath)]
37
+}
38
+
39
+// AddOpener messages that an FSRepo holds a handle to the repo at this path.
40
+// This method is not thread-safe. The caller must have this object locked.
41
+func (l *packageLock) AddOpener(repoPath string) {
42
+ l.repos[key(repoPath)]++
43
+}
44
+
45
+// RemoveOpener messgaes that an FSRepo no longer holds a handle to the repo at
46
+// this path. This method is not thread-safe. The caller must have this object
47
+// locked.
48
+func (l *packageLock) RemoveOpener(repoPath string) {
49
+ l.repos[key(repoPath)]--
50
+}
51
+
52
+func key(repoPath string) string {
53
+ return path.Clean(repoPath)
54
+}