feat(fsrepo): document lock usage and make the fsrepo thread-safe
fix(fsrepo): extract private, unsynced method to prevent deadlock
Brian Tiger Chow committed
Jan 13, 2015 at 21:13 UTC
6ec60ba861c6e39ba47b16ac8bea4bcd5e71f1f5
1 file changed
+85
-36
repo/fsrepo/fsrepo.go
+85
-36
@@ -41,7 +41,8 @@ func init() {
41
lockfiles = make(map[string]io.Closer)
42
}
43
44
-// FSRepo represents an IPFS FileSystem Repo. It is not thread-safe.
44
+// FSRepo represents an IPFS FileSystem Repo. It is safe for use by multiple
45
+// callers.
46
type FSRepo struct {
47
state state
48
path string
@@ -58,6 +59,11 @@ func At(repoPath string) *FSRepo {
59
}
60
61
func ConfigAt(repoPath string) (*config.Config, error) {
62
+
63
+ // packageLock must be held to ensure that the Read is atomic.
64
+ packageLock.Lock()
65
+ defer packageLock.Unlock()
66
+
67
configFilename, err := config.Filename(repoPath)
68
if err != nil {
69
return nil, err
@@ -67,7 +73,10 @@ func ConfigAt(repoPath string) (*config.Config, error) {
73
74
// Init initializes a new FSRepo at the given path with the provided config.
75
func Init(path string, conf *config.Config) error {
70
- packageLock.Lock() // lock must be held to ensure atomicity (prevent Removal)
76
+
77
+ // packageLock must be held to ensure that the repo is not initialized more
78
+ // than once.
79
+ packageLock.Lock()
80
defer packageLock.Unlock()
81
82
if isInitializedUnsynced(path) {
@@ -84,28 +93,42 @@ func Init(path string, conf *config.Config) error {
93
}
94
95
// Remove recursively removes the FSRepo at |path|.
87
-func Remove(path string) error {
96
+func Remove(repoPath string) error {
97
+ repoPath = path.Clean(repoPath)
98
+
99
+ // packageLock must be held to ensure that the repo is not removed while
100
+ // being accessed by others.
101
packageLock.Lock()
102
defer packageLock.Unlock()
90
- if openerCounter.NumOpeners(path) != 0 {
103
+
104
+ if openerCounter.NumOpeners(repoPath) != 0 {
105
return errors.New("repo in use")
106
}
93
- return os.RemoveAll(path)
107
+ return os.RemoveAll(repoPath)
108
}
109
110
// LockedByOtherProcess returns true if the FSRepo is locked by another
111
// process. If true, then the repo cannot be opened by this process.
112
func LockedByOtherProcess(repoPath string) bool {
113
+ repoPath = path.Clean(repoPath)
114
+
115
+ // packageLock must be held to check the number of openers.
116
packageLock.Lock()
117
defer packageLock.Unlock()
118
+
119
// NB: the lock is only held when repos are Open
120
return lockfile.Locked(repoPath) && openerCounter.NumOpeners(repoPath) == 0
121
}
122
123
// Open returns an error if the repo is not initialized.
124
func (r *FSRepo) Open() error {
125
+
126
+ // packageLock must be held to make sure that the repo is not destroyed by
127
+ // another caller. It must not be released until initialization is complete
128
+ // and the number of openers is incremeneted.
129
packageLock.Lock()
130
defer packageLock.Unlock()
131
+
132
if r.state != unopened {
133
return debugerror.Errorf("repo is %s", r.state)
134
}
@@ -154,8 +177,15 @@ func (r *FSRepo) Open() error {
177
//
178
// Result when not Open is undefined. The method may panic if it pleases.
179
func (r *FSRepo) Config() *config.Config {
157
- // no lock necessary because repo is either Open (and thus protected from
158
- // Removal) or has no side-effect
180
+
181
+ // It is not necessary to hold the package lock since the repo is in an
182
+ // opened state. The package lock is _not_ meant to ensure that the repo is
183
+ // thread-safe. The package lock is only meant to guard againt removal and
184
+ // coordinate the lockfile. However, we provide thread-safety to keep
185
+ // things simple.
186
+ packageLock.Lock()
187
+ defer packageLock.Unlock()
188
+
189
if r.state != opened {
190
panic(fmt.Sprintln("repo is", r.state))
191
}
@@ -164,37 +194,19 @@ func (r *FSRepo) Config() *config.Config {
194
195
// SetConfig updates the FSRepo's config.
196
func (r *FSRepo) SetConfig(updated *config.Config) error {
167
- // no lock required because repo should be Open
168
- if r.state != opened {
169
- panic(fmt.Sprintln("repo is", r.state))
170
- }
171
- configFilename, err := config.Filename(r.path)
172
- if err != nil {
173
- return err
174
- }
175
- // to avoid clobbering user-provided keys, must read the config from disk
176
- // as a map, write the updated struct values to the map and write the map
177
- // to disk.
178
- var mapconf map[string]interface{}
179
- if err := readConfigFile(configFilename, &mapconf); err != nil {
180
- return err
181
- }
182
- m, err := config.ToMap(updated)
183
- if err != nil {
184
- return err
185
- }
186
- for k, v := range m {
187
- mapconf[k] = v
188
- }
189
- if err := writeConfigFile(configFilename, mapconf); err != nil {
190
- return err
191
- }
192
- *r.config = *updated // copy so caller cannot modify this private config
193
- return nil
197
+
198
+ // packageLock is held to provide thread-safety.
199
+ packageLock.Lock()
200
+ defer packageLock.Unlock()
201
+
202
+ return r.setConfigUnsynced(updated)
203
}
204
205
// GetConfigKey retrieves only the value of a particular key.
206
func (r *FSRepo) GetConfigKey(key string) (interface{}, error) {
207
+ packageLock.Lock()
208
+ defer packageLock.Unlock()
209
+
210
if r.state != opened {
211
return nil, debugerror.Errorf("repo is %s", r.state)
212
}
@@ -211,7 +223,9 @@ func (r *FSRepo) GetConfigKey(key string) (interface{}, error) {
223
224
// SetConfigKey writes the value of a particular key.
225
func (r *FSRepo) SetConfigKey(key string, value interface{}) error {
214
- // no lock required because repo should be Open
226
+ packageLock.Lock()
227
+ defer packageLock.Unlock()
228
+
229
if r.state != opened {
230
return debugerror.Errorf("repo is %s", r.state)
231
}
@@ -233,13 +247,14 @@ func (r *FSRepo) SetConfigKey(key string, value interface{}) error {
247
if err != nil {
248
return err
249
}
236
- return r.SetConfig(conf)
250
+ return r.setConfigUnsynced(conf)
251
}
252
253
// Close closes the FSRepo, releasing held resources.
254
func (r *FSRepo) Close() error {
255
packageLock.Lock()
256
defer packageLock.Unlock()
257
+
258
if r.state != opened {
259
return debugerror.Errorf("repo is %s", r.state)
260
}
@@ -251,11 +266,15 @@ var _ repo.Repo = &FSRepo{}
266
267
// IsInitialized returns true if the repo is initialized at provided |path|.
268
func IsInitialized(path string) bool {
269
+ // packageLock is held to ensure that another caller doesn't attempt to
270
+ // Init or Remove the repo while this call is in progress.
271
packageLock.Lock()
272
defer packageLock.Unlock()
273
return isInitializedUnsynced(path)
274
}
275
276
+// private methods below this point. NB: packageLock must held by caller.
277
+
278
// isInitializedUnsynced reports whether the repo is initialized. Caller must
279
// hold openerCounter lock.
280
func isInitializedUnsynced(path string) bool {
@@ -317,3 +336,33 @@ func transitionToClosed(r *FSRepo) error {
336
}
337
return nil
338
}
339
+
340
+// setConfigUnsynced is for private use. Callers must hold the packageLock.
341
+func (r *FSRepo) setConfigUnsynced(updated *config.Config) error {
342
+ if r.state != opened {
343
+ return fmt.Errorf("repo is", r.state)
344
+ }
345
+ configFilename, err := config.Filename(r.path)
346
+ if err != nil {
347
+ return err
348
+ }
349
+ // to avoid clobbering user-provided keys, must read the config from disk
350
+ // as a map, write the updated struct values to the map and write the map
351
+ // to disk.
352
+ var mapconf map[string]interface{}
353
+ if err := readConfigFile(configFilename, &mapconf); err != nil {
354
+ return err
355
+ }
356
+ m, err := config.ToMap(updated)
357
+ if err != nil {
358
+ return err
359
+ }
360
+ for k, v := range m {
361
+ mapconf[k] = v
362
+ }
363
+ if err := writeConfigFile(configFilename, mapconf); err != nil {
364
+ return err
365
+ }
366
+ *r.config = *updated // copy so caller cannot modify this private config
367
+ return nil
368
+}