Merge lp:~chipaca/snappy/lock-ness into lp:~snappy-dev/snappy/snappy-moved-to-github

Proposed by John Lenton on 2015-10-15
Status: Needs review
Proposed branch: lp:~chipaca/snappy/lock-ness
Merge into: lp:~snappy-dev/snappy/snappy-moved-to-github
Diff against target: 988 lines (+597/-51)
8 files modified
cmd/snappy/common.go (+4/-5)
daemon/api.go (+53/-4)
daemon/api_test.go (+24/-40)
daemon/daemon.go (+4/-0)
daemon/daemon_test.go (+23/-2)
daemon/mmutex/mmutex.go (+213/-0)
daemon/mmutex/mmutex_test.go (+274/-0)
dirs/dirs.go (+2/-0)
To merge this branch: bzr merge lp:~chipaca/snappy/lock-ness
Reviewer Review Type Date Requested Status
Gustavo Niemeyer 2015-10-15 Needs Information on 2015-10-21
Review via email: mp+274547@code.launchpad.net

Commit Message

REST API now uses hierarchical package-grained locking.

Description of the Change

Moo. TeX. What's not to like.

To post a comment you must log in.
Michael Vogt (mvo) wrote :

Thanks for this branch! It looks good (but I need to read through it again as its quite complex at first). I got a bit of a meta-question. It seems like all the locking is done in the rest-api which is fine as its the only concurrent user of the API right now. But it also means that if someone builds on top of the snappy api he/she will have to rebuild the same locking. Is it a huge amount of work to put the locks inside snappy/* itself or is there another downside that I have not considered?

Michael Vogt (mvo) wrote :

Fwiw, I love the branch title :P

John Lenton (chipaca) wrote :

Remember the intention is to move away from having multiple things building against "the snappy api"; clients would use the rest api. There's a question about what that means for u-d-f, but locking shouldn't be an issue for it anyway.

The only downside to moving locking down in the call stack is that it's not clear where to lock, exactly, unless a lot of things grow a boolean "lock is already held" flag.

I can of course move the mmutex package one level down, but I don't think that's what you meant :)

Sergio Schvezov (sergiusens) wrote :

On Mon, Oct 19, 2015 at 6:47 AM, John Lenton <email address hidden>
wrote:

> Remember the intention is to move away from having multiple things
> building against "the snappy api"; clients would use the rest api. There's
> a question about what that means for u-d-f, but locking shouldn't be an
> issue for it anyway.
>

 The only thing u-d-f would need to do on its own is kernel, os and gadget
snap (bootstrapping problem); the apps and frameworks should be unpacked by
firstboot.

> The only downside to moving locking down in the call stack is that it's
> not clear where to lock, exactly, unless a lot of things grow a boolean
> "lock is already held" flag.
>

/me starts to type a reply
/me thinks http://cdn.meme.am/instances/500x/65135298.jpg

>
> I can of course move the mmutex package one level down, but I don't think
> that's what you meant :)
>
>

Michael Vogt (mvo) wrote :

Thanks for the branch and the explanation. One question inline and some minor stuff inline. I'm fine approving once the question is answered. In any case I read again tomorrow morning with a fresh mind, its not trivial (for me).

John Lenton (chipaca) :
Gustavo Niemeyer (niemeyer) wrote :
Download full text (3.7 KiB)

Gustavo Niemeyer, [21.10.15 10:16]
@chipaca I'm lacking some context for why we'd want that locked-locking-locker

John Lenton, [21.10.15 10:17]
@niemeyer you mean the mutex inside the mmutex, or you mean the whole branch?

Gustavo Niemeyer, [21.10.15 10:17]
I mean the whole thing

John Lenton, [21.10.15 10:18]
@niemeyer well, we need some kind of a lock, to avoid people doing things at the same time and putting us in an unknown state

John Lenton, [21.10.15 10:19]
@niemeyer and a single, global lock didn't seem like a good idea

Gustavo Niemeyer, [21.10.15 10:19]
@chipaca In general we try to accommodate for people doing things at the same time instead, and prevent the actual critical things that cannot be mutated at once from being mutated at once

John Lenton, [21.10.15 10:19]
@niemeyer yes, in general we do

Gustavo Niemeyer, [21.10.15 10:20]
@chipaca Right, thus why I'm trying to understand how this locking system fits

John Lenton, [21.10.15 10:24]
@niemeyer it fits in that most of snappy is still the commandline one-shot thing, which has a filesystem-based lock by necessity to prevent different invocations of it to collide

John Lenton, [21.10.15 10:25]
@niemeyer a refactor of the level needed to move it to accomplish this exclusion without explicit locks is beyond the scope of the work we can do right now

Gustavo Niemeyer, [21.10.15 10:25]
@Chipaca So keep the filesystem locks?

Gustavo Niemeyer, [21.10.15 10:26]
@chipaca Being in a hurry and doing a big complex locking system doesn't feel compatible

John Lenton, [21.10.15 10:27]
this isn't a big complex locking system, imo

John Lenton, [21.10.15 10:27]
the filesystem lock is still there

Gustavo Niemeyer, [21.10.15 10:27]
@Chipaca Rather than one big central system, it would feel much better to me to give the proper places in the code authority for their own concurrency handling

John Lenton, [21.10.15 10:28]
@niemeyer isn't that what this does?

Gustavo Niemeyer, [21.10.15 10:29]
@chipaca That often amounts to a single local mutex, if at all

Gustavo Niemeyer, [21.10.15 10:29]
@chipaca So, again, I may well not be aware of the use case, but it feels like the use case is what we should be talking about

Gustavo Niemeyer, [21.10.15 10:30]
@chipaca It feels complex to me, and it feels complex to mvo, FWIW

John Lenton, [21.10.15 10:31]
yes, but less than 200 lines of code including whitespace and comments is not "big and complex"

John Lenton, [21.10.15 10:31]
anyway, the use case

Gustavo Niemeyer, [21.10.15 10:32]
@chipaca mutex.go is 126 lines long

Gustavo Niemeyer, [21.10.15 10:32]
and a lot of it is docs :)

Gustavo Niemeyer, [21.10.15 10:33]
But I digress

John Lenton, [21.10.15 10:33]
plus 137 for rwmutex

Gustavo Niemeyer, [21.10.15 10:33]
The key here is the use case.. if we need it, so be it, but I'm not yet understanding how this would fit in that cannot be solved by something simpler

John Lenton, [21.10.15 10:34]
when somebody we need a list of packages, or services, etc, we need to avoid people installing/removing/starting/stopping things for a bit

John Lenton, [21.10.15 10:34]
when people operate on a package, we need to stop people operating on that package for a b...

Read more...

review: Needs Information
Michael Vogt (mvo) wrote :

Thanks for your replies, I replied inline as well.

Unmerged revisions

772. By John Lenton on 2015-10-15

Introducing the MMutex. It is not a cow in affordable clothing.

771. By John Lenton on 2015-10-14

Committing in search for a name

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'cmd/snappy/common.go'
2--- cmd/snappy/common.go 2015-07-09 06:05:53 +0000
3+++ cmd/snappy/common.go 2015-10-15 11:44:27 +0000
4@@ -20,17 +20,16 @@
5 package main
6
7 import (
8+ "github.com/jessevdk/go-flags"
9+
10+ "launchpad.net/snappy/dirs"
11 "launchpad.net/snappy/logger"
12 "launchpad.net/snappy/priv"
13-
14- "github.com/jessevdk/go-flags"
15 )
16
17-const snappyLockFile = "/run/snappy.lock"
18-
19 // withMutex runs the given function with a filelock mutex
20 func withMutex(f func() error) error {
21- return priv.WithMutex(snappyLockFile, f)
22+ return priv.WithMutex(dirs.SnapLockFile, f)
23 }
24
25 // addOptionDescription will try to find the given longName in the
26
27=== modified file 'daemon/api.go'
28--- daemon/api.go 2015-10-13 08:55:04 +0000
29+++ daemon/api.go 2015-10-15 11:44:27 +0000
30@@ -120,6 +120,9 @@
31 )
32
33 func v1Get(c *Command, r *http.Request) Response {
34+ c.d.mmutex.RLock()
35+ defer c.d.mmutex.RUnlock()
36+
37 rel := release.Get()
38 m := map[string]string{
39 "flavor": rel.Flavor,
40@@ -156,6 +159,9 @@
41 name := vars["name"]
42 origin := vars["origin"]
43
44+ c.d.mmutex.RLock(name, origin)
45+ defer c.d.mmutex.RUnlock(name, origin)
46+
47 repo := newRemoteRepo()
48 var part snappy.Part
49 if parts, _ := repo.Details(name, origin); len(parts) > 0 {
50@@ -228,6 +234,9 @@
51 return InternalError(nil, "router can't find route for packages")
52 }
53
54+ c.d.mmutex.RLock()
55+ defer c.d.mmutex.RUnlock()
56+
57 sources := make([]string, 1, 3)
58 sources[0] = "local"
59 // we're not worried if the remote repos error out
60@@ -323,9 +332,18 @@
61 action = cmd["action"]
62 }
63
64+ reachedAsync := false
65 switch action {
66- case "status", "start", "stop", "restart", "enable", "disable":
67- // ok
68+ case "status":
69+ c.d.mmutex.RLock(name, origin)
70+ defer c.d.mmutex.RUnlock(name, origin)
71+ case "start", "stop", "restart", "enable", "disable":
72+ c.d.mmutex.Lock(name, origin)
73+ defer func() {
74+ if !reachedAsync {
75+ c.d.mmutex.Unlock(name, origin)
76+ }
77+ }()
78 default:
79 return BadRequest(nil, "unknown action %s", action)
80 }
81@@ -393,7 +411,11 @@
82 return SyncResponse(f())
83 }
84
85+ reachedAsync = true
86+
87 return AsyncResponse(c.d.AddTask(func() interface{} {
88+ defer c.d.mmutex.Unlock(name, origin)
89+
90 switch action {
91 case "start":
92 err = actor.Start()
93@@ -425,6 +447,14 @@
94 }
95 pkgName := name + "." + origin
96
97+ if r.Method == "GET" {
98+ c.d.mmutex.RLock(name, origin)
99+ defer c.d.mmutex.RUnlock(name, origin)
100+ } else {
101+ c.d.mmutex.Lock(name, origin)
102+ defer c.d.mmutex.Unlock(name, origin)
103+ }
104+
105 bag := lightweight.PartBagByName(name, origin)
106 if bag == nil {
107 return NotFound
108@@ -471,6 +501,9 @@
109 }
110
111 return AsyncResponse(c.d.AddTask(func() interface{} {
112+ c.d.mmutex.Lock()
113+ defer c.d.mmutex.Unlock()
114+
115 rspmap := make(map[string]*configSubtask, len(pkgmap))
116 bags := lightweight.AllPartBags()
117 for pkg, cfg := range pkgmap {
118@@ -653,7 +686,9 @@
119 }
120
121 vars := muxVars(r)
122- inst.pkg = vars["name"] + "." + vars["origin"]
123+ name := vars["name"]
124+ origin := vars["origin"]
125+ inst.pkg = name + "." + origin
126 inst.prog = &progress.NullProgress{}
127
128 f := pkgActionDispatch(&inst)
129@@ -661,7 +696,11 @@
130 return BadRequest(nil, "unknown action %s", inst.Action)
131 }
132
133- return AsyncResponse(c.d.AddTask(f).Map(route))
134+ return AsyncResponse(c.d.AddTask(func() interface{} {
135+ c.d.mmutex.Lock(name, origin)
136+ defer c.d.mmutex.Unlock(name, origin)
137+ return f()
138+ }).Map(route))
139 }
140
141 const maxReadBuflen = 1024 * 1024
142@@ -740,6 +779,9 @@
143 return err
144 }
145
146+ c.d.mmutex.Lock()
147+ defer c.d.mmutex.Unlock()
148+
149 name, err := part.Install(&progress.NullProgress{}, 0)
150 if err != nil {
151 return err
152@@ -752,8 +794,12 @@
153 func getLogs(c *Command, r *http.Request) Response {
154 vars := muxVars(r)
155 name := vars["name"]
156+ origin := vars["origin"]
157 svcName := vars["service"]
158
159+ c.d.mmutex.RLock(name, origin)
160+ defer c.d.mmutex.RUnlock(name, origin)
161+
162 actor, err := findServices(name, svcName, &progress.NullProgress{})
163 if err != nil {
164 return NotFound(err, "no services found for %q: %v", name, err)
165@@ -791,6 +837,9 @@
166 name := vars["name"]
167 origin := vars["origin"]
168
169+ c.d.mmutex.RLock(name, origin)
170+ defer c.d.mmutex.RUnlock(name, origin)
171+
172 bag := lightweight.PartBagByName(name, origin)
173 if bag == nil || len(bag.Versions) == 0 {
174 return NotFound
175
176=== modified file 'daemon/api_test.go'
177--- daemon/api_test.go 2015-10-13 08:55:04 +0000
178+++ daemon/api_test.go 2015-10-15 11:44:27 +0000
179@@ -34,7 +34,6 @@
180 "os"
181 "path/filepath"
182 "strings"
183- "testing"
184 "time"
185
186 "gopkg.in/check.v1"
187@@ -48,9 +47,6 @@
188 "launchpad.net/snappy/systemd"
189 )
190
191-// Hook up check.v1 into the "go test" runner
192-func Test(t *testing.T) { check.TestingT(t) }
193-
194 type apiSuite struct {
195 parts []snappy.Part
196 err error
197@@ -75,6 +71,12 @@
198 return s.vars
199 }
200
201+// muLock is a minimal PrivLocker
202+type muLock struct{}
203+
204+func (muLock) Lock() error { return nil }
205+func (muLock) Unlock() error { return nil }
206+
207 func (s *apiSuite) SetUpSuite(c *check.C) {
208 newRemoteRepo = func() metarepo {
209 return s
210@@ -134,8 +136,7 @@
211 }
212
213 func (s *apiSuite) TestPackageInfoOneIntegration(c *check.C) {
214- d := New()
215- d.addRoutes()
216+ newTestDaemon()
217
218 s.vars = map[string]string{"name": "foo", "origin": "bar"}
219
220@@ -217,8 +218,7 @@
221 func (s *apiSuite) TestPackageInfoWeirdRoute(c *check.C) {
222 // can't really happen
223
224- d := New()
225- d.addRoutes()
226+ d := newTestDaemon()
227
228 // use the wrong command to force the issue
229 wrongCmd := &Command{Path: "/{what}", d: d}
230@@ -230,8 +230,7 @@
231 func (s *apiSuite) TestPackageInfoBadRoute(c *check.C) {
232 // can't really happen, v2
233
234- d := New()
235- d.addRoutes()
236+ d := newTestDaemon()
237
238 // get the route and break it
239 route := d.router.Get(packageCmd.Path)
240@@ -374,8 +373,7 @@
241 }
242
243 func (s *apiSuite) TestPackagesInfoOnePerIntegration(c *check.C) {
244- d := New()
245- d.addRoutes()
246+ newTestDaemon()
247
248 req, err := http.NewRequest("GET", "/1.0/packages", nil)
249 c.Assert(err, check.IsNil)
250@@ -416,8 +414,7 @@
251 }
252
253 func (s *apiSuite) TestDeleteOpNotFound(c *check.C) {
254- d := New()
255- d.addRoutes()
256+ newTestDaemon()
257
258 s.vars = map[string]string{"uuid": "42"}
259 rsp := deleteOp(operationCmd, nil).Self(nil, nil).(*resp)
260@@ -426,8 +423,7 @@
261 }
262
263 func (s *apiSuite) TestDeleteOpStillRunning(c *check.C) {
264- d := New()
265- d.addRoutes()
266+ d := newTestDaemon()
267
268 d.tasks["42"] = &Task{}
269 s.vars = map[string]string{"uuid": "42"}
270@@ -437,8 +433,7 @@
271 }
272
273 func (s *apiSuite) TestDeleteOp(c *check.C) {
274- d := New()
275- d.addRoutes()
276+ d := newTestDaemon()
277
278 task := &Task{}
279 d.tasks["42"] = task
280@@ -450,8 +445,7 @@
281 }
282
283 func (s *apiSuite) TestGetOpInfoIntegration(c *check.C) {
284- d := New()
285- d.addRoutes()
286+ d := newTestDaemon()
287
288 s.vars = map[string]string{"uuid": "42"}
289 rsp := getOpInfo(operationCmd, nil).Self(nil, nil).(*resp)
290@@ -504,8 +498,7 @@
291 }
292
293 func (s *apiSuite) TestPostPackageBadRequest(c *check.C) {
294- d := New()
295- d.addRoutes()
296+ newTestDaemon()
297
298 s.vars = map[string]string{"uuid": "42"}
299 rsp := getOpInfo(operationCmd, nil).Self(nil, nil).(*resp)
300@@ -524,8 +517,7 @@
301 }
302
303 func (s *apiSuite) TestPostPackageBadAction(c *check.C) {
304- d := New()
305- d.addRoutes()
306+ newTestDaemon()
307
308 s.vars = map[string]string{"uuid": "42"}
309 c.Check(getOpInfo(operationCmd, nil).Self(nil, nil).(*resp).Status, check.Equals, http.StatusNotFound)
310@@ -542,8 +534,7 @@
311 }
312
313 func (s *apiSuite) TestPostPackage(c *check.C) {
314- d := New()
315- d.addRoutes()
316+ d := newTestDaemon()
317
318 s.vars = map[string]string{"uuid": "42"}
319 c.Check(getOpInfo(operationCmd, nil).Self(nil, nil).(*resp).Status, check.Equals, http.StatusNotFound)
320@@ -623,8 +614,7 @@
321 }
322
323 func (s *apiSuite) TestPackageGetConfig(c *check.C) {
324- d := New()
325- d.addRoutes()
326+ newTestDaemon()
327
328 req, err := http.NewRequest("GET", "/1.0/packages/foo.bar/config", bytes.NewBuffer(nil))
329 c.Assert(err, check.IsNil)
330@@ -688,8 +678,7 @@
331 }
332
333 func (s *apiSuite) TestPackagePutConfig(c *check.C) {
334- d := New()
335- d.addRoutes()
336+ newTestDaemon()
337
338 newConfigStr := "some other config"
339 req, err := http.NewRequest("PUT", "/1.0/packages/foo.bar/config", bytes.NewBufferString(newConfigStr))
340@@ -754,8 +743,7 @@
341 }
342
343 func (s *apiSuite) TestConfigMultiBadBody(c *check.C) {
344- d := New()
345- d.addRoutes()
346+ newTestDaemon()
347
348 req, err := http.NewRequest("PUT", "/1.0/packages", bytes.NewBuffer(nil))
349 c.Assert(err, check.IsNil)
350@@ -785,8 +773,7 @@
351 }
352
353 func (s *apiSuite) genericTestPackagePut(c *check.C, body io.Reader, concreteNo int, expected map[string]*configSubtask) {
354- d := New()
355- d.addRoutes()
356+ d := newTestDaemon()
357
358 req, err := http.NewRequest("PUT", "/1.0/packages", body)
359 c.Assert(err, check.IsNil)
360@@ -858,8 +845,7 @@
361 }
362
363 func (s *apiSuite) TestPackageServiceGet(c *check.C) {
364- d := New()
365- d.addRoutes()
366+ newTestDaemon()
367
368 findServices = func(string, string, progress.Meter) (snappy.ServiceActor, error) {
369 return &tSA{ssout: []*snappy.PackageServiceStatus{{ServiceName: "svc"}}}, nil
370@@ -884,8 +870,7 @@
371 }
372
373 func (s *apiSuite) TestPackageServicePut(c *check.C) {
374- d := New()
375- d.addRoutes()
376+ newTestDaemon()
377
378 findServices = func(string, string, progress.Meter) (snappy.ServiceActor, error) {
379 return &tSA{ssout: []*snappy.PackageServiceStatus{{ServiceName: "svc"}}}, nil
380@@ -953,8 +938,7 @@
381 }
382
383 func (s *apiSuite) TestServiceLogs(c *check.C) {
384- d := New()
385- d.addRoutes()
386+ newTestDaemon()
387
388 log := systemd.Log{
389 "__REALTIME_TIMESTAMP": "42",
390
391=== modified file 'daemon/daemon.go'
392--- daemon/daemon.go 2015-10-12 14:55:27 +0000
393+++ daemon/daemon.go 2015-10-15 11:44:27 +0000
394@@ -31,6 +31,7 @@
395 "github.com/gorilla/mux"
396 "gopkg.in/tomb.v2"
397
398+ "launchpad.net/snappy/daemon/mmutex"
399 "launchpad.net/snappy/logger"
400 )
401
402@@ -41,6 +42,7 @@
403 listener net.Listener
404 tomb tomb.Tomb
405 router *mux.Router
406+ mmutex mmutex.MMutex
407 }
408
409 // A ResponseFunc handles one of the individual verbs for a method
410@@ -124,6 +126,8 @@
411
412 d.addRoutes()
413
414+ d.mmutex = mmutex.New()
415+
416 logger.Debugf("init done in %s", time.Now().Sub(t0))
417
418 return nil
419
420=== modified file 'daemon/daemon_test.go'
421--- daemon/daemon_test.go 2015-10-05 23:10:59 +0000
422+++ daemon/daemon_test.go 2015-10-15 11:44:27 +0000
423@@ -23,15 +23,37 @@
424 "fmt"
425 "net/http"
426 "net/http/httptest"
427+ "testing"
428
429 "github.com/gorilla/mux"
430 "gopkg.in/check.v1"
431 )
432
433+// Hook up check.v1 into the "go test" runner
434+func Test(t *testing.T) { check.TestingT(t) }
435+
436 type daemonSuite struct{}
437
438 var _ = check.Suite(&daemonSuite{})
439
440+// nopMMutex satisfies mmutex.MMutex but does nothing
441+type nopMMutex struct{}
442+
443+func (nopMMutex) Lock(...string) {}
444+func (nopMMutex) RLock(...string) {}
445+func (nopMMutex) Unlock(...string) {}
446+func (nopMMutex) RUnlock(...string) {}
447+
448+// build a new daemon, with only a little of Init(), suitable for the tests
449+func newTestDaemon() *Daemon {
450+ d := New()
451+ d.addRoutes()
452+ d.mmutex = nopMMutex{}
453+
454+ return d
455+}
456+
457+// aResponse suitable for testing
458 type mockHandler struct {
459 cmd *Command
460 lastMethod string
461@@ -75,8 +97,7 @@
462 }
463
464 func (s *daemonSuite) TestAddRoutes(c *check.C) {
465- d := New()
466- d.addRoutes()
467+ d := newTestDaemon()
468
469 expected := make([]string, len(api))
470 for i, v := range api {
471
472=== added directory 'daemon/mmutex'
473=== added file 'daemon/mmutex/mmutex.go'
474--- daemon/mmutex/mmutex.go 1970-01-01 00:00:00 +0000
475+++ daemon/mmutex/mmutex.go 2015-10-15 11:44:27 +0000
476@@ -0,0 +1,213 @@
477+// -*- Mode: Go; indent-tabs-mode: t -*-
478+
479+/*
480+ * Copyright (C) 2015 Canonical Ltd
481+ *
482+ * This program is free software: you can redistribute it and/or modify
483+ * it under the terms of the GNU General Public License version 3 as
484+ * published by the Free Software Foundation.
485+ *
486+ * This program is distributed in the hope that it will be useful,
487+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
488+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
489+ * GNU General Public License for more details.
490+ *
491+ * You should have received a copy of the GNU General Public License
492+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
493+ *
494+ */
495+
496+package mmutex
497+
498+import (
499+ "strings"
500+ "sync"
501+
502+ "launchpad.net/snappy/dirs"
503+ "launchpad.net/snappy/priv"
504+)
505+
506+/*
507+
508+(R)Lock:
509+
510+1. lock self
511+2. lock global:
512+ a. globalCount == 0? actually lock global
513+ b. increase globalCount
514+3. get locks for "" and key
515+4. unlock self
516+5. key != "" ? -> RLock ""
517+6. (R)Lock key
518+
519+(R)Unlock:
520+
521+1. lock self
522+2. (R)Unlock key
523+3. key != "" ? -> RUnlock ""
524+4. unlock global:
525+ a. decrease globalCount
526+ b. 0 globalCount? actually unlock global
527+5. unlock self
528+
529+*/
530+
531+// privLocker is the interface of priv.Mutex that we use
532+type privLocker interface {
533+ Lock() error
534+ Unlock() error
535+}
536+
537+// newGlock calls priv.New; override it in tests to not panic when not
538+// root.
539+var newGlock = func() privLocker {
540+ return priv.New(dirs.SnapLockFile)
541+}
542+
543+type node struct {
544+ sync.RWMutex
545+ count uint
546+}
547+
548+// A MMutex is a map of mutexes, with a special "root" mutex that lets you
549+// Lock or RLock any of the non-root mutexes, but if the root mutex is
550+// Locked you'll have to wait for it.
551+type MMutex interface {
552+ Lock(...string)
553+ RLock(...string)
554+ Unlock(...string)
555+ RUnlock(...string)
556+}
557+
558+type mmutex struct {
559+ mutex sync.Mutex // who locks the locker? This guy.
560+ nodemap map[string]*node
561+ glock privLocker
562+ gcount uint
563+}
564+
565+// New is the mmutex constructor.
566+func New() MMutex {
567+ return &mmutex{
568+ nodemap: make(map[string]*node),
569+ }
570+}
571+
572+// acquires/increases count of the global lock
573+// global lock only needed to interop with non-rest-api snappy
574+func (lt *mmutex) lockGlobal() {
575+ if lt.gcount == 0 {
576+ lt.glock = newGlock()
577+ if err := lt.glock.Lock(); err != nil {
578+ panic(err)
579+ }
580+ }
581+ lt.gcount++
582+}
583+
584+// decreases count and releases the global lock
585+// global lock only needed to interop with non-rest-api snappy
586+func (lt *mmutex) unlockGlobal() {
587+ lt.gcount--
588+ if lt.gcount == 0 {
589+ if err := lt.glock.Unlock(); err != nil {
590+ panic(err)
591+ }
592+ // priv's Unlock sets the internal, private lock to nil;
593+ // might as well get rid of the lock ourselves as it's
594+ // useless at this point.
595+ lt.glock = nil
596+ }
597+}
598+
599+// convenience autovivifying getter
600+func (lt *mmutex) get(key string) *node {
601+ if _, ok := lt.nodemap[key]; !ok {
602+ lt.nodemap[key] = &node{}
603+ }
604+
605+ return lt.nodemap[key]
606+}
607+
608+// get the "root" lock (lock for "") and the lock for key
609+func (lt *mmutex) rootNNode(key string) (root *node, node *node) {
610+ lt.mutex.Lock()
611+ defer lt.mutex.Unlock()
612+
613+ lt.lockGlobal()
614+
615+ root = lt.get("")
616+ node = lt.get(key)
617+
618+ if key != "" {
619+ node.count++
620+ }
621+
622+ return root, node
623+}
624+
625+// Lock the mutex for the given key. If the optional args are given, the
626+// root mutex is RLocked; otherwise, the root mutex itself is Locked.
627+func (lt *mmutex) Lock(args ...string) {
628+ key := strings.Join(args, ".")
629+ root, node := lt.rootNNode(key)
630+
631+ if key != "" {
632+ root.RLock()
633+ }
634+
635+ node.Lock()
636+}
637+
638+// Unlock the mutex for the given key, and RUnlock the root mutex if args
639+// are given.
640+func (lt *mmutex) Unlock(args ...string) {
641+ key := strings.Join(args, ".")
642+ lt.mutex.Lock()
643+ defer lt.mutex.Unlock()
644+
645+ node := lt.get(key)
646+ node.Unlock()
647+ if key != "" {
648+ lt.get("").RUnlock()
649+ node.count--
650+ if node.count == 0 {
651+ delete(lt.nodemap, key)
652+ }
653+ }
654+
655+ lt.unlockGlobal()
656+}
657+
658+// RLock the mutex for the given key; also RLock the root mutex if args are
659+// given.
660+func (lt *mmutex) RLock(args ...string) {
661+ key := strings.Join(args, ".")
662+ root, node := lt.rootNNode(key)
663+
664+ if key != "" {
665+ root.RLock()
666+ }
667+
668+ node.RLock()
669+}
670+
671+// RUnlock the mutex for the given key; also RUnlock the root mutex if args
672+// are given.
673+func (lt *mmutex) RUnlock(args ...string) {
674+ key := strings.Join(args, ".")
675+ lt.mutex.Lock()
676+ defer lt.mutex.Unlock()
677+
678+ node := lt.get(key)
679+ node.RUnlock()
680+ if key != "" {
681+ lt.get("").RUnlock()
682+ node.count--
683+ if node.count == 0 {
684+ delete(lt.nodemap, key)
685+ }
686+ }
687+
688+ lt.unlockGlobal()
689+}
690
691=== added file 'daemon/mmutex/mmutex_test.go'
692--- daemon/mmutex/mmutex_test.go 1970-01-01 00:00:00 +0000
693+++ daemon/mmutex/mmutex_test.go 2015-10-15 11:44:27 +0000
694@@ -0,0 +1,274 @@
695+// -*- Mode: Go; indent-tabs-mode: t -*-
696+
697+/*
698+ * Copyright (C) 2015 Canonical Ltd
699+ *
700+ * This program is free software: you can redistribute it and/or modify
701+ * it under the terms of the GNU General Public License version 3 as
702+ * published by the Free Software Foundation.
703+ *
704+ * This program is distributed in the hope that it will be useful,
705+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
706+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
707+ * GNU General Public License for more details.
708+ *
709+ * You should have received a copy of the GNU General Public License
710+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
711+ *
712+ */
713+
714+package mmutex
715+
716+import (
717+ "errors"
718+ "sync"
719+ "testing"
720+ "time"
721+
722+ "gopkg.in/check.v1"
723+)
724+
725+// Hook up check.v1 into the "go test" runner
726+func Test(t *testing.T) { check.TestingT(t) }
727+
728+type mmutexSuite struct {
729+ oldGlock func() privLocker
730+ l *muLck
731+}
732+
733+var _ = check.Suite(&mmutexSuite{})
734+
735+type muLck struct {
736+ m sync.Mutex
737+ e error
738+}
739+
740+func (mu *muLck) Lock() error { mu.m.Lock(); return mu.e }
741+func (mu *muLck) Unlock() error { mu.m.Unlock(); return mu.e }
742+
743+func (s *mmutexSuite) SetUpTest(c *check.C) {
744+ s.oldGlock = newGlock
745+ s.l = &muLck{}
746+ newGlock = func() privLocker {
747+ return s.l
748+ }
749+}
750+
751+func (s *mmutexSuite) TearDownTest(c *check.C) {
752+ newGlock = s.oldGlock
753+}
754+
755+func (s *mmutexSuite) TestSimultaneousLocks(c *check.C) {
756+ ch := make(chan bool)
757+
758+ mm := New().(*mmutex)
759+ go func() {
760+ mm.Lock("foo")
761+ defer mm.Unlock("foo")
762+ mm.Lock("bar")
763+ defer mm.Unlock("bar")
764+ c.Check(mm.nodemap, check.HasLen, 3)
765+ c.Check(mm.nodemap[""], check.NotNil)
766+ c.Check(mm.nodemap["foo"], check.NotNil)
767+ c.Check(mm.nodemap["bar"], check.NotNil)
768+
769+ ch <- true
770+ }()
771+ select {
772+ case <-time.After(time.Second):
773+ c.Fatalf("timed out")
774+ case <-ch:
775+ }
776+
777+ c.Check(mm.nodemap, check.HasLen, 1)
778+ c.Check(mm.nodemap[""], check.NotNil)
779+}
780+
781+func (s *mmutexSuite) TestSimultaneousRLocks(c *check.C) {
782+ ch := make(chan bool)
783+
784+ mm := New().(*mmutex)
785+ go func() {
786+ mm.RLock("foo")
787+ defer mm.RUnlock("foo")
788+ mm.RLock("bar")
789+ defer mm.RUnlock("bar")
790+ c.Check(mm.nodemap, check.HasLen, 3)
791+ c.Check(mm.nodemap[""], check.NotNil)
792+ c.Check(mm.nodemap["foo"], check.NotNil)
793+ c.Check(mm.nodemap["bar"], check.NotNil)
794+
795+ ch <- true
796+ }()
797+ select {
798+ case <-time.After(time.Second):
799+ c.Fatalf("timed out")
800+ case <-ch:
801+ }
802+
803+ c.Check(mm.nodemap, check.HasLen, 1)
804+ c.Check(mm.nodemap[""], check.NotNil)
805+}
806+
807+func (s *mmutexSuite) TestSimultaneousRLockWRoot(c *check.C) {
808+ ch := make(chan bool)
809+
810+ mm := New().(*mmutex)
811+ go func() {
812+ mm.RLock()
813+ defer mm.RUnlock()
814+ mm.RLock("foo")
815+ defer mm.RUnlock("foo")
816+ c.Check(mm.nodemap, check.HasLen, 2)
817+ c.Check(mm.nodemap[""], check.NotNil)
818+ c.Check(mm.nodemap["foo"], check.NotNil)
819+
820+ ch <- true
821+ }()
822+ select {
823+ case <-time.After(time.Second):
824+ c.Fatalf("timed out")
825+ case <-ch:
826+ }
827+
828+ c.Check(mm.nodemap, check.HasLen, 1)
829+ c.Check(mm.nodemap[""], check.NotNil)
830+}
831+
832+func (s *mmutexSuite) TestConcurrentRootLockWRLock(c *check.C) {
833+ ch := make(chan bool)
834+ buf := make(chan bool, 10)
835+
836+ mm := New().(*mmutex)
837+ go func() {
838+ mm.RLock("foo")
839+ <-ch
840+ defer mm.RUnlock("foo")
841+
842+ time.Sleep(100 * time.Millisecond)
843+
844+ buf <- false
845+ }()
846+
847+ ch <- true // waits for mm.RLock("foo")
848+
849+ go func() {
850+ mm.Lock()
851+ defer mm.Unlock()
852+
853+ buf <- true
854+ }()
855+
856+ c.Check(<-buf, check.Equals, false)
857+ c.Check(<-buf, check.Equals, true)
858+
859+ c.Check(mm.nodemap, check.HasLen, 1)
860+ c.Check(mm.nodemap[""], check.NotNil)
861+}
862+
863+func (s *mmutexSuite) TestConcurrentRLockWRootLock(c *check.C) {
864+ ch := make(chan bool)
865+ buf := make(chan bool, 10)
866+
867+ mm := New().(*mmutex)
868+ go func() {
869+ mm.Lock()
870+ defer mm.Unlock()
871+ <-ch
872+
873+ time.Sleep(100 * time.Millisecond)
874+
875+ buf <- true
876+ }()
877+
878+ ch <- true // waits for mm.Lock()
879+
880+ go func() {
881+ mm.RLock("foo")
882+ defer mm.RUnlock("foo")
883+
884+ buf <- false
885+ }()
886+
887+ c.Check(<-buf, check.Equals, true)
888+ c.Check(<-buf, check.Equals, false)
889+
890+ c.Check(mm.nodemap, check.HasLen, 1)
891+ c.Check(mm.nodemap[""], check.NotNil)
892+}
893+
894+func (s *mmutexSuite) TestConcurrentRootLockWLock(c *check.C) {
895+ ch := make(chan bool)
896+ buf := make(chan bool, 10)
897+
898+ mm := New().(*mmutex)
899+ go func() {
900+ mm.Lock("foo")
901+ <-ch
902+ defer mm.Unlock("foo")
903+
904+ time.Sleep(100 * time.Millisecond)
905+
906+ buf <- false
907+ }()
908+
909+ ch <- true // waits for mm.Lock("foo")
910+
911+ go func() {
912+ mm.Lock()
913+ defer mm.Unlock()
914+
915+ buf <- true
916+ }()
917+
918+ c.Check(<-buf, check.Equals, false)
919+ c.Check(<-buf, check.Equals, true)
920+
921+ c.Check(mm.nodemap, check.HasLen, 1)
922+ c.Check(mm.nodemap[""], check.NotNil)
923+}
924+
925+func (s *mmutexSuite) TestConcurrentLockWRootLock(c *check.C) {
926+ ch := make(chan bool)
927+ buf := make(chan bool, 10)
928+
929+ mm := New().(*mmutex)
930+ go func() {
931+ mm.Lock()
932+ <-ch
933+ defer mm.Unlock()
934+
935+ time.Sleep(100 * time.Millisecond)
936+
937+ buf <- true
938+ }()
939+
940+ ch <- true // waits for mm.Lock()
941+
942+ go func() {
943+ mm.Lock("foo")
944+ defer mm.Unlock("foo")
945+
946+ buf <- false
947+ }()
948+
949+ c.Check(<-buf, check.Equals, true)
950+ c.Check(<-buf, check.Equals, false)
951+
952+ c.Check(mm.nodemap, check.HasLen, 1)
953+ c.Check(mm.nodemap[""], check.NotNil)
954+}
955+
956+func (s *mmutexSuite) TestLockPanicsIfGlobalLockFails(c *check.C) {
957+ s.l.e = errors.New("failed")
958+ mm := New().(*mmutex)
959+ c.Check(func() { mm.Lock() }, check.Panics, s.l.e)
960+}
961+
962+func (s *mmutexSuite) TestUnlockPanicsIfGlobalUnlockFails(c *check.C) {
963+ mm := New().(*mmutex)
964+ mm.Lock()
965+ s.l.e = errors.New("failed")
966+
967+ c.Check(func() { mm.Unlock() }, check.Panics, s.l.e)
968+}
969
970=== modified file 'dirs/dirs.go'
971--- dirs/dirs.go 2015-09-25 15:27:11 +0000
972+++ dirs/dirs.go 2015-10-15 11:44:27 +0000
973@@ -35,6 +35,7 @@
974 LocaleDir string
975 SnapIconsDir string
976 SnapMetaDir string
977+ SnapLockFile string
978
979 SnapBinariesDir string
980 SnapServicesDir string
981@@ -59,6 +60,7 @@
982 SnapSeccompDir = filepath.Join(rootdir, SnappyDir, "seccomp", "profiles")
983 SnapIconsDir = filepath.Join(rootdir, SnappyDir, "icons")
984 SnapMetaDir = filepath.Join(rootdir, SnappyDir, "meta")
985+ SnapLockFile = filepath.Join(rootdir, "/run/snappy.lock")
986
987 SnapBinariesDir = filepath.Join(SnapAppsDir, "bin")
988 SnapServicesDir = filepath.Join(rootdir, "/etc/systemd/system")

Subscribers

People subscribed via source and target branches