Merge lp:~thumper/juju-core/juju-run-just-lock into lp:~go-bot/juju-core/trunk

Proposed by Tim Penhey
Status: Merged
Approved by: Tim Penhey
Approved revision: no longer in the source branch.
Merged at revision: 2205
Proposed branch: lp:~thumper/juju-core/juju-run-just-lock
Merge into: lp:~go-bot/juju-core/trunk
Prerequisite: lp:~thumper/juju-core/run-cmd-refactor
Diff against target: 477 lines (+181/-66)
8 files modified
cmd/jujud/run.go (+63/-21)
cmd/jujud/run_test.go (+78/-9)
state/apiserver/client/run.go (+5/-2)
state/apiserver/client/run_test.go (+3/-3)
utils/fslock/export_test.go (+0/-15)
utils/fslock/fslock.go (+7/-5)
utils/fslock/fslock_test.go (+1/-11)
utils/fslock/package_test.go (+24/-0)
To merge this branch: bzr merge lp:~thumper/juju-core/juju-run-just-lock
Reviewer Review Type Date Requested Status
Juju Engineering Pending
Review via email: mp+201702@code.launchpad.net

Commit message

juju run should always use hook execution lock

When juju run executes remotely, for services and
units it was grabbing the file system lock for the
hook execution to make sure things are serialized
appropriately.

This branch makes sure that the machine executions
also use the file system lock to make sure the
commands are serialized.

https://codereview.appspot.com/52470044/

Description of the change

juju run should always use hook execution lock

When juju run executes remotely, for services and
units it was grabbing the file system lock for the
hook execution to make sure things are serialized
appropriately.

This branch makes sure that the machine executions
also use the file system lock to make sure the
commands are serialized.

https://codereview.appspot.com/52470044/

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

Reviewers: mp+201702_code.launchpad.net,

Message:
Please take a look.

Description:
juju run should always use hook execution lock

When juju run executes remotely, for services and
units it was grabbing the file system lock for the
hook execution to make sure things are serialized
appropriately.

This branch makes sure that the machine executions
also use the file system lock to make sure the
commands are serialized.

https://code.launchpad.net/~thumper/juju-core/juju-run-just-lock/+merge/201702

Requires:
https://code.launchpad.net/~thumper/juju-core/run-cmd-refactor/+merge/201680

(do not edit description out of merge proposal)

Please review this at https://codereview.appspot.com/52470044/

Affected files (+183, -66 lines):
   A [revision details]
   M cmd/jujud/run.go
   M cmd/jujud/run_test.go
   M state/apiserver/client/run.go
   M state/apiserver/client/run_test.go
   D utils/fslock/export_test.go
   M utils/fslock/fslock.go
   M utils/fslock/fslock_test.go
   A utils/fslock/package_test.go

Revision history for this message
Ian Booth (wallyworld) wrote :

LGTM with some consideration to my questions

https://codereview.appspot.com/52470044/diff/1/cmd/jujud/run.go
File cmd/jujud/run.go (right):

https://codereview.appspot.com/52470044/diff/1/cmd/jujud/run.go#newcode42
cmd/jujud/run.go:42: If --no-context is specified, the <unit-name>
positional
Not sure if --no-unit would be better here from a user perspective?

https://codereview.appspot.com/52470044/diff/1/cmd/jujud/run.go#newcode61
cmd/jujud/run.go:61: f.BoolVar(&c.noContext, "no-context", false, "do
not run the command in a unit context")
--no-unit???

https://codereview.appspot.com/52470044/diff/1/cmd/jujud/run_test.go
File cmd/jujud/run_test.go (right):

https://codereview.appspot.com/52470044/diff/1/cmd/jujud/run_test.go#newcode138
cmd/jujud/run_test.go:138: s.PatchValue(&fslock.LockWaitDelay,
10*time.Millisecond)
should this be ShortWait?

https://codereview.appspot.com/52470044/diff/1/cmd/jujud/run_test.go#newcode146
cmd/jujud/run_test.go:146: c.Assert(err, gc.ErrorMatches, "timeout")
What happens if this assert fails? The lock will still be held. Will
that adversely affect other tests? I think we should either add cleanup
or change the Assert to Check

https://codereview.appspot.com/52470044/

Revision history for this message
Tim Penhey (thumper) wrote :

https://codereview.appspot.com/52470044/diff/1/cmd/jujud/run.go
File cmd/jujud/run.go (right):

https://codereview.appspot.com/52470044/diff/1/cmd/jujud/run.go#newcode42
cmd/jujud/run.go:42: If --no-context is specified, the <unit-name>
positional
On 2014/01/15 02:03:18, wallyworld wrote:
> Not sure if --no-unit would be better here from a user perspective?

Users don't see this. This is only really used by the juju run
apiserver component.

https://codereview.appspot.com/52470044/diff/1/cmd/jujud/run_test.go
File cmd/jujud/run_test.go (right):

https://codereview.appspot.com/52470044/diff/1/cmd/jujud/run_test.go#newcode138
cmd/jujud/run_test.go:138: s.PatchValue(&fslock.LockWaitDelay,
10*time.Millisecond)
On 2014/01/15 02:03:18, wallyworld wrote:
> should this be ShortWait?

No, I wanted to make sure that it checked faster than short wait,
otherwise there was the potential that after unlocking, it didn't have
enough time to execute fully.

https://codereview.appspot.com/52470044/diff/1/cmd/jujud/run_test.go#newcode146
cmd/jujud/run_test.go:146: c.Assert(err, gc.ErrorMatches, "timeout")
On 2014/01/15 02:03:18, wallyworld wrote:
> What happens if this assert fails? The lock will still be held. Will
that
> adversely affect other tests? I think we should either add cleanup or
change the
> Assert to Check

No, the lock is in the LockDir, which is mocked out above in a temporary
test directory.

https://codereview.appspot.com/52470044/

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'cmd/jujud/run.go'
2--- cmd/jujud/run.go 2014-01-15 01:05:05 +0000
3+++ cmd/jujud/run.go 2014-01-15 01:05:06 +0000
4@@ -14,16 +14,21 @@
5 "launchpad.net/juju-core/cmd"
6 "launchpad.net/juju-core/names"
7 "launchpad.net/juju-core/utils/exec"
8+ "launchpad.net/juju-core/utils/fslock"
9 "launchpad.net/juju-core/worker/uniter"
10 )
11
12-var AgentDir = "/var/lib/juju/agents"
13+var (
14+ AgentDir = "/var/lib/juju/agents"
15+ LockDir = "/var/lib/juju/locks"
16+)
17
18 type RunCommand struct {
19 cmd.CommandBase
20- unit string
21- commands string
22- showHelp bool
23+ unit string
24+ commands string
25+ showHelp bool
26+ noContext bool
27 }
28
29 const runCommandDoc = `
30@@ -34,6 +39,9 @@
31 or the unit id:
32 i.e. ubuntu/0
33
34+If --no-context is specified, the <unit-name> positional
35+argument is not needed.
36+
37 The commands are executed with '/bin/bash -s', and the output returned.
38 `
39
40@@ -50,6 +58,7 @@
41 func (c *RunCommand) SetFlags(f *gnuflag.FlagSet) {
42 f.BoolVar(&c.showHelp, "h", false, "show help on juju-run")
43 f.BoolVar(&c.showHelp, "help", false, "")
44+ f.BoolVar(&c.noContext, "no-context", false, "do not run the command in a unit context")
45 }
46
47 func (c *RunCommand) Init(args []string) error {
48@@ -57,19 +66,21 @@
49 if contextId, err := getenv("JUJU_CONTEXT_ID"); err == nil && contextId != "" {
50 return fmt.Errorf("juju-run cannot be called from within a hook, have context %q", contextId)
51 }
52+ if !c.noContext {
53+ if len(args) < 1 {
54+ return fmt.Errorf("missing unit-name")
55+ }
56+ c.unit, args = args[0], args[1:]
57+ // If the command line param is a unit id (like service/2) we need to
58+ // change it to the unit tag as that is the format of the agent directory
59+ // on disk (unit-service-2).
60+ if names.IsUnit(c.unit) {
61+ c.unit = names.UnitTag(c.unit)
62+ }
63+ }
64 if len(args) < 1 {
65- return fmt.Errorf("missing unit-name")
66- }
67- if len(args) < 2 {
68 return fmt.Errorf("missing commands")
69 }
70- c.unit, args = args[0], args[1:]
71- // If the command line param is a unit id (like service/2) we need to
72- // change it to the unit tag as that is the format of the agent directory
73- // on disk (unit-service-2).
74- if names.IsUnit(c.unit) {
75- c.unit = names.UnitTag(c.unit)
76- }
77 c.commands, args = args[0], args[1:]
78 return cmd.CheckEmpty(args)
79 }
80@@ -79,31 +90,62 @@
81 return gnuflag.ErrHelp
82 }
83
84+ var result *exec.ExecResponse
85+ var err error
86+ if c.noContext {
87+ result, err = c.executeNoContext()
88+ } else {
89+ result, err = c.executeInUnitContext()
90+ }
91+ if err != nil {
92+ return err
93+ }
94+
95+ ctx.Stdout.Write(result.Stdout)
96+ ctx.Stderr.Write(result.Stderr)
97+ return cmd.NewRcPassthroughError(result.Code)
98+}
99+
100+func (c *RunCommand) executeInUnitContext() (*exec.ExecResponse, error) {
101 unitDir := filepath.Join(AgentDir, c.unit)
102 logger.Debugf("looking for unit dir %s", unitDir)
103 // make sure the unit exists
104 _, err := os.Stat(unitDir)
105 if os.IsNotExist(err) {
106- return fmt.Errorf("unit %q not found on this machine", c.unit)
107+ return nil, fmt.Errorf("unit %q not found on this machine", c.unit)
108 } else if err != nil {
109- return err
110+ return nil, err
111 }
112
113 socketPath := filepath.Join(unitDir, uniter.RunListenerFile)
114 // make sure the socket exists
115 client, err := rpc.Dial(uniter.RunListenerNetType, socketPath)
116 if err != nil {
117- return err
118+ return nil, err
119 }
120 defer client.Close()
121
122 var result exec.ExecResponse
123 err = client.Call(uniter.JujuRunEndpoint, c.commands, &result)
124+ return &result, err
125+}
126+
127+func getLock() (*fslock.Lock, error) {
128+ return fslock.NewLock(LockDir, "uniter-hook-execution")
129+}
130+
131+func (c *RunCommand) executeNoContext() (*exec.ExecResponse, error) {
132+ // Acquire the uniter hook execution lock to make sure we don't
133+ // stomp on each other.
134+ lock, err := getLock()
135 if err != nil {
136- return err
137+ return nil, err
138 }
139+ lock.Lock("juju-run")
140+ defer lock.Unlock()
141
142- ctx.Stdout.Write(result.Stdout)
143- ctx.Stderr.Write(result.Stderr)
144- return cmd.NewRcPassthroughError(result.Code)
145+ return exec.RunCommands(
146+ exec.RunParams{
147+ Commands: c.commands,
148+ })
149 }
150
151=== modified file 'cmd/jujud/run_test.go'
152--- cmd/jujud/run_test.go 2014-01-15 01:05:05 +0000
153+++ cmd/jujud/run_test.go 2014-01-15 01:05:06 +0000
154@@ -7,6 +7,7 @@
155 "fmt"
156 "os"
157 "path/filepath"
158+ "time"
159
160 gc "launchpad.net/gocheck"
161 "launchpad.net/loggo"
162@@ -16,6 +17,7 @@
163 jc "launchpad.net/juju-core/testing/checkers"
164 "launchpad.net/juju-core/testing/testbase"
165 "launchpad.net/juju-core/utils/exec"
166+ "launchpad.net/juju-core/utils/fslock"
167 "launchpad.net/juju-core/worker/uniter"
168 )
169
170@@ -27,11 +29,12 @@
171
172 func (*RunTestSuite) TestWrongArgs(c *gc.C) {
173 for i, test := range []struct {
174- title string
175- args []string
176- errMatch string
177- unit string
178- commands string
179+ title string
180+ args []string
181+ errMatch string
182+ unit string
183+ commands string
184+ avoidContext bool
185 }{{
186 title: "no args",
187 errMatch: "missing unit-name",
188@@ -53,15 +56,21 @@
189 args: []string{"foo/1", "command"},
190 unit: "unit-foo-1",
191 commands: "command",
192+ }, {
193+ title: "execute not in a context",
194+ args: []string{"--no-context", "command"},
195+ commands: "command",
196+ avoidContext: true,
197 },
198 } {
199- c.Log(fmt.Sprintf("\n%d: %s", i, test.title))
200+ c.Logf("\n%d: %s", i, test.title)
201 runCommand := &RunCommand{}
202- err := runCommand.Init(test.args)
203+ err := testing.InitCommand(runCommand, test.args)
204 if test.errMatch == "" {
205 c.Assert(err, gc.IsNil)
206 c.Assert(runCommand.unit, gc.Equals, test.unit)
207 c.Assert(runCommand.commands, gc.Equals, test.commands)
208+ c.Assert(runCommand.noContext, gc.Equals, test.avoidContext)
209 } else {
210 c.Assert(err, gc.ErrorMatches, test.errMatch)
211 }
212@@ -76,13 +85,73 @@
213 }
214
215 func (s *RunTestSuite) TestMissingAgent(c *gc.C) {
216- agentDir := c.MkDir()
217- s.PatchValue(&AgentDir, agentDir)
218+ s.PatchValue(&AgentDir, c.MkDir())
219
220 _, err := testing.RunCommand(c, &RunCommand{}, []string{"foo", "bar"})
221 c.Assert(err, gc.ErrorMatches, `unit "foo" not found on this machine`)
222 }
223
224+func waitForResult(running <-chan *cmd.Context) (*cmd.Context, error) {
225+ select {
226+ case result := <-running:
227+ return result, nil
228+ case <-time.After(testing.ShortWait):
229+ return nil, fmt.Errorf("timeout")
230+ }
231+}
232+
233+func startRunAsync(c *gc.C, params []string) <-chan *cmd.Context {
234+ resultChannel := make(chan *cmd.Context)
235+ go func() {
236+ ctx, err := testing.RunCommand(c, &RunCommand{}, params)
237+ c.Assert(err, jc.Satisfies, cmd.IsRcPassthroughError)
238+ c.Assert(err, gc.ErrorMatches, "rc: 0")
239+ resultChannel <- ctx
240+ close(resultChannel)
241+ }()
242+ return resultChannel
243+}
244+
245+func (s *RunTestSuite) TestNoContext(c *gc.C) {
246+ s.PatchValue(&LockDir, c.MkDir())
247+ s.PatchValue(&AgentDir, c.MkDir())
248+
249+ ctx, err := testing.RunCommand(c, &RunCommand{}, []string{"--no-context", "echo done"})
250+ c.Assert(err, jc.Satisfies, cmd.IsRcPassthroughError)
251+ c.Assert(err, gc.ErrorMatches, "rc: 0")
252+ c.Assert(testing.Stdout(ctx), gc.Equals, "done\n")
253+}
254+
255+func (s *RunTestSuite) TestNoContextAsync(c *gc.C) {
256+ s.PatchValue(&LockDir, c.MkDir())
257+ s.PatchValue(&AgentDir, c.MkDir())
258+
259+ channel := startRunAsync(c, []string{"--no-context", "echo done"})
260+ ctx, err := waitForResult(channel)
261+ c.Assert(err, gc.IsNil)
262+ c.Assert(testing.Stdout(ctx), gc.Equals, "done\n")
263+}
264+
265+func (s *RunTestSuite) TestNoContextWithLock(c *gc.C) {
266+ s.PatchValue(&LockDir, c.MkDir())
267+ s.PatchValue(&AgentDir, c.MkDir())
268+ s.PatchValue(&fslock.LockWaitDelay, 10*time.Millisecond)
269+
270+ lock, err := getLock()
271+ c.Assert(err, gc.IsNil)
272+ lock.Lock("juju-run test")
273+
274+ channel := startRunAsync(c, []string{"--no-context", "echo done"})
275+ ctx, err := waitForResult(channel)
276+ c.Assert(err, gc.ErrorMatches, "timeout")
277+
278+ lock.Unlock()
279+
280+ ctx, err = waitForResult(channel)
281+ c.Assert(err, gc.IsNil)
282+ c.Assert(testing.Stdout(ctx), gc.Equals, "done\n")
283+}
284+
285 func (s *RunTestSuite) TestMissingSocket(c *gc.C) {
286 s.PatchValue(&AgentDir, c.MkDir())
287 testAgentDir := filepath.Join(AgentDir, "foo")
288
289=== modified file 'state/apiserver/client/run.go'
290--- state/apiserver/client/run.go 2014-01-15 01:05:05 +0000
291+++ state/apiserver/client/run.go 2014-01-15 01:05:06 +0000
292@@ -106,7 +106,8 @@
293 if err != nil {
294 return results, err
295 }
296- execParam := remoteParamsForMachine(machine, run.Commands, run.Timeout)
297+ command := fmt.Sprintf("juju-run --no-context %s", quotedCommands)
298+ execParam := remoteParamsForMachine(machine, command, run.Timeout)
299 params = append(params, execParam)
300 }
301 return ParallelExecute(c.api.dataDir, params), nil
302@@ -119,8 +120,10 @@
303 return params.RunResults{}, err
304 }
305 var params []*RemoteExec
306+ quotedCommands := utils.ShQuote(run.Commands)
307+ command := fmt.Sprintf("juju-run --no-context %s", quotedCommands)
308 for _, machine := range machines {
309- params = append(params, remoteParamsForMachine(machine, run.Commands, run.Timeout))
310+ params = append(params, remoteParamsForMachine(machine, command, run.Timeout))
311 }
312 return ParallelExecute(c.api.dataDir, params), nil
313 }
314
315=== modified file 'state/apiserver/client/run_test.go'
316--- state/apiserver/client/run_test.go 2014-01-15 01:05:05 +0000
317+++ state/apiserver/client/run_test.go 2014-01-15 01:05:06 +0000
318@@ -126,7 +126,7 @@
319 units: []string{"magic/0"},
320 expected: []string{"magic/0", "magic/1"},
321 }} {
322- c.Log(fmt.Sprintf("%v: %s", i, test.message))
323+ c.Logf("%v: %s", i, test.message)
324 result, err := client.GetAllUnitNames(s.State, test.units, test.services)
325 if test.error == "" {
326 c.Check(err, gc.IsNil)
327@@ -230,7 +230,7 @@
328 for i := 0; i < 3; i++ {
329 expectedResults = append(expectedResults,
330 params.RunResult{
331- ExecResponse: exec.ExecResponse{Stdout: []byte("hostname\n")},
332+ ExecResponse: exec.ExecResponse{Stdout: []byte("juju-run --no-context 'hostname'\n")},
333 MachineId: fmt.Sprint(i),
334 })
335 }
336@@ -264,7 +264,7 @@
337 c.Assert(results, gc.HasLen, 3)
338 expectedResults := []params.RunResult{
339 params.RunResult{
340- ExecResponse: exec.ExecResponse{Stdout: []byte("hostname\n")},
341+ ExecResponse: exec.ExecResponse{Stdout: []byte("juju-run --no-context 'hostname'\n")},
342 MachineId: "0",
343 },
344 params.RunResult{
345
346=== removed file 'utils/fslock/export_test.go'
347--- utils/fslock/export_test.go 2013-05-02 15:55:42 +0000
348+++ utils/fslock/export_test.go 1970-01-01 00:00:00 +0000
349@@ -1,15 +0,0 @@
350-// Copyright 2013 Canonical Ltd.
351-// Licensed under the AGPLv3, see LICENCE file for details.
352-
353-package fslock
354-
355-import (
356- "time"
357-)
358-
359-// SetLockWaitDelay updates the package lockWaitDelay for testing purposes.
360-func SetLockWaitDelay(delay time.Duration) time.Duration {
361- oldValue := lockWaitDelay
362- lockWaitDelay = delay
363- return oldValue
364-}
365
366=== modified file 'utils/fslock/fslock.go'
367--- utils/fslock/fslock.go 2013-10-10 20:58:54 +0000
368+++ utils/fslock/fslock.go 2014-01-15 01:05:06 +0000
369@@ -20,24 +20,26 @@
370 "regexp"
371 "time"
372
373- "launchpad.net/juju-core/log"
374+ "launchpad.net/loggo"
375+
376 "launchpad.net/juju-core/utils"
377 )
378
379 const (
380- // NameRegexp specifies the regular expression used to itentify valid lock names.
381+ // NameRegexp specifies the regular expression used to identify valid lock names.
382 NameRegexp = "^[a-z]+[a-z0-9.-]*$"
383 heldFilename = "held"
384 messageFilename = "message"
385 )
386
387 var (
388+ logger = loggo.GetLogger("juju.utils.fslock")
389 ErrLockNotHeld = errors.New("lock not held")
390 ErrTimeout = errors.New("lock timeout exceeded")
391
392 validName = regexp.MustCompile(NameRegexp)
393
394- lockWaitDelay = 1 * time.Second
395+ LockWaitDelay = 1 * time.Second
396 )
397
398 type Lock struct {
399@@ -141,10 +143,10 @@
400 }
401 currMessage := lock.Message()
402 if currMessage != heldMessage {
403- log.Infof("attempted lock failed %q, %s, currently held: %s", lock.name, message, currMessage)
404+ logger.Infof("attempted lock failed %q, %s, currently held: %s", lock.name, message, currMessage)
405 heldMessage = currMessage
406 }
407- time.Sleep(lockWaitDelay)
408+ time.Sleep(LockWaitDelay)
409 }
410 }
411
412
413=== modified file 'utils/fslock/fslock_test.go'
414--- utils/fslock/fslock_test.go 2013-09-20 02:33:04 +0000
415+++ utils/fslock/fslock_test.go 2014-01-15 01:05:06 +0000
416@@ -10,7 +10,6 @@
417 "path"
418 "runtime"
419 "sync/atomic"
420- "testing"
421 "time"
422
423 gc "launchpad.net/gocheck"
424@@ -21,10 +20,6 @@
425 "launchpad.net/juju-core/utils/fslock"
426 )
427
428-func Test(t *testing.T) {
429- gc.TestingT(t)
430-}
431-
432 type fslockSuite struct {
433 testbase.LoggingSuite
434 lockDelay time.Duration
435@@ -34,12 +29,7 @@
436
437 func (s *fslockSuite) SetUpSuite(c *gc.C) {
438 s.LoggingSuite.SetUpSuite(c)
439- s.lockDelay = fslock.SetLockWaitDelay(1 * time.Millisecond)
440-}
441-
442-func (s *fslockSuite) TearDownSuite(c *gc.C) {
443- fslock.SetLockWaitDelay(s.lockDelay)
444- s.LoggingSuite.TearDownSuite(c)
445+ s.PatchValue(&fslock.LockWaitDelay, 1*time.Millisecond)
446 }
447
448 // This test also happens to test that locks can get created when the parent
449
450=== added file 'utils/fslock/package_test.go'
451--- utils/fslock/package_test.go 1970-01-01 00:00:00 +0000
452+++ utils/fslock/package_test.go 2014-01-15 01:05:06 +0000
453@@ -0,0 +1,24 @@
454+// Copyright 2014 Canonical Ltd.
455+// Licensed under the AGPLv3, see LICENCE file for details.
456+
457+package fslock_test
458+
459+import (
460+ "testing"
461+
462+ gc "launchpad.net/gocheck"
463+
464+ "launchpad.net/juju-core/testing/testbase"
465+)
466+
467+func Test(t *testing.T) { gc.TestingT(t) }
468+
469+type Dependencies struct{}
470+
471+var _ = gc.Suite(&Dependencies{})
472+
473+func (*Dependencies) TestPackageDependencies(c *gc.C) {
474+ // This test is to ensure we don't bring in dependencies without thinking.
475+ c.Assert(testbase.FindJujuCoreImports(c, "launchpad.net/juju-core/utils/fslock"),
476+ gc.DeepEquals, []string{"utils"})
477+}

Subscribers

People subscribed via source and target branches

to status/vote changes: