Merge lp:~axwalk/juju-core/state-environment-life into lp:~go-bot/juju-core/trunk

Proposed by Andrew Wilkins
Status: Merged
Approved by: Andrew Wilkins
Approved revision: no longer in the source branch.
Merged at revision: 2131
Proposed branch: lp:~axwalk/juju-core/state-environment-life
Merge into: lp:~go-bot/juju-core/trunk
Diff against target: 604 lines (+298/-39)
11 files modified
names/environ.go (+9/-7)
state/addmachine.go (+14/-0)
state/cleanup.go (+22/-0)
state/cleanup_test.go (+42/-0)
state/environ.go (+69/-13)
state/environ_test.go (+1/-3)
state/initialize_test.go (+9/-4)
state/interface.go (+1/-0)
state/state.go (+34/-6)
state/state_test.go (+92/-6)
state/watcher.go (+5/-0)
To merge this branch: bzr merge lp:~axwalk/juju-core/state-environment-life
Reviewer Review Type Date Requested Status
Juju Engineering Pending
Review via email: mp+195710@code.launchpad.net

Commit message

state: add Life to state.Environment

Environment has been given new methods
to mark an environment as dying, and
to remove the environment from state.
An environment must now exist in state
and be Alive for services or machines
to be added to state.

A secondary change here is to use the
environment's UUID in the tag, rather
than the environment's name, which is
non-unique. We continue to accept the
name as input, until we know all users
are passing in UUIDs.

https://codereview.appspot.com/28880043/

Description of the change

state: add Life to state.Environment

Environment has been given new methods
to mark an environment as dying, and
to remove the environment from state.
An environment must now exist in state
and be Alive for services or machines
to be added to state.

A secondary change here is to use the
environment's UUID in the tag, rather
than the environment's name, which is
non-unique. We continue to accept the
name as input, until we know all users
are passing in UUIDs.

https://codereview.appspot.com/28880043/

To post a comment you must log in.
Revision history for this message
Andrew Wilkins (axwalk) wrote :

Reviewers: mp+195710_code.launchpad.net,

Message:
Please take a look.

Description:
state: add Life to state.Environment

Environment has been given new methods
to mark an environment as dying, and
to remove the environment from state.
An environment must now exist in state
and be Alive for services or machines
to be added to state.

A secondary change here is to use the
environment's UUID in the tag, rather
than the environment's name, which is
non-unique.

https://code.launchpad.net/~axwalk/juju-core/state-environment-life/+merge/195710

(do not edit description out of merge proposal)

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

Affected files (+172, -34 lines):
   A [revision details]
   M names/environ.go
   M state/environ.go
   M state/environ_test.go
   M state/initialize_test.go
   M state/interface.go
   M state/state.go
   M state/state_test.go
   M state/watcher.go

Revision history for this message
Andrew Wilkins (axwalk) wrote :

On 2013/11/19 06:47:52, axw wrote:
> Please take a look.

I should have mentioned in the description: this is an offshoot of
https://codereview.appspot.com/22870045/

The purpose of this CL is twofold:
  - to prevent machines and services from being added while an
environment is being destroyed
  - and to provide a clean-termination mechanism for state-server machine
agents

https://codereview.appspot.com/28880043/

Revision history for this message
Frank Mueller (themue) wrote :

LGTM, only a real minor comment.

https://codereview.appspot.com/28880043/diff/1/names/environ.go
File names/environ.go (right):

https://codereview.appspot.com/28880043/diff/1/names/environ.go#newcode15
names/environ.go:15: // IsEnvironment returns whether id is a valid
environment id.
id (in the comment) or name (in the code)?

https://codereview.appspot.com/28880043/

Revision history for this message
Andrew Wilkins (axwalk) wrote :

Please take a look.

https://codereview.appspot.com/28880043/diff/1/names/environ.go
File names/environ.go (right):

https://codereview.appspot.com/28880043/diff/1/names/environ.go#newcode15
names/environ.go:15: // IsEnvironment returns whether id is a valid
environment id.
On 2013/11/20 15:24:19, mue wrote:
> id (in the comment) or name (in the code)?

id, but actually, we can do that TODO now since the id is now a UUID
with a well defined format. Updated to do that, and updated tests.

https://codereview.appspot.com/28880043/

Revision history for this message
William Reade (fwereade) wrote :
Download full text (3.6 KiB)

Needs a bit more thought, I'm afraid.

https://codereview.appspot.com/28880043/diff/20001/names/environ.go
File names/environ.go (right):

https://codereview.appspot.com/28880043/diff/20001/names/environ.go#newcode16
names/environ.go:16: func IsEnvironment(id string) bool {
Have we checked all possible clients of this? I'm pretty sure we don't
accept environment tags ever, but we do produce them, and the GUI may
need to be prepared for this change -- please check with frankban.

https://codereview.appspot.com/28880043/diff/20001/state/environ.go
File state/environ.go (right):

https://codereview.appspot.com/28880043/diff/20001/state/environ.go#newcode85
state/environ.go:85: Assert: notDeadDoc,
If it's already dying we should abort, too, so this is just the
assertAliveOp

https://codereview.appspot.com/28880043/diff/20001/state/environ.go#newcode96
state/environ.go:96: return err
We can't track the complete set of services transactionally, so I reckon
the next best thing is to add a cleanup job that sets all services to
dying one the environment has been set to dying.

I kinda hate this, but we're stuck with it without a schemachange for a
total-service-refcount somewhere -- so please tag the code with
SCHEMACHANGE so we have a chance of fixing it.

https://codereview.appspot.com/28880043/diff/20001/state/environ.go#newcode108
state/environ.go:108: return onAbort(e.st.runTransaction([]txn.Op{op}),
nil)
I'm not sure how meaningful this code is at this point -- *everything*
has an implicit environment reference at the moment, so I'm not when
this can be sanely called. Ofc, something like this is a necessary
component of a multi-tenant server, in the future -- but it will
certainly require that removing the environment removes everything that
references it. Thoughts? Can we maybe even just stick with alive/dying
for now?

https://codereview.appspot.com/28880043/diff/20001/state/initialize_test.go
File state/initialize_test.go (right):

https://codereview.appspot.com/28880043/diff/20001/state/initialize_test.go#newcode75
state/initialize_test.go:75: annotations, err := annotator.Annotations()
Find out from the GUI guys exactly what they use environment annotations
for, if anything, and whether the tag change will affect them.

https://codereview.appspot.com/28880043/diff/20001/state/state.go
File state/state.go (right):

https://codereview.appspot.com/28880043/diff/20001/state/state.go#newcode359
state/state.go:359: env, err := st.Environment()
If we're going to pay the DB hit for the environment here, we should
probably check for life and abort here as well.

To avoid duplicating code, you may prefer to structure this as a loop
for i < 2.

https://codereview.appspot.com/28880043/diff/20001/state/state.go#newcode616
state/state.go:616: if id != env.UUID() {
It may be that this is the only bit we need to fix -- and we should just
accept that there's only one environment ever in play, so we should just
assume that environment-<anything> refers to the local environment.

That'll work for now, at least.

https://codereview.appspot.com/28880043/diff/20001/state/state.go#newcode735
state/state.go:735: return nil, err
Check for dying before running tran...

Read more...

Revision history for this message
Andrew Wilkins (axwalk) wrote :
Download full text (5.8 KiB)

Please take a look.

https://codereview.appspot.com/28880043/diff/20001/names/environ.go
File names/environ.go (right):

https://codereview.appspot.com/28880043/diff/20001/names/environ.go#newcode16
names/environ.go:16: func IsEnvironment(id string) bool {
On 2013/11/21 13:04:18, fwereade wrote:
> Have we checked all possible clients of this? I'm pretty sure we don't
accept
> environment tags ever, but we do produce them, and the GUI may need to
be
> prepared for this change -- please check with frankban.

Turns out that Landscape is using it, but only in IS. Have discussed
with Bjorn Tillenius, and he will update Landscape to use UUID with
fallback to name for the next version of Landscape, and we'll make the
change in the following version of Juju. After that is in IS, Landscape
can drop the backwards compatibility.

https://codereview.appspot.com/28880043/diff/20001/state/environ.go
File state/environ.go (right):

https://codereview.appspot.com/28880043/diff/20001/state/environ.go#newcode85
state/environ.go:85: Assert: notDeadDoc,
On 2013/11/21 13:04:18, fwereade wrote:
> If it's already dying we should abort, too, so this is just the
assertAliveOp

Why should we abort if Dying? While destroying other entities aborts if
they're dying, they ultimately do not return an error to the caller.

Is there a reason to prefer aborting and then checking if Life==Dying
and returning nil? Is the no-op modification not really a no-op?

I don't have a problem doing it, but I'd rather understand why first.

https://codereview.appspot.com/28880043/diff/20001/state/environ.go#newcode96
state/environ.go:96: return err
On 2013/11/21 13:04:18, fwereade wrote:
> We can't track the complete set of services transactionally, so I
reckon the
> next best thing is to add a cleanup job that sets all services to
dying one the
> environment has been set to dying.

Okay, I'll need to add a new cleanup kind then. What about machines? I
can't use the "machine" cleanup as that implies --force, which I don't
think we want here do we?

> I kinda hate this, but we're stuck with it without a schemachange for
a
> total-service-refcount somewhere -- so please tag the code with
SCHEMACHANGE so
> we have a chance of fixing it.

Done.

https://codereview.appspot.com/28880043/diff/20001/state/environ.go#newcode108
state/environ.go:108: return onAbort(e.st.runTransaction([]txn.Op{op}),
nil)
On 2013/11/21 13:04:18, fwereade wrote:
> I'm not sure how meaningful this code is at this point -- *everything*
has an
> implicit environment reference at the moment, so I'm not when this can
be sanely
> called. Ofc, something like this is a necessary component of a
multi-tenant
> server, in the future -- but it will certainly require that removing
the
> environment removes everything that references it. Thoughts? Can we
maybe even
> just stick with alive/dying for now?

Okay, fair enough. The intention was that Remove would trigger state
termination, so there'd be nothing left anyway.

Without it we're back to having no way of signalling manager nodes to
terminate. I will have to go back to my earlier approach of "ssh <host>
pkill -SIGABRT jujud" in the manual provider, and all others will be
de...

Read more...

Revision history for this message
William Reade (fwereade) wrote :
Download full text (3.8 KiB)

Nearly there, I think.

I do feel that accepting both env-tag kinds is the best way for us to
support the transition, but maybe I'm confused about something.

https://codereview.appspot.com/28880043/diff/20001/state/environ.go
File state/environ.go (right):

https://codereview.appspot.com/28880043/diff/20001/state/environ.go#newcode85
state/environ.go:85: Assert: notDeadDoc,
On 2013/11/28 09:42:48, axw wrote:
> On 2013/11/21 13:04:18, fwereade wrote:
> > If it's already dying we should abort, too, so this is just the
assertAliveOp

> Why should we abort if Dying? While destroying other entities aborts
if they're
> dying, they ultimately do not return an error to the caller.

> Is there a reason to prefer aborting and then checking if Life==Dying
and
> returning nil? Is the no-op modification not really a no-op?

> I don't have a problem doing it, but I'd rather understand why first.

It's just defensive -- if the transaction ever grows such that it's not
a no-op, it'll already be correct.

https://codereview.appspot.com/28880043/diff/20001/state/state.go
File state/state.go (right):

https://codereview.appspot.com/28880043/diff/20001/state/state.go#newcode616
state/state.go:616: if id != env.UUID() {
On 2013/11/28 09:42:48, axw wrote:
> On 2013/11/21 13:04:18, fwereade wrote:
> > It may be that this is the only bit we need to fix -- and we should
just
> accept
> > that there's only one environment ever in play, so we should just
assume that
> > environment-<anything> refers to the local environment.
> >
> > That'll work for now, at least.

> It's not: we also check the tag value looks like a UUID. We'll be fine
once
> Launchpad is updated.

For now, though, LP is not updated. Is it not better to accept both
forms (ie in IsEnvironment too), treat any value as "this environment",
and target a bug for some future point at which we can drop the old
form?

https://codereview.appspot.com/28880043/diff/20001/state/state_test.go
File state/state_test.go (right):

https://codereview.appspot.com/28880043/diff/20001/state/state_test.go#newcode567
state/state_test.go:567: }
On 2013/11/28 09:42:48, axw wrote:
> On 2013/11/21 13:04:18, fwereade wrote:
> > We'll need explicit testing for the possibility of new services
racing with
> > dying environments. If you do a cleanup job for services, the races
won't be a
> > problem in themselves.

> I've added the cleanup, and added a test in cleanup_test.go that
ensures that a
> cleanup is scheduled to destroy preexisting services.

The cleanup is good -- it shows that any services that did exist before
the env was destroyed will be cleaned up -- but I think we still need to
prevent new services from being created, lest they be added after the
cleanup has run.

https://codereview.appspot.com/28880043/diff/40001/state/environ.go
File state/environ.go (right):

https://codereview.appspot.com/28880043/diff/40001/state/environ.go#newcode84
state/environ.go:84: // not currently have.
I don't think this is quite right -- surely it's always ok to destroy an
environment and take down all the services, and all those other
machines? The real problem is manually-provisioned machines, of whatever
kind, which need more carefu...

Read more...

Revision history for this message
Andrew Wilkins (axwalk) wrote :
Download full text (3.9 KiB)

Please take a look.

https://codereview.appspot.com/28880043/diff/20001/state/environ.go
File state/environ.go (right):

https://codereview.appspot.com/28880043/diff/20001/state/environ.go#newcode85
state/environ.go:85: Assert: notDeadDoc,
On 2013/12/02 08:58:18, fwereade wrote:
> On 2013/11/28 09:42:48, axw wrote:
> > On 2013/11/21 13:04:18, fwereade wrote:
> > > If it's already dying we should abort, too, so this is just the
> assertAliveOp
> >
> > Why should we abort if Dying? While destroying other entities aborts
if
> they're
> > dying, they ultimately do not return an error to the caller.
> >
> > Is there a reason to prefer aborting and then checking if
Life==Dying and
> > returning nil? Is the no-op modification not really a no-op?
> >
> > I don't have a problem doing it, but I'd rather understand why
first.

> It's just defensive -- if the transaction ever grows such that it's
not a no-op,
> it'll already be correct.

Fair enough, thanks for explaining. Done.

https://codereview.appspot.com/28880043/diff/20001/state/state_test.go
File state/state_test.go (right):

https://codereview.appspot.com/28880043/diff/20001/state/state_test.go#newcode567
state/state_test.go:567: }
On 2013/12/02 08:58:18, fwereade wrote:
> On 2013/11/28 09:42:48, axw wrote:
> > On 2013/11/21 13:04:18, fwereade wrote:
> > > We'll need explicit testing for the possibility of new services
racing with
> > > dying environments. If you do a cleanup job for services, the
races won't be
> a
> > > problem in themselves.
> >
> > I've added the cleanup, and added a test in cleanup_test.go that
ensures that
> a
> > cleanup is scheduled to destroy preexisting services.

> The cleanup is good -- it shows that any services that did exist
before the env
> was destroyed will be cleaned up -- but I think we still need to
prevent new
> services from being created, lest they be added after the cleanup has
run.

This is handled with the "env.assertAliveOp" assertion in add-service
(and add-machine) transactions. Am I missing something?

https://codereview.appspot.com/28880043/diff/40001/state/environ.go
File state/environ.go (right):

https://codereview.appspot.com/28880043/diff/40001/state/environ.go#newcode84
state/environ.go:84: // not currently have.
On 2013/12/02 08:58:18, fwereade wrote:
> I don't think this is quite right -- surely it's always ok to destroy
an
> environment and take down all the services, and all those other
machines? The
> real problem is manually-provisioned machines, of whatever kind, which
need more
> careful cleanup (ie units and containers need to be removed properly).

After discussion on IRC, I'm going to remove this comment. I think the
way we're doing it is right. Destroy environment prevents new machines
and services; schedule cleanups for services to destroy the existing
ones; machines are destroyed via provider interface after
destroy-environment API returns successfully.

Manual is the exception, but for that we can just prevent
destroy-environment from completing while there are manual non-manager
machines in state, as discussed previously. We could, potentially,
destroy them later using the same mechanism as I intend to use for
manager machin...

Read more...

Revision history for this message
Andrew Wilkins (axwalk) wrote :

https://codereview.appspot.com/28880043/diff/20001/state/state_test.go
File state/state_test.go (right):

https://codereview.appspot.com/28880043/diff/20001/state/state_test.go#newcode567
state/state_test.go:567: }
On 2013/12/04 05:28:49, axw wrote:
> On 2013/12/02 08:58:18, fwereade wrote:
> > On 2013/11/28 09:42:48, axw wrote:
> > > On 2013/11/21 13:04:18, fwereade wrote:
> > > > We'll need explicit testing for the possibility of new services
racing
> with
> > > > dying environments. If you do a cleanup job for services, the
races won't
> be
> > a
> > > > problem in themselves.
> > >
> > > I've added the cleanup, and added a test in cleanup_test.go that
ensures
> that
> > a
> > > cleanup is scheduled to destroy preexisting services.
> >
> > The cleanup is good -- it shows that any services that did exist
before the
> env
> > was destroyed will be cleaned up -- but I think we still need to
prevent new
> > services from being created, lest they be added after the cleanup
has run.

> This is handled with the "env.assertAliveOp" assertion in add-service
(and
> add-machine) transactions. Am I missing something?

Ignore this comment, this was pre-discussion and I forgot to delete it.

https://codereview.appspot.com/28880043/

Revision history for this message
William Reade (fwereade) wrote :

Last few trivials. Thanks for bearing with me on this.

https://codereview.appspot.com/28880043/diff/60001/state/cleanup.go
File state/cleanup.go (right):

https://codereview.appspot.com/28880043/diff/60001/state/cleanup.go#newcode108
state/cleanup.go:108: for iter.Next(&service.doc) {
Hmm, I do worry a bit about annotations. I guess it's probably not that
important here.

https://codereview.appspot.com/28880043/diff/60001/state/environ.go
File state/environ.go (right):

https://codereview.appspot.com/28880043/diff/60001/state/environ.go#newcode69
state/environ.go:69: err := e.st.environments.Find(nil).One(&e.doc)
FindId(envGlobalKey)? I'm sure there's a constant alone those lines
somewhere.

https://codereview.appspot.com/28880043/diff/60001/state/environ.go#newcode79
state/environ.go:79: if e.Life() == Dying {
!= Alive?

https://codereview.appspot.com/28880043/diff/60001/state/environ.go#newcode85
state/environ.go:85: // of which prevent a successful
destroy-environment.
This isn't enforced in state, just at the API level; maybe make that
clearer?

Hopefully soon we'll have this stuff behind the API and be able to
maintain it properly...

https://codereview.appspot.com/28880043/diff/60001/state/environ.go#newcode105
state/environ.go:105: panic(err)
Please, no panics in the API server. And it's not an error to destroy
something that's already dead -- given that this is currently the only
fail case, I think we're ok just returning nil.

If you're worried about the inconsistency compared to what I said about
the transaction list I wouldn't object to an explicit check -- in the
case where we can't figure out what went wrong we usually go with
ErrExcessiveContention (even though in practice it *usually* means
ErrSomeoneMessedUpTheCode).

https://codereview.appspot.com/28880043/diff/60001/state/initialize_test.go
File state/initialize_test.go (left):

https://codereview.appspot.com/28880043/diff/60001/state/initialize_test.go#oldcode69
state/initialize_test.go:69: env0, err :=
s.State.FindEntity("environment-" + cfg.Name())
Shouldn't this still work, until we know no clients are still using it?

https://codereview.appspot.com/28880043/diff/60001/state/state.go
File state/state.go (right):

https://codereview.appspot.com/28880043/diff/60001/state/state.go#newcode785
state/state.go:785: if err := env.Refresh(); err != nil {
Generally we go with "X is no longer alive", IIRC -- because then you
get a consistent message for a NotFound. Or at least it's *usually*
consistent, and in this case I think it always is, because to have made
a state connection implies the existence of this environment at some
point in the past.

Probably worth using that message for all the "is being destroyed"s in
this CL...

https://codereview.appspot.com/28880043/

Revision history for this message
Andrew Wilkins (axwalk) wrote :
Download full text (3.4 KiB)

Please take a look.

https://codereview.appspot.com/28880043/diff/60001/state/cleanup.go
File state/cleanup.go (right):

https://codereview.appspot.com/28880043/diff/60001/state/cleanup.go#newcode108
state/cleanup.go:108: for iter.Next(&service.doc) {
On 2013/12/04 14:03:23, fwereade wrote:
> Hmm, I do worry a bit about annotations. I guess it's probably not
that
> important here.

Sorry, what about annotations?

https://codereview.appspot.com/28880043/diff/60001/state/environ.go
File state/environ.go (right):

https://codereview.appspot.com/28880043/diff/60001/state/environ.go#newcode69
state/environ.go:69: err := e.st.environments.Find(nil).One(&e.doc)
On 2013/12/04 14:03:23, fwereade wrote:
> FindId(envGlobalKey)? I'm sure there's a constant alone those lines
somewhere.

Updated to use FindId(UUID). State.Environment() still needs to use
Find(nil), since it doesn't know the UUID.

https://codereview.appspot.com/28880043/diff/60001/state/environ.go#newcode79
state/environ.go:79: if e.Life() == Dying {
On 2013/12/04 14:03:23, fwereade wrote:
> != Alive?

Done.

https://codereview.appspot.com/28880043/diff/60001/state/environ.go#newcode85
state/environ.go:85: // of which prevent a successful
destroy-environment.
On 2013/12/04 14:03:23, fwereade wrote:
> This isn't enforced in state, just at the API level; maybe make that
clearer?

> Hopefully soon we'll have this stuff behind the API and be able to
maintain it
> properly...

Done.

https://codereview.appspot.com/28880043/diff/60001/state/environ.go#newcode105
state/environ.go:105: panic(err)
On 2013/12/04 14:03:23, fwereade wrote:
> Please, no panics in the API server. And it's not an error to destroy
something
> that's already dead -- given that this is currently the only fail
case, I think
> we're ok just returning nil.

> If you're worried about the inconsistency compared to what I said
about the
> transaction list I wouldn't object to an explicit check -- in the case
where we
> can't figure out what went wrong we usually go with
ErrExcessiveContention (even
> though in practice it *usually* means ErrSomeoneMessedUpTheCode).

Fixed. Sorry, I was under the mistaken impression that the other Destroy
methods returned an error if Destroy was called on something that's
dead. I appear to be mistaken.

https://codereview.appspot.com/28880043/diff/60001/state/initialize_test.go
File state/initialize_test.go (left):

https://codereview.appspot.com/28880043/diff/60001/state/initialize_test.go#oldcode69
state/initialize_test.go:69: env0, err :=
s.State.FindEntity("environment-" + cfg.Name())
On 2013/12/04 14:03:23, fwereade wrote:
> Shouldn't this still work, until we know no clients are still using
it?

Yes, it should and does. Backwards compatibility is tested explicitly in
TestFindEntity.

https://codereview.appspot.com/28880043/diff/60001/state/state.go
File state/state.go (right):

https://codereview.appspot.com/28880043/diff/60001/state/state.go#newcode785
state/state.go:785: if err := env.Refresh(); err != nil {
On 2013/12/04 14:03:23, fwereade wrote:
> Generally we go with "X is no longer alive", IIRC -- because then you
get a
> consistent message for a NotFound. Or at least it's *usually*
c...

Read more...

Revision history for this message
William Reade (fwereade) wrote :
Download full text (4.9 KiB)

Final round of trivials. LGTM with the below if they're clear to you.

https://codereview.appspot.com/28880043/diff/60001/state/cleanup.go
File state/cleanup.go (right):

https://codereview.appspot.com/28880043/diff/60001/state/cleanup.go#newcode108
state/cleanup.go:108: for iter.Next(&service.doc) {
On 2013/12/06 08:37:40, axw wrote:
> On 2013/12/04 14:03:23, fwereade wrote:
> > Hmm, I do worry a bit about annotations. I guess it's probably not
that
> > important here.

> Sorry, what about annotations?

The service type -- and a few others -- have annotator fields that we're
really lax about cloning properly because they were added really late
and aren't generally used (except by the API) IIRC. It doesn't matter...
until it does... but I think this is a case where it doesn't because
they won't leak out of this method at all.

It just tripped one of my "watch out for this" flags, but not usefully
or actionably -- I probably just shouldn't have mentioned it.

https://codereview.appspot.com/28880043/diff/60001/state/environ.go
File state/environ.go (right):

https://codereview.appspot.com/28880043/diff/60001/state/environ.go#newcode105
state/environ.go:105: panic(err)
On 2013/12/06 08:37:40, axw wrote:
> On 2013/12/04 14:03:23, fwereade wrote:
> > Please, no panics in the API server. And it's not an error to
destroy
> something
> > that's already dead -- given that this is currently the only fail
case, I
> think
> > we're ok just returning nil.
> >
> > If you're worried about the inconsistency compared to what I said
about the
> > transaction list I wouldn't object to an explicit check -- in the
case where
> we
> > can't figure out what went wrong we usually go with
ErrExcessiveContention
> (even
> > though in practice it *usually* means ErrSomeoneMessedUpTheCode).

> Fixed. Sorry, I was under the mistaken impression that the other
Destroy methods
> returned an error if Destroy was called on something that's dead. I
appear to be
> mistaken.

No worries :). The rationale is that racing destroys are not a problem
-- both clients want it dead, and both clients' desires will be
satisfied no matter what order things happen in, so we should be good.

https://codereview.appspot.com/28880043/diff/80001/state/environ.go
File state/environ.go (right):

https://codereview.appspot.com/28880043/diff/80001/state/environ.go#newcode90
state/environ.go:90: // manual machines exist.
This is a race and deserves a bug. Doesn't need to be fixed today
though.

https://codereview.appspot.com/28880043/diff/80001/state/environ.go#newcode104
state/environ.go:104: err = e.Refresh()
hmm, but if it's actually *removed* -- in the multi-tenant future, in
which it can be removed -- this'll be an error where it doesn't actually
need to be. I'd be fine with setting `e.doc.Life = Dying` (and returning
nil) in the case of either nil or ErrAborted.

(FWIW, the reason that's not crazy is that setting it to Dying doesn't
violate any expectations at the client end: all the client sees is that
the lifecycle advanced, as expected, and if it happens to then refresh
and find it gone that's still consistent with the model. We couldn't set
Dead speculatively, because it might actually jus...

Read more...

Revision history for this message
Andrew Wilkins (axwalk) wrote :

https://codereview.appspot.com/28880043/diff/80001/state/environ.go
File state/environ.go (right):

https://codereview.appspot.com/28880043/diff/80001/state/environ.go#newcode90
state/environ.go:90: // manual machines exist.
On 2013/12/09 10:14:41, fwereade wrote:
> This is a race and deserves a bug. Doesn't need to be fixed today
though.

Can you please clarify? What is the race exactly? Sorry if I'm being
dense.

https://codereview.appspot.com/28880043/diff/80001/state/environ.go#newcode104
state/environ.go:104: err = e.Refresh()
On 2013/12/09 10:14:41, fwereade wrote:
> hmm, but if it's actually *removed* -- in the multi-tenant future, in
which it
> can be removed -- this'll be an error where it doesn't actually need
to be. I'd
> be fine with setting `e.doc.Life = Dying` (and returning nil) in the
case of
> either nil or ErrAborted.

> (FWIW, the reason that's not crazy is that setting it to Dying doesn't
violate
> any expectations at the client end: all the client sees is that the
lifecycle
> advanced, as expected, and if it happens to then refresh and find it
gone that's
> still consistent with the model. We couldn't set Dead speculatively,
because it
> might actually just be Dying, and thereby run the risk of a later
refresh moving
> the life state back from Dead to Dying: *that* would be a violation of
client
> expectations.)

Sounds crazy at first blush, but I understand and it sounds reasonable.
Will do.

https://codereview.appspot.com/28880043/

Revision history for this message
Andrew Wilkins (axwalk) wrote :
Download full text (3.2 KiB)

Please take a look.

https://codereview.appspot.com/28880043/diff/80001/state/environ.go
File state/environ.go (right):

https://codereview.appspot.com/28880043/diff/80001/state/environ.go#newcode104
state/environ.go:104: err = e.Refresh()
On 2013/12/09 13:27:05, axw wrote:
> On 2013/12/09 10:14:41, fwereade wrote:
> > hmm, but if it's actually *removed* -- in the multi-tenant future,
in which it
> > can be removed -- this'll be an error where it doesn't actually need
to be.
> I'd
> > be fine with setting `e.doc.Life = Dying` (and returning nil) in the
case of
> > either nil or ErrAborted.
> >
> > (FWIW, the reason that's not crazy is that setting it to Dying
doesn't violate
> > any expectations at the client end: all the client sees is that the
lifecycle
> > advanced, as expected, and if it happens to then refresh and find it
gone
> that's
> > still consistent with the model. We couldn't set Dead speculatively,
because
> it
> > might actually just be Dying, and thereby run the risk of a later
refresh
> moving
> > the life state back from Dead to Dying: *that* would be a violation
of client
> > expectations.)

> Sounds crazy at first blush, but I understand and it sounds
reasonable. Will do.

Done.

https://codereview.appspot.com/28880043/diff/80001/state/environ.go#newcode121
state/environ.go:121: // assertAliveOp returns a read-only txn.Op that
asserts
On 2013/12/09 10:14:41, fwereade wrote:
> I think "read-only" is slightly ambiguous and doesn't really add
value. YMMV,
> though, keep it if you feel strongly.

Done.

https://codereview.appspot.com/28880043/diff/80001/state/initialize_test.go
File state/initialize_test.go (right):

https://codereview.appspot.com/28880043/diff/80001/state/initialize_test.go#newcode54
state/initialize_test.go:54: // TODO(axw) remove backwards compatibility
for environment-tag; see lp:1257587
On 2013/12/09 10:14:41, fwereade wrote:
> There's a standard format for these:

> TODO(person) date bug#
> explanation

Done.

https://codereview.appspot.com/28880043/diff/80001/state/state.go
File state/state.go (right):

https://codereview.appspot.com/28880043/diff/80001/state/state.go#newcode492
state/state.go:492: return nil, fmt.Errorf("environment is no longer
alive")
On 2013/12/09 10:14:41, fwereade wrote:
> Again, future-proofing: if we get a NotFound, convert it to "no longer
alive".

Sorry, I don't see what that would future proof against. What's wrong
with just returning not found when the environment is not found?

https://codereview.appspot.com/28880043/diff/80001/state/state.go#newcode631
state/state.go:631: if id != conf.Name() {
On 2013/12/09 10:14:41, fwereade wrote:
> Hmm. Should we log a warning instead here, and just return the env
anyway? As we
> move towards names being aliases, we open up the possibility of
multiple clients
> having different names for the same environment. That may be a
paranoion too far
> though... I can't see a path that enables multiple names before we
switch to
> using UUIDs across the board.

Do we need to support that before we move to UUIDs? I previously had the
code supporting anything, but changed it to check name again. That's
what it was doing before. Can we leave future...

Read more...

Revision history for this message
William Reade (fwereade) wrote :

https://codereview.appspot.com/28880043/diff/80001/state/environ.go
File state/environ.go (right):

https://codereview.appspot.com/28880043/diff/80001/state/environ.go#newcode90
state/environ.go:90: // manual machines exist.
On 2013/12/09 13:27:05, axw wrote:
> On 2013/12/09 10:14:41, fwereade wrote:
> > This is a race and deserves a bug. Doesn't need to be fixed today
though.

> Can you please clarify? What is the race exactly? Sorry if I'm being
dense.

Client 1 checks no manual machines
Client 2 adds a manual machine
Client 1 destroys environment

Bad Thing.

If you're still around when I come online tomorrow morning, can we chat
a little bit about making machines' providers explicit? At the moment
the best we have is the "manual:" prefix on instance ids, but I don't
think that's quite adequate -- it's merely *unlikely* that such an id
willcollide with a legitimate one from another provider.

https://codereview.appspot.com/28880043/

Revision history for this message
Andrew Wilkins (axwalk) wrote :

On 2013/12/11 07:46:22, fwereade wrote:
> https://codereview.appspot.com/28880043/diff/80001/state/environ.go
> File state/environ.go (right):

https://codereview.appspot.com/28880043/diff/80001/state/environ.go#newcode90
> state/environ.go:90: // manual machines exist.
> On 2013/12/09 13:27:05, axw wrote:
> > On 2013/12/09 10:14:41, fwereade wrote:
> > > This is a race and deserves a bug. Doesn't need to be fixed today
though.
> >
> > Can you please clarify? What is the race exactly? Sorry if I'm being
dense.

> Client 1 checks no manual machines
> Client 2 adds a manual machine
> Client 1 destroys environment

> Bad Thing.

As discussed on IRC: the way I *had* intended to handle that was to
first set Environment to Dying, and then check for manual machines. But,
as you point out, this leaves the environment in an unusable state if it
does fail. I'll leave the race in there (with a TODO), and we can come
back to it with a way to transactionally ensure no manual machines have
been added since last check.

> If you're still around when I come online tomorrow morning, can we
chat a little
> bit about making machines' providers explicit? At the moment the best
we have is
> the "manual:" prefix on instance ids, but I don't think that's quite
adequate --
> it's merely *unlikely* that such an id willcollide with a legitimate
one from
> another provider.

https://codereview.appspot.com/28880043/

Revision history for this message
Andrew Wilkins (axwalk) wrote :

Please take a look.

https://codereview.appspot.com/28880043/diff/80001/state/state.go
File state/state.go (right):

https://codereview.appspot.com/28880043/diff/80001/state/state.go#newcode492
state/state.go:492: return nil, fmt.Errorf("environment is no longer
alive")
On 2013/12/11 07:35:20, axw wrote:
> On 2013/12/09 10:14:41, fwereade wrote:
> > Again, future-proofing: if we get a NotFound, convert it to "no
longer alive".

> Sorry, I don't see what that would future proof against. What's wrong
with just
> returning not found when the environment is not found?

Done.

https://codereview.appspot.com/28880043/diff/80001/state/state.go#newcode631
state/state.go:631: if id != conf.Name() {
On 2013/12/11 07:35:20, axw wrote:
> On 2013/12/09 10:14:41, fwereade wrote:
> > Hmm. Should we log a warning instead here, and just return the env
anyway? As
> we
> > move towards names being aliases, we open up the possibility of
multiple
> clients
> > having different names for the same environment. That may be a
paranoion too
> far
> > though... I can't see a path that enables multiple names before we
switch to
> > using UUIDs across the board.

> Do we need to support that before we move to UUIDs? I previously had
the code
> supporting anything, but changed it to check name again. That's what
it was
> doing before. Can we leave future functionality to the future, or does
this need
> to be done now?

I'm not convinced either way, but not doing it could put us in a
difficult position later. Done.

https://codereview.appspot.com/28880043/diff/80001/state/state.go#newcode795
state/state.go:795: return nil, fmt.Errorf("environment is no longer
alive")
On 2013/12/09 10:14:41, fwereade wrote:
> ditto NotFound handling

Done.

https://codereview.appspot.com/28880043/

Revision history for this message
Andrew Wilkins (axwalk) wrote :

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'names/environ.go'
2--- names/environ.go 2013-09-13 14:48:13 +0000
3+++ names/environ.go 2013-12-11 09:15:40 +0000
4@@ -7,13 +7,15 @@
5 "strings"
6 )
7
8-// EnvironTag returns the tag of an environment with the given name.
9-func EnvironTag(name string) string {
10- return makeTag(EnvironTagKind, name)
11+// EnvironTag returns the tag of an environment with the given environment UUID.
12+func EnvironTag(uuid string) string {
13+ return makeTag(EnvironTagKind, uuid)
14 }
15
16-// IsEnvironment returns whether id is a valid environment id.
17-// TODO(rog) stricter constraints
18-func IsEnvironment(name string) bool {
19- return !strings.Contains(name, "/")
20+// IsEnvironment returns whether id is a valid environment UUID.
21+func IsEnvironment(id string) bool {
22+ // TODO(axw) 2013-12-04 #1257587
23+ // We should not accept environment tags that
24+ // do not look like UUIDs.
25+ return !strings.Contains(id, "/")
26 }
27
28=== modified file 'state/addmachine.go'
29--- state/addmachine.go 2013-12-03 18:09:32 +0000
30+++ state/addmachine.go 2013-12-11 09:15:40 +0000
31@@ -10,6 +10,7 @@
32 "labix.org/v2/mgo/txn"
33
34 "launchpad.net/juju-core/constraints"
35+ "launchpad.net/juju-core/errors"
36 "launchpad.net/juju-core/instance"
37 "launchpad.net/juju-core/state/api/params"
38 "launchpad.net/juju-core/utils"
39@@ -176,7 +177,20 @@
40 }
41
42 func (st *State) addMachine(mdoc *machineDoc, ops []txn.Op) (*Machine, error) {
43+ env, err := st.Environment()
44+ if err != nil {
45+ return nil, err
46+ } else if env.Life() != Alive {
47+ return nil, fmt.Errorf("environment is no longer alive")
48+ }
49+ ops = append([]txn.Op{env.assertAliveOp()}, ops...)
50 if err := st.runTransaction(ops); err != nil {
51+ enverr := env.Refresh()
52+ if (enverr == nil && env.Life() != Alive) || errors.IsNotFoundError(enverr) {
53+ return nil, fmt.Errorf("environment is no longer alive")
54+ } else if enverr != nil {
55+ err = enverr
56+ }
57 return nil, err
58 }
59 m := newMachine(st, mdoc)
60
61=== modified file 'state/cleanup.go'
62--- state/cleanup.go 2013-12-10 09:28:57 +0000
63+++ state/cleanup.go 2013-12-11 09:15:40 +0000
64@@ -55,6 +55,8 @@
65 err = st.cleanupSettings(doc.Prefix)
66 case "units":
67 err = st.cleanupUnits(doc.Prefix)
68+ case "services":
69+ err = st.cleanupServices()
70 case "machine":
71 err = st.cleanupMachine(doc.Prefix)
72 default:
73@@ -94,6 +96,26 @@
74 return nil
75 }
76
77+// cleanupServices sets all services to Dying, if they are not already Dying
78+// or Dead. It's expected to be used when an environment is destroyed.
79+func (st *State) cleanupServices() error {
80+ // This won't miss services, because a Dying environment cannot have
81+ // services added to it. But we do have to remove the services themselves
82+ // via individual transactions, because they could be in any state at all.
83+ service := &Service{st: st}
84+ sel := D{{"life", Alive}}
85+ iter := st.services.Find(sel).Iter()
86+ for iter.Next(&service.doc) {
87+ if err := service.Destroy(); err != nil {
88+ return err
89+ }
90+ }
91+ if err := iter.Err(); err != nil {
92+ return fmt.Errorf("cannot read service document: %v", err)
93+ }
94+ return nil
95+}
96+
97 // cleanupUnits sets all units with the given prefix to Dying, if they are not
98 // already Dying or Dead. It's expected to be used when a service is destroyed.
99 func (st *State) cleanupUnits(prefix string) error {
100
101=== modified file 'state/cleanup_test.go'
102--- state/cleanup_test.go 2013-12-10 09:28:57 +0000
103+++ state/cleanup_test.go 2013-12-11 09:15:40 +0000
104@@ -55,6 +55,48 @@
105 c.Assert(err, jc.Satisfies, errors.IsNotFoundError)
106 }
107
108+func (s *CleanupSuite) TestCleanupEnvironmentServices(c *gc.C) {
109+ s.assertDoesNotNeedCleanup(c)
110+
111+ // Create a service with some units.
112+ mysql, err := s.State.AddService("mysql", s.AddTestingCharm(c, "mysql"))
113+ c.Assert(err, gc.IsNil)
114+ units := make([]*state.Unit, 3)
115+ for i := range units {
116+ unit, err := mysql.AddUnit()
117+ c.Assert(err, gc.IsNil)
118+ units[i] = unit
119+ }
120+ s.assertDoesNotNeedCleanup(c)
121+
122+ // Destroy the environment and check the service and units are
123+ // unaffected, but a cleanup for the service has been scheduled.
124+ env, err := s.State.Environment()
125+ c.Assert(err, gc.IsNil)
126+ err = env.Destroy()
127+ c.Assert(err, gc.IsNil)
128+ s.assertNeedsCleanup(c)
129+ s.assertCleanupRuns(c)
130+ err = mysql.Refresh()
131+ c.Assert(err, gc.IsNil)
132+ c.Assert(mysql.Life(), gc.Equals, state.Dying)
133+ for _, unit := range units {
134+ err = unit.Refresh()
135+ c.Assert(err, gc.IsNil)
136+ c.Assert(unit.Life(), gc.Equals, state.Alive)
137+ }
138+
139+ // The first cleanup Destroys the service, which
140+ // schedules another cleanup to destroy the units.
141+ s.assertNeedsCleanup(c)
142+ s.assertCleanupRuns(c)
143+ for _, unit := range units {
144+ err = unit.Refresh()
145+ c.Assert(err, jc.Satisfies, errors.IsNotFoundError)
146+ }
147+ s.assertDoesNotNeedCleanup(c)
148+}
149+
150 func (s *CleanupSuite) TestCleanupRelationSettings(c *gc.C) {
151 s.assertDoesNotNeedCleanup(c)
152
153
154=== modified file 'state/environ.go'
155--- state/environ.go 2013-11-15 13:02:24 +0000
156+++ state/environ.go 2013-12-11 09:15:40 +0000
157@@ -26,21 +26,15 @@
158 type environmentDoc struct {
159 UUID string `bson:"_id"`
160 Name string
161+ Life Life
162 }
163
164 // Environment returns the environment entity.
165 func (st *State) Environment() (*Environment, error) {
166- doc := environmentDoc{}
167- err := st.environments.Find(nil).One(&doc)
168- if err == mgo.ErrNotFound {
169- return nil, errors.NotFoundf("environment")
170- } else if err != nil {
171+ env := &Environment{st: st}
172+ if err := env.refresh(st.environments.Find(nil)); err != nil {
173 return nil, err
174 }
175- env := &Environment{
176- st: st,
177- doc: doc,
178- }
179 env.annotator = annotator{
180 globalKey: env.globalKey(),
181 tag: env.Tag(),
182@@ -52,24 +46,77 @@
183 // Tag returns a name identifying the environment.
184 // The returned name will be different from other Tag values returned
185 // by any other entities from the same state.
186-func (e Environment) Tag() string {
187- return names.EnvironTag(e.doc.Name)
188+func (e *Environment) Tag() string {
189+ return names.EnvironTag(e.doc.UUID)
190 }
191
192 // UUID returns the universally unique identifier of the environment.
193-func (e Environment) UUID() string {
194+func (e *Environment) UUID() string {
195 return e.doc.UUID
196 }
197
198+// Life returns whether the environment is Alive, Dying or Dead.
199+func (e *Environment) Life() Life {
200+ return e.doc.Life
201+}
202+
203 // globalKey returns the global database key for the environment.
204 func (e *Environment) globalKey() string {
205 return environGlobalKey
206 }
207
208+func (e *Environment) Refresh() error {
209+ return e.refresh(e.st.environments.FindId(e.UUID()))
210+}
211+
212+func (e *Environment) refresh(query *mgo.Query) error {
213+ err := query.One(&e.doc)
214+ if err == mgo.ErrNotFound {
215+ return errors.NotFoundf("environment")
216+ }
217+ return err
218+}
219+
220+// Destroy sets the environment's lifecycle to Dying, preventing
221+// addition of services or machines to state.
222+func (e *Environment) Destroy() error {
223+ if e.Life() != Alive {
224+ return nil
225+ }
226+ // TODO(axw) 2013-12-11 #1218688
227+ // Resolve the race between checking for manual machines and
228+ // destroying the environment. We can set Environment to Dying
229+ // to resolve the race, but that puts the environment into an
230+ // unusable state.
231+ //
232+ // We add a cleanup for services, but not for machines;
233+ // machines are destroyed via the provider interface. The
234+ // exception to this rule is manual machines; the API prevents
235+ // destroy-environment from succeeding if any non-manager
236+ // manual machines exist.
237+ ops := []txn.Op{{
238+ C: e.st.environments.Name,
239+ Id: e.doc.UUID,
240+ Update: D{{"$set", D{{"life", Dying}}}},
241+ Assert: isAliveDoc,
242+ }, e.st.newCleanupOp("services", "")}
243+ err := e.st.runTransaction(ops)
244+ switch err {
245+ case nil, txn.ErrAborted:
246+ // If the transaction aborted, the environment is either
247+ // Dying or Dead; neither case is an error. If it's Dead,
248+ // reporting it as Dying is not incorrect; the user thought
249+ // it was Alive, so we've progressed towards Dead. If the
250+ // user then calls Refresh they'll get the true value.
251+ e.doc.Life = Dying
252+ }
253+ return err
254+}
255+
256 // createEnvironmentOp returns the operation needed to create
257 // an environment document with the given name and UUID.
258 func createEnvironmentOp(st *State, name, uuid string) txn.Op {
259- doc := &environmentDoc{uuid, name}
260+ doc := &environmentDoc{uuid, name, Alive}
261 return txn.Op{
262 C: st.environments.Name,
263 Id: uuid,
264@@ -77,3 +124,12 @@
265 Insert: doc,
266 }
267 }
268+
269+// assertAliveOp returns a txn.Op that asserts the environment is alive.
270+func (e *Environment) assertAliveOp() txn.Op {
271+ return txn.Op{
272+ C: e.st.environments.Name,
273+ Id: e.UUID(),
274+ Assert: isAliveDoc,
275+ }
276+}
277
278=== modified file 'state/environ_test.go'
279--- state/environ_test.go 2013-08-19 11:20:02 +0000
280+++ state/environ_test.go 2013-12-11 09:15:40 +0000
281@@ -24,9 +24,7 @@
282 }
283
284 func (s *EnvironSuite) TestTag(c *gc.C) {
285- cfg, err := s.State.EnvironConfig()
286- c.Assert(err, gc.IsNil)
287- expected := "environment-" + cfg.Name()
288+ expected := "environment-" + s.env.UUID()
289 c.Assert(s.env.Tag(), gc.Equals, expected)
290 }
291
292
293=== modified file 'state/initialize_test.go'
294--- state/initialize_test.go 2013-12-06 04:47:45 +0000
295+++ state/initialize_test.go 2013-12-11 09:15:40 +0000
296@@ -51,6 +51,8 @@
297 _, err := s.State.EnvironConfig()
298 c.Assert(err, jc.Satisfies, errors.IsNotFoundError)
299 _, err = s.State.FindEntity("environment-foo")
300+ // TODO(axw) 2013-12-04 #1257587
301+ // remove backwards compatibility for environment-tag; see state.go
302 c.Assert(err, jc.Satisfies, errors.IsNotFoundError)
303 _, err = s.State.EnvironConstraints()
304 c.Assert(err, jc.Satisfies, errors.IsNotFoundError)
305@@ -66,10 +68,13 @@
306 cfg, err = s.State.EnvironConfig()
307 c.Assert(err, gc.IsNil)
308 c.Assert(cfg.AllAttrs(), gc.DeepEquals, initial)
309- env0, err := s.State.FindEntity("environment-" + cfg.Name())
310- c.Assert(err, gc.IsNil)
311- env := env0.(state.Annotator)
312- annotations, err := env.Annotations()
313+
314+ env, err := s.State.Environment()
315+ c.Assert(err, gc.IsNil)
316+ entity, err := s.State.FindEntity("environment-" + env.UUID())
317+ c.Assert(err, gc.IsNil)
318+ annotator := entity.(state.Annotator)
319+ annotations, err := annotator.Annotations()
320 c.Assert(err, gc.IsNil)
321 c.Assert(annotations, gc.HasLen, 0)
322 cons, err := s.State.EnvironConstraints()
323
324=== modified file 'state/interface.go'
325--- state/interface.go 2013-10-02 13:14:49 +0000
326+++ state/interface.go 2013-12-11 09:15:40 +0000
327@@ -128,6 +128,7 @@
328 _ NotifyWatcherFactory = (*Machine)(nil)
329 _ NotifyWatcherFactory = (*Unit)(nil)
330 _ NotifyWatcherFactory = (*Service)(nil)
331+ _ NotifyWatcherFactory = (*Environment)(nil)
332 )
333
334 // AgentEntity represents an entity that can
335
336=== modified file 'state/state.go'
337--- state/state.go 2013-12-06 04:47:45 +0000
338+++ state/state.go 2013-12-11 09:15:40 +0000
339@@ -390,16 +390,29 @@
340 case names.ServiceTagKind:
341 return st.Service(id)
342 case names.EnvironTagKind:
343- conf, err := st.EnvironConfig()
344+ env, err := st.Environment()
345 if err != nil {
346 return nil, err
347 }
348 // Return an invalid entity error if the requested environment is not
349 // the current one.
350- if id != conf.Name() {
351- return nil, errors.NotFoundf("environment %q", id)
352+ if id != env.UUID() {
353+ if utils.IsValidUUIDString(id) {
354+ return nil, errors.NotFoundf("environment %q", id)
355+ }
356+ // TODO(axw) 2013-12-04 #1257587
357+ // We should not accept environment tags that do not match the
358+ // environment's UUID. We accept anything for now, to cater
359+ // both for past usage, and for potentially supporting aliases.
360+ logger.Warningf("environment-tag does not match current environment UUID: %q != %q", id, env.UUID())
361+ conf, err := st.EnvironConfig()
362+ if err != nil {
363+ logger.Warningf("EnvironConfig failed: %v", err)
364+ } else if id != conf.Name() {
365+ logger.Warningf("environment-tag does not match current environment name: %q != %q", id, conf.Name())
366+ }
367 }
368- return st.Environment()
369+ return env, nil
370 case names.RelationTagKind:
371 return st.KeyRelation(id)
372 }
373@@ -424,6 +437,8 @@
374 coll = st.users.Name
375 case names.RelationTagKind:
376 coll = st.relations.Name
377+ case names.EnvironTagKind:
378+ coll = st.environments.Name
379 default:
380 return "", "", fmt.Errorf("%q is not a valid collection tag", tag)
381 }
382@@ -511,6 +526,12 @@
383 } else if exists {
384 return nil, fmt.Errorf("service already exists")
385 }
386+ env, err := st.Environment()
387+ if err != nil {
388+ return nil, err
389+ } else if env.Life() != Alive {
390+ return nil, fmt.Errorf("environment is no longer alive")
391+ }
392 // Create the service addition operations.
393 peers := ch.Meta().Peers
394 svcDoc := &serviceDoc{
395@@ -523,6 +544,7 @@
396 }
397 svc := newService(st, svcDoc)
398 ops := []txn.Op{
399+ env.assertAliveOp(),
400 createConstraintsOp(st, svc.globalKey(), constraints.Value{}),
401 createSettingsOp(st, svc.settingsKey(), nil),
402 {
403@@ -544,9 +566,15 @@
404 ops = append(ops, peerOps...)
405
406 // Run the transaction; happily, there's never any reason to retry,
407- // because all the possible failed assertions imply that the service
408- // already exists.
409+ // because all the possible failed assertions imply that either the
410+ // service already exists, or the environment is being destroyed.
411 if err := st.runTransaction(ops); err == txn.ErrAborted {
412+ err := env.Refresh()
413+ if (err == nil && env.Life() != Alive) || errors.IsNotFoundError(err) {
414+ return nil, fmt.Errorf("environment is no longer alive")
415+ } else if err != nil {
416+ return nil, err
417+ }
418 return nil, fmt.Errorf("service already exists")
419 } else if err != nil {
420 return nil, err
421
422=== modified file 'state/state_test.go'
423--- state/state_test.go 2013-12-06 04:47:45 +0000
424+++ state/state_test.go 2013-12-11 09:15:40 +0000
425@@ -207,6 +207,33 @@
426 check(m[1], "1", "blahblah", allJobs)
427 }
428
429+func (s *StateSuite) TestAddMachinesEnvironmentDying(c *gc.C) {
430+ _, err := s.State.AddMachine("quantal", state.JobHostUnits)
431+ c.Assert(err, gc.IsNil)
432+ env, err := s.State.Environment()
433+ c.Assert(err, gc.IsNil)
434+ err = env.Destroy()
435+ c.Assert(err, gc.IsNil)
436+ // Check that machines cannot be added if the environment is initially Dying.
437+ _, err = s.State.AddMachine("quantal", state.JobHostUnits)
438+ c.Assert(err, gc.ErrorMatches, "cannot add a new machine: environment is no longer alive")
439+}
440+
441+func (s *StateSuite) TestAddMachinesEnvironmentDyingAfterInitial(c *gc.C) {
442+ _, err := s.State.AddMachine("quantal", state.JobHostUnits)
443+ c.Assert(err, gc.IsNil)
444+ env, err := s.State.Environment()
445+ c.Assert(err, gc.IsNil)
446+ // Check that machines cannot be added if the environment is initially
447+ // Alive but set to Dying immediately before the transaction is run.
448+ defer state.SetBeforeHooks(c, s.State, func() {
449+ c.Assert(env.Life(), gc.Equals, state.Alive)
450+ c.Assert(env.Destroy(), gc.IsNil)
451+ }).Check()
452+ _, err = s.State.AddMachine("quantal", state.JobHostUnits)
453+ c.Assert(err, gc.ErrorMatches, "cannot add a new machine: environment is no longer alive")
454+}
455+
456 func (s *StateSuite) TestAddMachineExtraConstraints(c *gc.C) {
457 err := s.State.SetEnvironConstraints(constraints.MustParse("mem=4G"))
458 c.Assert(err, gc.IsNil)
459@@ -585,6 +612,35 @@
460 c.Assert(ch.URL(), gc.DeepEquals, charm.URL())
461 }
462
463+func (s *StateSuite) TestAddServiceEnvironmentDying(c *gc.C) {
464+ charm := s.AddTestingCharm(c, "dummy")
465+ _, err := s.State.AddService("s0", charm)
466+ c.Assert(err, gc.IsNil)
467+ // Check that services cannot be added if the environment is initially Dying.
468+ env, err := s.State.Environment()
469+ c.Assert(err, gc.IsNil)
470+ err = env.Destroy()
471+ c.Assert(err, gc.IsNil)
472+ _, err = s.State.AddService("s1", charm)
473+ c.Assert(err, gc.ErrorMatches, `cannot add service "s1": environment is no longer alive`)
474+}
475+
476+func (s *StateSuite) TestAddServiceEnvironmentDyingAfterInitial(c *gc.C) {
477+ charm := s.AddTestingCharm(c, "dummy")
478+ _, err := s.State.AddService("s0", charm)
479+ c.Assert(err, gc.IsNil)
480+ env, err := s.State.Environment()
481+ c.Assert(err, gc.IsNil)
482+ // Check that services cannot be added if the environment is initially
483+ // Alive but set to Dying immediately before the transaction is run.
484+ defer state.SetBeforeHooks(c, s.State, func() {
485+ c.Assert(env.Life(), gc.Equals, state.Alive)
486+ c.Assert(env.Destroy(), gc.IsNil)
487+ }).Check()
488+ _, err = s.State.AddService("s1", charm)
489+ c.Assert(err, gc.ErrorMatches, `cannot add service "s1": environment is no longer alive`)
490+}
491+
492 func (s *StateSuite) TestServiceNotFound(c *gc.C) {
493 _, err := s.State.Service("bummer")
494 c.Assert(err, gc.ErrorMatches, `service "bummer" not found`)
495@@ -1611,10 +1667,12 @@
496 c.Assert(err, gc.IsNil)
497 }
498
499-var findEntityTests = []struct {
500+type findEntityTest struct {
501 tag string
502 err string
503-}{{
504+}
505+
506+var findEntityTests = []findEntityTest{{
507 tag: "",
508 err: `"" is not a valid tag`,
509 }, {
510@@ -1651,8 +1709,8 @@
511 tag: "service-foo/bar",
512 err: `"service-foo/bar" is not a valid service tag`,
513 }, {
514- tag: "environment-foo",
515- err: `environment "foo" not found`,
516+ tag: "environment-9f484882-2f18-4fd2-967d-db9663db7bea",
517+ err: `environment "9f484882-2f18-4fd2-967d-db9663db7bea" not found`,
518 }, {
519 tag: "machine-1234",
520 err: `machine 1234 not found`,
521@@ -1673,7 +1731,13 @@
522 }, {
523 tag: "user-arble",
524 }, {
525+ // TODO(axw) 2013-12-04 #1257587
526+ // remove backwards compatibility for environment-tag; see state.go
527+ tag: "environment-notauuid",
528+ //err: `"environment-notauuid" is not a valid environment tag`,
529+}, {
530 tag: "environment-testenv",
531+ //err: `"environment-testenv" is not a valid environment tag`,
532 }}
533
534 var entityTypes = map[string]interface{}{
535@@ -1702,6 +1766,14 @@
536 c.Assert(err, gc.IsNil)
537 c.Assert(rel.String(), gc.Equals, "wordpress:db ser-vice2:server")
538
539+ // environment tag is dynamically generated
540+ env, err := s.State.Environment()
541+ c.Assert(err, gc.IsNil)
542+ findEntityTests = append([]findEntityTest{}, findEntityTests...)
543+ findEntityTests = append(findEntityTests, findEntityTest{
544+ tag: "environment-" + env.UUID(),
545+ })
546+
547 for i, test := range findEntityTests {
548 c.Logf("test %d: %q", i, test.tag)
549 e, err := s.State.FindEntity(test.tag)
550@@ -1712,7 +1784,14 @@
551 kind, err := names.TagKind(test.tag)
552 c.Assert(err, gc.IsNil)
553 c.Assert(e, gc.FitsTypeOf, entityTypes[kind])
554- c.Assert(e.Tag(), gc.Equals, test.tag)
555+ if kind == "environment" {
556+ // TODO(axw) 2013-12-04 #1257587
557+ // We *should* only be able to get the entity with its tag, but
558+ // for backwards-compatibility we accept any non-UUID tag.
559+ c.Assert(e.Tag(), gc.Equals, env.Tag())
560+ } else {
561+ c.Assert(e.Tag(), gc.Equals, test.tag)
562+ }
563 }
564 }
565 }
566@@ -1725,7 +1804,6 @@
567 "foo-",
568 "---",
569 "foo-bar",
570- "environment-foo",
571 "unit-foo",
572 }
573 for _, name := range bad {
574@@ -1767,6 +1845,14 @@
575 c.Assert(coll, gc.Equals, "users")
576 c.Assert(id, gc.Equals, user.Name())
577 c.Assert(err, gc.IsNil)
578+
579+ // Parse an environment entity name.
580+ env, err := s.State.Environment()
581+ c.Assert(err, gc.IsNil)
582+ coll, id, err = state.ParseTag(s.State, env.Tag())
583+ c.Assert(coll, gc.Equals, "environments")
584+ c.Assert(id, gc.Equals, env.UUID())
585+ c.Assert(err, gc.IsNil)
586 }
587
588 func (s *StateSuite) TestWatchCleanups(c *gc.C) {
589
590=== modified file 'state/watcher.go'
591--- state/watcher.go 2013-11-25 05:03:44 +0000
592+++ state/watcher.go 2013-12-11 09:15:40 +0000
593@@ -1070,6 +1070,11 @@
594 return newEntityWatcher(u.st, u.st.units, u.doc.Name)
595 }
596
597+// Watch returns a watcher for observing changes to an environment.
598+func (e *Environment) Watch() NotifyWatcher {
599+ return newEntityWatcher(e.st, e.st.environments, e.doc.UUID)
600+}
601+
602 // WatchForEnvironConfigChanges return a NotifyWatcher waiting for the Environ
603 // Config to change. This differs from WatchEnvironConfig in that the watcher
604 // is a NotifyWatcher that does not give content during Changes()

Subscribers

People subscribed via source and target branches

to status/vote changes: