Merge lp:~jtv/juju-core/generic-locking-test into lp:~go-bot/juju-core/trunk

Proposed by Jeroen T. Vermeulen
Status: Merged
Approved by: Jeroen T. Vermeulen
Approved revision: no longer in the source branch.
Merged at revision: 1314
Proposed branch: lp:~jtv/juju-core/generic-locking-test
Merge into: lp:~go-bot/juju-core/trunk
Diff against target: 259 lines (+201/-1)
4 files modified
environs/azure/environ.go (+34/-1)
environs/azure/environ_test.go (+51/-0)
testing/locking.go (+71/-0)
testing/locking_test.go (+45/-0)
To merge this branch: bzr merge lp:~jtv/juju-core/generic-locking-test
Reviewer Review Type Date Requested Status
Tim Penhey (community) Approve
Review via email: mp+170477@code.launchpad.net

Commit message

Helper: test a function for correct locking.

This is the outcome of discussion at
https://code.launchpad.net/~jtv/juju-core/create-gwacl-sessions/+merge/170032
but produces a helper that is much, much simpler. It could become generic,
although one thing is still missing: it currently only works for locks that
are of type sync.Mutex. It probably ought to work for any type of lock.

Failure of a function to acquire the right lock(s) is a constant risk in
concurrent programs, so one of the things most worth testing, but it's also
relatively hard to test for and so nobody ever does it. Of course if you
can forget the locking you can also forget the test, but this helper makes it
very easy to guard against regressions.

Description of the change

Helper: test a function for correct locking.

This is the outcome of discussion at
https://code.launchpad.net/~jtv/juju-core/create-gwacl-sessions/+merge/170032
but produces a helper that is much, much simpler. It could become generic,
although one thing is still missing: it currently only works for locks that
are of type sync.Mutex. It probably ought to work for any type of lock.

Failure of a function to acquire the right lock(s) is a constant risk in
concurrent programs, so one of the things most worth testing, but it's also
relatively hard to test for and so nobody ever does it. Of course if you
can forget the locking you can also forget the test, but this helper makes it
very easy to guard against regressions.

https://codereview.appspot.com/10433043/

To post a comment you must log in.
Revision history for this message
Tim Penhey (thumper) wrote :

LGTM for what it is, but might be nice to have as a checker.

https://codereview.appspot.com/10433043/diff/1/environs/azure/environ_test.go
File environs/azure/environ_test.go (right):

https://codereview.appspot.com/10433043/diff/1/environs/azure/environ_test.go#newcode39
environs/azure/environ_test.go:39: func testLockingFunction(lock
*sync.Mutex, function func()) {
If this is to be a generic testing function, you should move it to the
testing package and export it.

Here is a thought. Want to write this as a checker?

c.Assert(some_func, ObservesLockOn(some_mutex)) ?

https://codereview.appspot.com/10433043/diff/1/environs/azure/environ_test.go#newcode60
environs/azure/environ_test.go:60: // misbehaved "function" plenty of
rope to hang itself.
You could have a few sleep(0) type calls, as this should do the same
thing.

https://codereview.appspot.com/10433043/diff/1/environs/azure/environ_test.go#newcode74
environs/azure/environ_test.go:74: blankLock := sync.Mutex{}
Wow, surprised there is not a way to ask if a mutex is locked.

https://codereview.appspot.com/10433043/

Revision history for this message
Tim Penhey (thumper) :
review: Approve
Revision history for this message
John A Meinel (jameinel) wrote :

LGTM though maybe it could be tweaked a bit.

https://codereview.appspot.com/10433043/diff/1/environs/azure/environ_test.go
File environs/azure/environ_test.go (right):

https://codereview.appspot.com/10433043/diff/1/environs/azure/environ_test.go#newcode57
environs/azure/environ_test.go:57: <-proceed
You could probably buffer proceed so that function gets a bit more time
to hang itself. We don't need 'function' to *wait* until the main loop
gets to this point, so buffering the channel makes sense to me.

https://codereview.appspot.com/10433043/diff/1/environs/azure/environ_test.go#newcode60
environs/azure/environ_test.go:60: // misbehaved "function" plenty of
rope to hang itself.
On 2013/06/20 02:47:22, thumper wrote:
> You could have a few sleep(0) type calls, as this should do the same
thing.

runtime.GoSched exists in 1.1, and we want to support using 1.1 (just
not require it).
Is there a reason you can't just add those calls?

https://codereview.appspot.com/10433043/diff/1/environs/azure/environ_test.go#newcode70
environs/azure/environ_test.go:70: panic(errors.New("function did not
obey lock"))
Rather than panic to indicate failures, you could have this function
take a c *C and then call c.Assert instead.

Or use Tim's idea and make it a Checker.

https://codereview.appspot.com/10433043/diff/1/environs/azure/environ_test.go#newcode76
environs/azure/environ_test.go:76: panic(errors.New("function did not
release lock"))
I would think that supporting the Locker interface might be more useful
than this final assert. I guess the tradeoff is that you would have to
do something like try to acquire the lock, which potentially leaves you
with a hanging test. (Right now if function doesn't release the lock,
you get a panic, the alternative would give you a hanging test).

I just know a fair amount of code uses a RWMutex rather than only a
plain Mutex, and it seems a shame to need to have 2 code paths for
something so similar.

Also, you could use runtime type assertions to still confirm the Mutex
behavior. Just use:

if mutex, ok := lock.(*sync.Mutex); ok {
   blankMutex := sync.Mutex{}
   ...
}

Or even a:

switch lock.(type) {
   case *sync.Mutex:
    ...
   case *sync.RWMutex:
    ...
}

https://codereview.appspot.com/10433043/

Revision history for this message
Dave Cheney (dave-cheney) wrote :

Umm, race detector anyone ?

On Thu, Jun 20, 2013 at 11:43 PM, <email address hidden> wrote:

> The proposal to merge lp:~jtv/juju-core/generic-locking-test into
> lp:juju-core has been updated.
>
> Status: Approved => Merged
>
> For more details, see:
>
> https://code.launchpad.net/~jtv/juju-core/generic-locking-test/+merge/170477
> --
>
> https://code.launchpad.net/~jtv/juju-core/generic-locking-test/+merge/170477
> You are subscribed to branch lp:juju-core.
>

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'environs/azure/environ.go'
2--- environs/azure/environ.go 2013-06-18 09:37:20 +0000
3+++ environs/azure/environ.go 2013-06-20 09:42:24 +0000
4@@ -4,6 +4,7 @@
5 package azure
6
7 import (
8+ "launchpad.net/gwacl"
9 "launchpad.net/juju-core/constraints"
10 "launchpad.net/juju-core/environs"
11 "launchpad.net/juju-core/environs/config"
12@@ -64,7 +65,8 @@
13
14 // Config is specified in the Environ interface.
15 func (env *azureEnviron) Config() *config.Config {
16- panic("unimplemented")
17+ snap := env.getSnapshot()
18+ return snap.ecfg.Config
19 }
20
21 // SetConfig is specified in the Environ interface.
22@@ -126,3 +128,34 @@
23 func (env *azureEnviron) Provider() environs.EnvironProvider {
24 panic("unimplemented")
25 }
26+
27+// TODO: Temporarily deactivating this code. Passing certificate in-memory
28+// may require gwacl change.
29+/*
30+// getManagementAPI obtains a context object for interfacing with Azure's
31+// management API.
32+// For now, each invocation just returns a separate object. This is probably
33+// wasteful (each context gets its own SSL connection) and may need optimizing
34+// later.
35+func (env *azureEnviron) getManagementAPI() (*gwacl.ManagementAPI, error) {
36+ snap := env.getSnapshot()
37+ subscription := snap.ecfg.ManagementSubscriptionId()
38+ cert := snap.ecfg.ManagementCertificate()
39+ return gwacl.NewManagementAPI(subscription, cert)
40+}
41+*/
42+
43+// getStorageContext obtains a context object for interfacing with Azure's
44+// storage API.
45+// For now, each invocation just returns a separate object. This is probably
46+// wasteful (each context gets its own SSL connection) and may need optimizing
47+// later.
48+func (env *azureEnviron) getStorageContext() (*gwacl.StorageContext, error) {
49+ snap := env.getSnapshot()
50+ context := gwacl.StorageContext{
51+ Account: snap.ecfg.StorageAccountName(),
52+ Key: snap.ecfg.StorageAccountKey(),
53+ }
54+ // There is currently no way for this to fail.
55+ return &context, nil
56+}
57
58=== modified file 'environs/azure/environ_test.go'
59--- environs/azure/environ_test.go 2013-06-17 10:24:42 +0000
60+++ environs/azure/environ_test.go 2013-06-20 09:42:24 +0000
61@@ -5,6 +5,8 @@
62
63 import (
64 . "launchpad.net/gocheck"
65+ "launchpad.net/juju-core/environs/config"
66+ "launchpad.net/juju-core/testing"
67 "sync"
68 )
69
70@@ -14,6 +16,18 @@
71
72 var _ = Suite(new(EnvironSuite))
73
74+func makeEnviron(c *C) *azureEnviron {
75+ attrs := makeAzureConfigMap(c)
76+ cfg, err := config.New(attrs)
77+ c.Assert(err, IsNil)
78+ ecfg, err := azureEnvironProvider{}.newConfig(cfg)
79+ c.Assert(err, IsNil)
80+ return &azureEnviron{
81+ name: "env",
82+ ecfg: ecfg,
83+ }
84+}
85+
86 func (EnvironSuite) TestGetSnapshot(c *C) {
87 original := azureEnviron{name: "this-env", ecfg: new(azureEnvironConfig)}
88 snapshot := original.getSnapshot()
89@@ -32,7 +46,44 @@
90 c.Check(snapshot.Mutex, Equals, sync.Mutex{})
91 }
92
93+func (EnvironSuite) TestGetSnapshotLocksEnviron(c *C) {
94+ original := azureEnviron{}
95+ testing.TestLockingFunction(&original.Mutex, func() { original.getSnapshot() })
96+}
97+
98 func (EnvironSuite) TestName(c *C) {
99 env := azureEnviron{name: "foo"}
100 c.Check(env.Name(), Equals, env.name)
101 }
102+
103+func (EnvironSuite) TestConfigReturnsConfig(c *C) {
104+ cfg := new(config.Config)
105+ ecfg := azureEnvironConfig{Config: cfg}
106+ env := azureEnviron{ecfg: &ecfg}
107+ c.Check(env.Config(), Equals, cfg)
108+}
109+
110+func (EnvironSuite) TestConfigLocksEnviron(c *C) {
111+ env := azureEnviron{name: "env", ecfg: new(azureEnvironConfig)}
112+ testing.TestLockingFunction(&env.Mutex, func() { env.Config() })
113+}
114+
115+// TODO: Temporarily deactivating this code. Passing certificate in-memory
116+// may require gwacl change.
117+/*
118+func (EnvironSuite) TestGetManagementAPI(c *C) {
119+ env := makeEnviron(c)
120+ management, err := env.getManagementAPI()
121+ c.Assert(err, IsNil)
122+ c.Check(management, NotNil)
123+}
124+*/
125+
126+func (EnvironSuite) TestGetStorageContext(c *C) {
127+ env := makeEnviron(c)
128+ storage, err := env.getStorageContext()
129+ c.Assert(err, IsNil)
130+ c.Assert(storage, NotNil)
131+ c.Check(storage.Account, Equals, env.ecfg.StorageAccountName())
132+ c.Check(storage.Key, Equals, env.ecfg.StorageAccountKey())
133+}
134
135=== added file 'testing/locking.go'
136--- testing/locking.go 1970-01-01 00:00:00 +0000
137+++ testing/locking.go 2013-06-20 09:42:24 +0000
138@@ -0,0 +1,71 @@
139+// Copyright 2013 Canonical Ltd.
140+// Licensed under the AGPLv3, see LICENCE file for details.
141+
142+package testing
143+
144+import (
145+ "errors"
146+ "sync"
147+ "time"
148+)
149+
150+// TestLockingFunction verifies that a function obeys a given lock.
151+//
152+// Use this as a building block in your own tests for proper locking.
153+// Parameters are a lock that you expect your function to block on, and
154+// the function that you want to test for proper locking on that lock.
155+//
156+// This helper attempts to verify that the function both obtains and releases
157+// the lock. It will panic if the function fails to do either.
158+// TODO: Support generic sync.Locker instead of just Mutex.
159+// TODO: This could be a gocheck checker.
160+func TestLockingFunction(lock *sync.Mutex, function func()) {
161+ // We record two events that must happen in the right order.
162+ // Buffer the channel so that we don't get hung up during attempts
163+ // to push the events in.
164+ events := make(chan string, 2)
165+ // Synchronization channel, to make sure that the function starts
166+ // trying to run at the point where we're going to make it block.
167+ proceed := make(chan bool, 1)
168+
169+ goroutine := func() {
170+ proceed <- true
171+ function()
172+ events <- "complete function"
173+ }
174+
175+ lock.Lock()
176+ go goroutine()
177+ // Make the goroutine start now. It should block inside "function()."
178+ // (It's fine, technically even better, if the goroutine started right
179+ // away, and this channel is buffered specifically so that it can.)
180+ <-proceed
181+
182+ // Give a misbehaved function plenty of rope to hang itself. We don't
183+ // want it to block for a microsecond, hand control back to here so we
184+ // think it's stuck on the lock, and then later continue on its merry
185+ // lockless journey to finish last, as expected but for the wrong
186+ // reason.
187+ for counter := 0; counter < 10; counter++ {
188+ // TODO: In Go 1.1, use runtime.GoSched instead.
189+ time.Sleep(0)
190+ }
191+
192+ // Unblock the goroutine.
193+ events <- "release lock"
194+ lock.Unlock()
195+
196+ // Now that we've released the lock, the function is unblocked. Read
197+ // the 2 events. (This will wait until the function has completed.)
198+ firstEvent := <-events
199+ secondEvent := <-events
200+ if firstEvent != "release lock" || secondEvent != "complete function" {
201+ panic(errors.New("function did not obey lock"))
202+ }
203+
204+ // Also, the function must have released the lock.
205+ blankLock := sync.Mutex{}
206+ if *lock != blankLock {
207+ panic(errors.New("function did not release lock"))
208+ }
209+}
210
211=== added file 'testing/locking_test.go'
212--- testing/locking_test.go 1970-01-01 00:00:00 +0000
213+++ testing/locking_test.go 2013-06-20 09:42:24 +0000
214@@ -0,0 +1,45 @@
215+// Copyright 2013 Canonical Ltd.
216+// Licensed under the AGPLv3, see LICENCE file for details.
217+
218+package testing
219+
220+import (
221+ "errors"
222+ . "launchpad.net/gocheck"
223+ "sync"
224+)
225+
226+type LockingSuite struct{}
227+
228+var _ = Suite(&LockingSuite{})
229+
230+func (LockingSuite) TestTestLockingFunctionPassesCorrectLock(c *C) {
231+ lock := sync.Mutex{}
232+ function := func() {
233+ lock.Lock()
234+ lock.Unlock()
235+ }
236+ // TestLockingFunction succeeds.
237+ TestLockingFunction(&lock, function)
238+}
239+
240+func (LockingSuite) TestTestLockingFunctionDetectsDisobeyedLock(c *C) {
241+ lock := sync.Mutex{}
242+ function := func() {}
243+ c.Check(
244+ func() { TestLockingFunction(&lock, function) },
245+ Panics,
246+ errors.New("function did not obey lock"))
247+}
248+
249+func (LockingSuite) TestTestLockingFunctionDetectsFailureToReleaseLock(c *C) {
250+ lock := sync.Mutex{}
251+ defer lock.Unlock()
252+ function := func() {
253+ lock.Lock()
254+ }
255+ c.Check(
256+ func() { TestLockingFunction(&lock, function) },
257+ Panics,
258+ errors.New("function did not release lock"))
259+}

Subscribers

People subscribed via source and target branches

to status/vote changes: