Merge lp:~johnweldon4/juju-core/action into lp:~go-bot/juju-core/trunk
- action
- Merge into trunk
Status: | Merged |
---|---|
Approved by: | William Reade |
Approved revision: | no longer in the source branch. |
Merged at revision: | 2787 |
Proposed branch: | lp:~johnweldon4/juju-core/action |
Merge into: | lp:~go-bot/juju-core/trunk |
Diff against target: |
278 lines (+206/-2) 5 files modified
state/action.go (+46/-0) state/action_test.go (+99/-0) state/open.go (+1/-0) state/state.go (+16/-2) state/unit.go (+44/-0) |
To merge this branch: | bzr merge lp:~johnweldon4/juju-core/action |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
William Reade | Pending | ||
Review via email: mp+219225@code.launchpad.net |
Commit message
Actions: Work in Progress
Adding action documents to the State
Description of the change
Actions: Work in Progress
Adding action documents to the State
William Reade (fwereade) wrote : | # |
John Weldon (johnweldon4) wrote : | # |
Reviewers: mp+219225_
Message:
Please take a look.
Description:
Actions: Work in Progress
Adding action documents to the State
https:/
(do not edit description out of merge proposal)
Please review this at https:/
Affected files (+173, -4 lines):
A [revision details]
A state/action.go
A state/action_
M state/open.go
M state/state.go
M state/state_test.go
M state/unit.go
William Reade (fwereade) wrote : | # |
Should just be another round, I think. Ping me when you're online and we
can chat.
https:/
File state/action.go (right):
https:/
state/action.go:23: Status ActionStatus
Hmm, wondering now about the consequences on watches if we have the
status direct in the action doc. Would it make sense to leave status out
for now? The unit's going to be watching this collection, and doesn't
need to respond to status changes; but the client will be watching the
*results* collection, and might well want to respond to changes there.
Sane?
https:/
state/action.go:34: action := &Action{
return &Action{
?
https:/
state/action.go:70: err := a.st.runTransac
we should handle txn.ErrAborted and give a real error back -- in this
case it indicates (I think) that the action has already completed.
Right?
https:/
File state/action_
https:/
state/action_
FWIW we usually leave these out, but I think the explicitness of this
approach may actually be better. Don't feel obliged to include them if
you don't think it's necessary.
https:/
state/action_
s.SettingsSuite
Hmm, I think we *should* make sure that the unit exists and isn't Dead.
(I've been thinking, and have tentatively concluded that actions on
dying units are fine, they just might not run in time; but actions on
dead units are definitely insane.)
https:/
File state/state.go (right):
https:/
state/state.go:320: for k := range oldConfig.
thanks for this cleanup btw
https:/
state/state.
string, payload interface{}) (*Action, error) {
Thinking about this: I think it should be (u *Unit) AddAction(name
string, payload interface{}) (...).
Should be a simple move (and also make it easier to assert that the unit
isn't dead).
https:/
state/state.
I don't think errDead is a sensible error message for "doc already
exists".
https:/
state/state.
Given that we'll want two asserts here, we'll have two possible failure
modes (even if the one you're currently handling should be a cant-happen
one). Let's talk today about how to write the code in (what I think is)
the cleanest way, because it's not actually very obvious, I'm afraid.
Yeah, this is another thing I should be documenting.
John Weldon (johnweldon4) wrote : | # |
A few comments and responses. We can chat on irc or hangouts whenever.
https:/
File state/action.go (right):
https:/
state/action.go:34: action := &Action{
On 2014/05/15 12:47:26, fwereade wrote:
> return &Action{
> ?
Done.
https:/
File state/action_
https:/
state/action_
On 2014/05/15 12:47:26, fwereade wrote:
> FWIW we usually leave these out, but I think the explicitness of this
approach
> may actually be better. Don't feel obliged to include them if you
don't think
> it's necessary.
Do you mean the (SetUp|
Because they're just calling the inner SettingsSuite funcs does that
mean the test harness will do it automatically?
https:/
state/action_
s.SettingsSuite
On 2014/05/15 12:47:27, fwereade wrote:
> Hmm, I think we *should* make sure that the unit exists and isn't
Dead.
> (I've been thinking, and have tentatively concluded that actions on
dying units
> are fine, they just might not run in time; but actions on dead units
are
> definitely insane.)
In this case I'm actually not even testing any "real" unit. Of course
when we do the refactor to make func (u* Unit) AddAction(...) then we'll
need a real Unit for this test.
But this test is kind of superfluous any way. state_test has a very
similar test already.
https:/
File state/state.go (right):
https:/
state/state.go:320: for k := range oldConfig.
On 2014/05/15 12:47:27, fwereade wrote:
> thanks for this cleanup btw
I love cleanup :)
https:/
state/state.
On 2014/05/15 12:47:27, fwereade wrote:
> I don't think errDead is a sensible error message for "doc already
exists".
Er. Yes. Copy / Paste dumbness
https:/
File state/state_test.go (right):
https:/
state/state_
s.State.
On 2014/05/15 12:47:27, fwereade wrote:
> It might be nice to have some sort of check on the form of the Id.
Makes sense... I'll do something like "u#" == answerAction.
maybe min length check and substring match on "#" in id[2:]. If you have
any suggestions let me know.
William Reade (fwereade) wrote : | # |
https:/
File state/action_
https:/
state/action_
Ooh, didn't spot that: this will include all the Test* methods from
SettingsSuite as well, and we don't want that. I'd recommend ConnSuite
(the one right here in state_test, not the one in... er juju/testing?).
https:/
state/action_
On 2014/05/15 16:23:28, johnweldon4 wrote:
> On 2014/05/15 12:47:26, fwereade wrote:
> > FWIW we usually leave these out, but I think the explicitness of
this approach
> > may actually be better. Don't feel obliged to include them if you
don't think
> > it's necessary.
> Do you mean the (SetUp|
> Because they're just calling the inner SettingsSuite funcs does that
mean the
> test harness will do it automatically?
Yeah, if you embed a type you get all its methods (so long as there's no
ambiguity, which there isn't in this case).
https:/
state/action_
s.SettingsSuite
On 2014/05/15 16:23:28, johnweldon4 wrote:
> On 2014/05/15 12:47:27, fwereade wrote:
> > Hmm, I think we *should* make sure that the unit exists and isn't
Dead.
> >
> > (I've been thinking, and have tentatively concluded that actions on
dying
> units
> > are fine, they just might not run in time; but actions on dead units
are
> > definitely insane.)
> In this case I'm actually not even testing any "real" unit. Of course
when we
> do the refactor to make func (u* Unit) AddAction(...) then we'll need
a real
> Unit for this test.
> But this test is kind of superfluous any way. state_test has a very
similar
> test already.
I think this suite's existence is good, it's nice to cluster the tests
for related functionality, and there's an awful lot on the direct State
tests already.
John Weldon (johnweldon4) wrote : | # |
https:/
File state/state.go (right):
https:/
state/state.
string, payload interface{}) (*Action, error) {
On 2014/05/15 12:47:27, fwereade wrote:
> Thinking about this: I think it should be (u *Unit) AddAction(name
string,
> payload interface{}) (...).
> Should be a simple move (and also make it easier to assert that the
unit isn't
> dead).
I'm assuming we put the Action doc in the same actions collection on
state? We're just doing the add on the Unit?
John Weldon (johnweldon4) wrote : | # |
Updated code to address items in this review.
Still need to do:
1) Transaction loop
2) Cleanup (maybe wait til fwereade refactor makes it to juju-core?)
https:/
File state/action.go (right):
https:/
state/action.go:23: Status ActionStatus
On 2014/05/15 12:47:26, fwereade wrote:
> Hmm, wondering now about the consequences on watches if we have the
status
> direct in the action doc. Would it make sense to leave status out for
now? The
> unit's going to be watching this collection, and doesn't need to
respond to
> status changes; but the client will be watching the *results*
collection, and
> might well want to respond to changes there. Sane?
Done.
https:/
state/action.go:70: err := a.st.runTransac
On 2014/05/15 12:47:26, fwereade wrote:
> we should handle txn.ErrAborted and give a real error back -- in this
case it
> indicates (I think) that the action has already completed. Right?
Done.
https:/
File state/action_
https:/
state/action_
On 2014/05/15 18:02:34, fwereade wrote:
> Ooh, didn't spot that: this will include all the Test* methods from
> SettingsSuite as well, and we don't want that. I'd recommend ConnSuite
(the one
> right here in state_test, not the one in... er juju/testing?).
Done.
https:/
state/action_
On 2014/05/15 18:02:34, fwereade wrote:
> On 2014/05/15 16:23:28, johnweldon4 wrote:
> >
> > On 2014/05/15 12:47:26, fwereade wrote:
> > > FWIW we usually leave these out, but I think the explicitness of
this
> approach
> > > may actually be better. Don't feel obliged to include them if you
don't
> think
> > > it's necessary.
> >
> >
> > Do you mean the (SetUp|
> > Because they're just calling the inner SettingsSuite funcs does that
mean the
> > test harness will do it automatically?
> Yeah, if you embed a type you get all its methods (so long as there's
no
> ambiguity, which there isn't in this case).
Done.
https:/
state/action_
s.SettingsSuite
On 2014/05/15 18:02:34, fwereade wrote:
> On 2014/05/15 16:23:28, johnweldon4 wrote:
> > On 2014/05/15 12:47:27, fwereade wrote:
> > > Hmm, I think we *should* make sure that the unit exists and isn't
Dead.
> > >
> > > (I've been thinking, and have tentatively concluded that actions
on dying
> > units
> > > are fine, they just might not run in time; but actions on dead
units are
> > > definitely insane.)
> >
> >
> > In this case I'm actually not even testing any "real" unit. Of
course when we
> > do the refactor to make func (u* Unit) AddAction(...) then we'll
need a real
> > Unit for this test.
> > But this test is kind of superfluous any way. state_test has a very
s...
John Weldon (johnweldon4) wrote : | # |
Please take a look.
William Reade (fwereade) wrote : | # |
Looking good. Ping me when you're on, and we can go through the
transaction loop stuff.
https:/
File state/action.go (right):
https:/
state/action.go:8: Unit string
Drop the Unit field.
https:/
state/action.go:31: // Id returns the mongo Id of the Action
even if it *is* the mongo id, we shouldn't *tell* people it is (if you
see what I mean).
I think that what we end up exposing to the user will want to be some
long hex string (identifiable by first few characters -- think git
revisions)... but let's not quibble about that now. This is the PK we
want, and until we expose the functionality we're free to fix it later.
https:/
File state/unit.go (right):
https:/
state/unit.go:1314: func (u *Unit) AddAction(name string, payload
map[string]
is it an interface{}, or a map[string]
latter, but then the doc needs to match as well.
https:/
state/unit.go:1332: // TODO(jcw4) transaction loop
also needs assert on unit life in the loop.
https:/
File state/unit_test.go (right):
https:/
state/unit_
"^u#"+s.
I'd drop the empty TokenTest in the other file and just add a couple of
bits here: check that you can get the action by Id, and that the objects
match.
William Reade (fwereade) wrote : | # |
https:/
File state/unit.go (right):
https:/
state/unit.go:1332: // TODO(jcw4) transaction loop
See
https:/
for comments on loops -- ping me when you've read it.
John Weldon (johnweldon4) wrote : | # |
Addressed all of these issues I believe.
https:/
File state/action.go (right):
https:/
state/action.go:8: Unit string
On 2014/05/20 07:12:17, fwereade wrote:
> Drop the Unit field.
Done.
https:/
state/action.go:31: // Id returns the mongo Id of the Action
On 2014/05/20 07:12:17, fwereade wrote:
> even if it *is* the mongo id, we shouldn't *tell* people it is (if you
see what
> I mean).
> I think that what we end up exposing to the user will want to be some
long hex
> string (identifiable by first few characters -- think git
revisions)... but
> let's not quibble about that now. This is the PK we want, and until we
expose
> the functionality we're free to fix it later.
Done.
https:/
File state/unit.go (right):
https:/
state/unit.go:1314: func (u *Unit) AddAction(name string, payload
map[string]
On 2014/05/20 07:12:17, fwereade wrote:
> is it an interface{}, or a map[string]
latter, but
> then the doc needs to match as well.
Done.
https:/
state/unit.go:1332: // TODO(jcw4) transaction loop
On 2014/05/20 14:32:08, fwereade wrote:
> See
https:/
> for comments on loops -- ping me when you've read it.
Done.
https:/
File state/unit_test.go (right):
https:/
state/unit_
"^u#"+s.
On 2014/05/20 07:12:17, fwereade wrote:
> I'd drop the empty TokenTest in the other file and just add a couple
of bits
> here: check that you can get the action by Id, and that the objects
match.
Done.
John Weldon (johnweldon4) wrote : | # |
Please take a look.
William Reade (fwereade) wrote : | # |
Many comments, because I like the sound of my own typing, but very
little actionable. I'd appreciate your thoughts but this basically LGTM.
Fix any trivials and let me know when it's ready to merge, and I'll
approve it.
https:/
File state/unit.go (right):
https:/
state/unit.go:1325: if notDead, err := isNotDead(
u.doc.Name); err != nil {
This one is interesting -- it's a not-strictly-
running the txn, and I'd ordinarily shy away from that in favour of
checking u.doc.Life -- but let's see how it goes. I suspect we'll need
to go with the other approach in the end, as we start to care about
things like charm versions, but we can wait and see if I'm right about
that.
https:/
state/unit.go:1331: sel := bson.D{{"_id", newActionID}}
This block is almost redundant, because we should be able to depend on
the uniqueness of sequence IDs (even in the case of a new unit being
created with the same name as an old one (which is itself a bug
(although it only happens when a service is destroyed and recreated))).
If it didn't involve an extra DB hit I'd have no problem with it; but as
it is I'm gently leaning towards dropping it. If there's a problem here
we'll fall out to ErrExcessiveCon
be pretty easy to diagnose from there.
https:/
state/unit.go:1340: ops := []txn.Op{{
As discussed yesterday, in this particular case the ops don't have to be
rebuilt each time, but it's probably good to keep the familiar
structure.
https:/
state/unit.go:1351: return newActionID, err
returning the id is not "normal" for this package, but IMO it's a good
idea. thanks.
https:/
File state/unit_test.go (right):
https:/
state/unit_
id prefix, + sequence)
Add a note that this is temporary, and we shouldn't really leak the
actual _id.
https:/
state/unit_
s/unitDead/
https:/
state/unit_
'contention' into the transaction
Haha. I've thought of a way, and I kinda endorse you doing it, so long
as you clearly comment that it'll stop working when lp:1174610 is fixed.
In the Before hook, you get st.Unit(name) and make it Dead; in the After
hook, you remove the unit and the service, and create a new service with
the same name. From the POV of that method the unit will appear to be
resurrected.
But that's also dirty as sin, and will break when we fix other stuff, so
I'm reasonably comfortable skipping it. (The other poss...
John Weldon (johnweldon4) wrote : | # |
Addressed the few changes needed. Couple questions I'll ask about in
#juju-dev
https:/
File state/unit.go (right):
https:/
state/unit.go:1325: if notDead, err := isNotDead(
u.doc.Name); err != nil {
On 2014/05/21 07:39:48, fwereade wrote:
> This one is interesting -- it's a not-strictly-
running
> the txn, and I'd ordinarily shy away from that in favour of checking
u.doc.Life
> -- but let's see how it goes. I suspect we'll need to go with the
other approach
> in the end, as we start to care about things like charm versions, but
we can
> wait and see if I'm right about that.
How is this different than doing a clone of the unit as you initially
suggested. I went this route because it seemed more direct and I
thought the db hit would be comparable, if not slightly lighter than a
full clone?
https:/
state/unit.go:1331: sel := bson.D{{"_id", newActionID}}
On 2014/05/21 07:39:48, fwereade wrote:
> This block is almost redundant, because we should be able to depend on
the
> uniqueness of sequence IDs (even in the case of a new unit being
created with
> the same name as an old one (which is itself a bug (although it only
happens
> when a service is destroyed and recreated))).
> If it didn't involve an extra DB hit I'd have no problem with it; but
as it is
> I'm gently leaning towards dropping it. If there's a problem here
we'll fall out
> to ErrExcessiveCon
easy to
> diagnose from there.
Done.
https:/
state/unit.go:1340: ops := []txn.Op{{
On 2014/05/21 07:39:48, fwereade wrote:
> As discussed yesterday, in this particular case the ops don't have to
be rebuilt
> each time, but it's probably good to keep the familiar structure.
Done.
https:/
state/unit.go:1351: return newActionID, err
On 2014/05/21 07:39:48, fwereade wrote:
> returning the id is not "normal" for this package, but IMO it's a good
idea.
> thanks.
Done.
https:/
File state/unit_test.go (right):
https:/
state/unit_
id prefix, + sequence)
On 2014/05/21 07:39:48, fwereade wrote:
> Add a note that this is temporary, and we shouldn't really leak the
actual _id.
Done.
https:/
state/unit_
On 2014/05/21 07:39:48, fwereade wrote:
> s/unitDead/
Done.
https:/
state/unit_
'contention' into the transaction
On 2014/05/21 07:39:48, fwereade wrote:
> Haha. I've thought of a way, and I kinda endorse you doing it, so l...
John Weldon (johnweldon4) wrote : | # |
Please take a look.
Martin Packman (gz) wrote : | # |
LGTM.
https:/
File state/action.go (right):
https:/
state/action.go:8: Payload map[string]
I think I'd comment these members. Clarifies thinks like what defines
the action name, and what determines the payload type.
https:/
File state/unit_test.go (right):
https:/
state/unit_
Probably want to ErrorMatches this even though it's from below the
AddAction function.
https:/
state/unit_
'contention' into the transaction
This is where we really want a generic contention helper so each add
doesn't have to futz around with testing its loop. I think William has
hopes for that at some point later.
John Weldon (johnweldon4) wrote : | # |
Okay proposing again.
https:/
File state/action.go (right):
https:/
state/action.go:8: Payload map[string]
On 2014/05/21 20:35:25, gz wrote:
> I think I'd comment these members. Clarifies thinks like what defines
the action
> name, and what determines the payload type.
Done.
https:/
File state/unit_test.go (right):
https:/
state/unit_
On 2014/05/21 20:35:25, gz wrote:
> Probably want to ErrorMatches this even though it's from below the
AddAction
> function.
Done.
https:/
state/unit_
'contention' into the transaction
On 2014/05/21 20:35:25, gz wrote:
> This is where we really want a generic contention helper so each add
doesn't
> have to futz around with testing its loop. I think William has hopes
for that at
> some point later.
Yep
John Weldon (johnweldon4) wrote : | # |
Please take a look.
John Weldon (johnweldon4) wrote : | # |
Please take a look.
William Reade (fwereade) wrote : | # |
LGTM with final trivials. Sorry :(.
https:/
File state/action.go (right):
https:/
state/action.go:6: Id string `bson:"_id"`
blank lines before comments please
https:/
state/action.go:9: // action to actually perform
Name identifies the action; it should match an action defined by the
unit's charm.
?
https:/
state/action.go:12: // when this action is being performed.
Payload holds the action's parameters, if any; it should validate
against the schema defined by the named action in the unit's charm.
?
https:/
File state/unit.go (right):
https:/
state/unit.go:1344: return newActionID, err
ah, dammit. We shouldn't really return newActionID if err != nil.
`switch err := ...; err {`?
https:/
File state/unit_test.go (right):
https:/
state/unit_
hey, can we move these to a new suite in action_test.go? unit_test is
already too damn big IMO.
https:/
state/unit_
"^u#"+s.
how about an assertSaneActionId? You can use it below where you're
currently discarding the new ids.
https:/
state/unit_
TestAddActionFa
TestAddActionLi
https:/
state/unit_
TestAddActionFa
I'd just drop this.
John Weldon (johnweldon4) wrote : | # |
Okay... addressed all
Running the tests and then I'll lbox propose
https:/
File state/action.go (right):
https:/
state/action.go:6: Id string `bson:"_id"`
On 2014/05/22 08:02:13, fwereade wrote:
> blank lines before comments please
Done.
https:/
state/action.go:9: // action to actually perform
On 2014/05/22 08:02:13, fwereade wrote:
> Name identifies the action; it should match an action defined by the
unit's
> charm.
> ?
Done.
https:/
state/action.go:12: // when this action is being performed.
On 2014/05/22 08:02:13, fwereade wrote:
> Payload holds the action's parameters, if any; it should validate
against the
> schema defined by the named action in the unit's charm.
> ?
Done.
https:/
File state/unit.go (right):
https:/
state/unit.go:1344: return newActionID, err
On 2014/05/22 08:02:13, fwereade wrote:
> ah, dammit. We shouldn't really return newActionID if err != nil.
`switch err :=
> ...; err {`?
Done.
https:/
File state/unit_test.go (right):
https:/
state/unit_
On 2014/05/22 08:02:14, fwereade wrote:
> hey, can we move these to a new suite in action_test.go? unit_test is
already
> too damn big IMO.
Done.
https:/
state/unit_
"^u#"+s.
On 2014/05/22 08:02:14, fwereade wrote:
> how about an assertSaneActionId? You can use it below where you're
currently
> discarding the new ids.
Done. but only applies to the places where we don't expect an error...
https:/
state/unit_
TestAddActionFa
On 2014/05/22 08:02:13, fwereade wrote:
> TestAddActionLi
Done.
https:/
state/unit_
TestAddActionFa
On 2014/05/22 08:02:14, fwereade wrote:
> I'd just drop this.
Done.
John Weldon (johnweldon4) wrote : | # |
Please take a look.
William Reade (fwereade) wrote : | # |
LGTM, approving
Go Bot (go-bot) wrote : | # |
There are additional revisions which have not been approved in review. Please seek review and approval of these new revisions.
Preview Diff
1 | === added file 'state/action.go' |
2 | --- state/action.go 1970-01-01 00:00:00 +0000 |
3 | +++ state/action.go 2014-05-23 14:50:36 +0000 |
4 | @@ -0,0 +1,46 @@ |
5 | +package state |
6 | + |
7 | +import () |
8 | + |
9 | +type actionDoc struct { |
10 | + Id string `bson:"_id"` |
11 | + |
12 | + // Name identifies the action; it should match an action defined by |
13 | + // the unit's charm. |
14 | + Name string |
15 | + |
16 | + // Payload holds the action's parameters, if any; it should validate |
17 | + // against the schema defined by the named action in the unit's charm |
18 | + Payload map[string]interface{} |
19 | +} |
20 | + |
21 | +// Action represents an instruction to do some "action" and is expected |
22 | +// to match an action definition in a charm. |
23 | +type Action struct { |
24 | + st *State |
25 | + doc actionDoc |
26 | +} |
27 | + |
28 | +func newAction(st *State, adoc actionDoc) *Action { |
29 | + return &Action{ |
30 | + st: st, |
31 | + doc: adoc, |
32 | + } |
33 | +} |
34 | + |
35 | +// Name returns the name of the Action |
36 | +func (a *Action) Name() string { |
37 | + return a.doc.Name |
38 | +} |
39 | + |
40 | +// Id returns the id of the Action |
41 | +func (a *Action) Id() string { |
42 | + return a.doc.Id |
43 | +} |
44 | + |
45 | +// Payload will contain a structure representing arguments or parameters to |
46 | +// an action, and is expected to be validated by the Unit using the Charm |
47 | +// definition of the Action |
48 | +func (a *Action) Payload() map[string]interface{} { |
49 | + return a.doc.Payload |
50 | +} |
51 | |
52 | === added file 'state/action_test.go' |
53 | --- state/action_test.go 1970-01-01 00:00:00 +0000 |
54 | +++ state/action_test.go 2014-05-23 14:50:36 +0000 |
55 | @@ -0,0 +1,99 @@ |
56 | +// Copyright 2014 Canonical Ltd. |
57 | +// Licensed under the AGPLv3, see LICENCE file for details. |
58 | + |
59 | +package state_test |
60 | + |
61 | +import ( |
62 | + jc "github.com/juju/testing/checkers" |
63 | + gc "launchpad.net/gocheck" |
64 | + |
65 | + "launchpad.net/juju-core/state" |
66 | +) |
67 | + |
68 | +type ActionSuite struct { |
69 | + ConnSuite |
70 | + charm *state.Charm |
71 | + service *state.Service |
72 | + unit *state.Unit |
73 | +} |
74 | + |
75 | +var _ = gc.Suite(&ActionSuite{}) |
76 | + |
77 | +func (s *ActionSuite) SetUpTest(c *gc.C) { |
78 | + s.ConnSuite.SetUpTest(c) |
79 | + s.charm = s.AddTestingCharm(c, "wordpress") |
80 | + var err error |
81 | + s.service = s.AddTestingService(c, "wordpress", s.charm) |
82 | + c.Assert(err, gc.IsNil) |
83 | + s.unit, err = s.service.AddUnit() |
84 | + c.Assert(err, gc.IsNil) |
85 | + c.Assert(s.unit.Series(), gc.Equals, "quantal") |
86 | +} |
87 | + |
88 | +func (s *ActionSuite) TestAddAction(c *gc.C) { |
89 | + actionName := "fakeaction" |
90 | + actionParams := map[string]interface{}{"outfile": "outfile.tar.bz2"} |
91 | + |
92 | + // verify can add an Action |
93 | + actionId, err := s.unit.AddAction(actionName, actionParams) |
94 | + c.Assert(err, gc.IsNil) |
95 | + assertSaneActionId(c, actionId, s.unit.Name()) |
96 | + |
97 | + // verify we can get it back out by Id |
98 | + action, err := s.State.Action(actionId) |
99 | + c.Assert(err, gc.IsNil) |
100 | + c.Assert(action, gc.NotNil) |
101 | + c.Assert(action.Id(), gc.Equals, actionId) |
102 | + |
103 | + // verify we get out what we put in |
104 | + c.Assert(action.Name(), gc.Equals, actionName) |
105 | + c.Assert(action.Payload(), jc.DeepEquals, actionParams) |
106 | +} |
107 | + |
108 | +func (s *ActionSuite) TestAddActionLifecycle(c *gc.C) { |
109 | + unit, err := s.State.Unit(s.unit.Name()) |
110 | + c.Assert(err, gc.IsNil) |
111 | + preventUnitDestroyRemove(c, unit) |
112 | + |
113 | + // make unit state Dying |
114 | + err = unit.Destroy() |
115 | + c.Assert(err, gc.IsNil) |
116 | + |
117 | + // can add action to a dying unit |
118 | + actionId, err := unit.AddAction("fakeaction1", map[string]interface{}{}) |
119 | + c.Assert(err, gc.IsNil) |
120 | + assertSaneActionId(c, actionId, s.unit.Name()) |
121 | + |
122 | + // make sure unit is dead |
123 | + err = unit.EnsureDead() |
124 | + c.Assert(err, gc.IsNil) |
125 | + |
126 | + // cannot add action to a dead unit |
127 | + _, err = unit.AddAction("fakeaction2", map[string]interface{}{}) |
128 | + c.Assert(err, gc.ErrorMatches, "unit .* is dead") |
129 | +} |
130 | + |
131 | +func (s *ActionSuite) TestAddActionFailsOnDeadUnitInTransaction(c *gc.C) { |
132 | + unit, err := s.State.Unit(s.unit.Name()) |
133 | + c.Assert(err, gc.IsNil) |
134 | + preventUnitDestroyRemove(c, unit) |
135 | + |
136 | + killUnit := state.TransactionHook{ |
137 | + Before: func() { |
138 | + c.Assert(unit.Destroy(), gc.IsNil) |
139 | + c.Assert(unit.EnsureDead(), gc.IsNil) |
140 | + }, |
141 | + } |
142 | + defer state.SetTransactionHooks(c, s.State, killUnit).Check() |
143 | + |
144 | + _, err = unit.AddAction("fakeaction", map[string]interface{}{}) |
145 | + c.Assert(err, gc.ErrorMatches, "unit .* is dead") |
146 | +} |
147 | + |
148 | +// assertSaneActionId verifies that the actionId is of the expected |
149 | +// form (unit id prefix + sequence) |
150 | +// This is a temporary assertion, we shouldn't be leaking the actual |
151 | +// mongo _id |
152 | +func assertSaneActionId(c *gc.C, actionId, unitName string) { |
153 | + c.Assert(actionId, gc.Matches, "^u#"+unitName+"#\\d+") |
154 | +} |
155 | |
156 | === modified file 'state/open.go' |
157 | --- state/open.go 2014-05-13 04:30:48 +0000 |
158 | +++ state/open.go 2014-05-23 14:50:36 +0000 |
159 | @@ -292,6 +292,7 @@ |
160 | settingsrefs: db.C("settingsrefs"), |
161 | constraints: db.C("constraints"), |
162 | units: db.C("units"), |
163 | + actions: db.C("actions"), |
164 | users: db.C("users"), |
165 | presence: pdb.C("presence"), |
166 | cleanups: db.C("cleanups"), |
167 | |
168 | === modified file 'state/state.go' |
169 | --- state/state.go 2014-05-20 22:58:59 +0000 |
170 | +++ state/state.go 2014-05-23 14:50:36 +0000 |
171 | @@ -1,7 +1,7 @@ |
172 | // Copyright 2012, 2013 Canonical Ltd. |
173 | // Licensed under the AGPLv3, see LICENCE file for details. |
174 | |
175 | -// The state package enables reading, observing, and changing |
176 | +// Package state enables reading, observing, and changing |
177 | // the state stored in MongoDB of a whole environment |
178 | // managed by juju. |
179 | package state |
180 | @@ -64,6 +64,7 @@ |
181 | settingsrefs *mgo.Collection |
182 | constraints *mgo.Collection |
183 | units *mgo.Collection |
184 | + actions *mgo.Collection |
185 | users *mgo.Collection |
186 | presence *mgo.Collection |
187 | cleanups *mgo.Collection |
188 | @@ -316,7 +317,7 @@ |
189 | } |
190 | |
191 | validAttrs := validCfg.AllAttrs() |
192 | - for k, _ := range oldConfig.AllAttrs() { |
193 | + for k := range oldConfig.AllAttrs() { |
194 | if _, ok := validAttrs[k]; !ok { |
195 | settings.Delete(k) |
196 | } |
197 | @@ -1394,6 +1395,19 @@ |
198 | return rdc[i].Id < rdc[j].Id |
199 | } |
200 | |
201 | +// Action returns an Action by Id. |
202 | +func (st *State) Action(id string) (*Action, error) { |
203 | + doc := actionDoc{} |
204 | + err := st.actions.FindId(id).One(&doc) |
205 | + if err == mgo.ErrNotFound { |
206 | + return nil, errors.NotFoundf("action %q", id) |
207 | + } |
208 | + if err != nil { |
209 | + return nil, fmt.Errorf("cannot get action %q: %v", id, err) |
210 | + } |
211 | + return newAction(st, doc), nil |
212 | +} |
213 | + |
214 | // Unit returns a unit by name. |
215 | func (st *State) Unit(name string) (*Unit, error) { |
216 | if !names.IsUnit(name) { |
217 | |
218 | === modified file 'state/unit.go' |
219 | --- state/unit.go 2014-05-20 17:57:10 +0000 |
220 | +++ state/unit.go 2014-05-23 14:50:36 +0000 |
221 | @@ -803,6 +803,7 @@ |
222 | return fmt.Sprintf("unit %q is not assigned to a machine", e.Unit) |
223 | } |
224 | |
225 | +// IsNotAssigned verifies that err is an instance of NotAssignedError |
226 | func IsNotAssigned(err error) bool { |
227 | _, ok := err.(*NotAssignedError) |
228 | return ok |
229 | @@ -1327,6 +1328,49 @@ |
230 | return nil |
231 | } |
232 | |
233 | +// AddAction adds a new Action of type name and using arguments payload to |
234 | +// this Unit, and returns its ID |
235 | +func (u *Unit) AddAction(name string, payload map[string]interface{}) (string, error) { |
236 | + |
237 | + prefix := fmt.Sprintf("u#%s#", u.doc.Name) |
238 | + suffix, err := u.st.sequence(prefix) |
239 | + if err != nil { |
240 | + return "", fmt.Errorf("cannot assign new sequence for prefix '%s'. %s", prefix, err) |
241 | + } |
242 | + |
243 | + newActionID := fmt.Sprintf("%s%d", prefix, suffix) |
244 | + |
245 | + doc := actionDoc{Id: newActionID, Name: name, Payload: payload} |
246 | + ops := []txn.Op{{ |
247 | + C: u.st.units.Name, |
248 | + Id: u.doc.Name, |
249 | + Assert: notDeadDoc, |
250 | + }, { |
251 | + C: u.st.actions.Name, |
252 | + Id: doc.Id, |
253 | + Assert: txn.DocMissing, |
254 | + Insert: doc, |
255 | + }} |
256 | + |
257 | + for i := 0; i < 3; i++ { |
258 | + if notDead, err := isNotDead(u.st.units, u.doc.Name); err != nil { |
259 | + return "", err |
260 | + } else if !notDead { |
261 | + return "", fmt.Errorf("unit %q is dead", u) |
262 | + } |
263 | + |
264 | + switch err := u.st.runTransaction(ops); err { |
265 | + case txn.ErrAborted: |
266 | + continue |
267 | + case nil: |
268 | + return newActionID, nil |
269 | + default: |
270 | + return "", err |
271 | + } |
272 | + } |
273 | + return "", ErrExcessiveContention |
274 | +} |
275 | + |
276 | // Resolve marks the unit as having had any previous state transition |
277 | // problems resolved, and informs the unit that it may attempt to |
278 | // reestablish normal workflow. The retryHooks parameter informs |
This is a good start, but I have a few issues. Setting WIP; ping me when you're online, I'd be happy to chat about all this.
FWIW, I'd really appreciate an `lbox propose` so I could do this line-by-line :).
23 + Payload string
I'm a bit surprised payload is a string -- I was expecting an interface{}, that would have been checked against (or rather would at least be cleanly check*able* against) the action schema on creation.
35 + doc: *adoc,
I think that if you just passed an actionDoc by value you'd get the same copying behaviour, with less punctuation. Have I missed something?
56 +func (a *Action) setRunning() error {
71 +func (a *Action) setPending() error {
These don't seem to be used (tested)? And I don't see that setPending will ever be called, will it?
169 +// AddAction takes a unit Id, and an action name, and a payload containing
170 +// json-schema options for the action and inserts it into the actions
171 +// collection
Shouldn't the payload be "structured data conforming to the json-schema defined in the charm"? ...and at this point questions of compatibility creep in. There's no way to deal with them right now, because we haven't dealt with the charm changes, so that's not mandatory here; but we'll need to get the schemas into the DB sooner rather than later, I think.
173 + newActionID := fmt.Sprintf( "%s_%s" , unit, string( bson.NewObjectI d()))
Given that we'll have actions targeted to things other than units in the long term, we shoudl probably go with a globalKey-style id here: something like "u#<unit- id>#<action- id>". And I'm not wholly convinced by the embedded ObjectId -- those things do have useful properties but we'll be hard-pressed to extract them usefully.
If you get the "u#foo/3#" prefix, you can use st.sequence(prefix) to get a unique int to tack on the end; this will give us ordering for free (if not *alphabetical* ordering... although you could go with leading zeros for that). We might also want a timestamp or two (queued-at, started-at) in the doc somewhere, but that's probably not necessary in this CL.
174 + doc := actionDoc{ newActionID, name, unit, payload, ActionPending}
Please use field names in constructors this size (or any size above... 1, really :)).
222 +// Destroy , when called
revert please
### missing
We should make sure that when units are destroyed, their pending actions are destroyed (and, I guess, marked as failed, but ofc you can't do that yet). I'm pretty sure there's already a cleanup action that gets triggered on unit removal, you can extend that one.