@cryptotaxi247 / kubo / commits / 6ec60ba86

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 +}