The following transaction is an exemplar of the principles outlined in docs/hacking-state.txt:
func (e *Environment) Destroy() (err error) {
defer errors.DeferredAnnotatef(&err, "failed to destroy environment")
// create a local variable so we can get a newer version in case we need to retry.
env := e
buildTxn := func (attempt int) ([]txn.Op, error) {
// On the first attempt, we assume memory state is recent
// enough to try using...
if attempt != 0 {
// ...but on subsequent attempts, we read fresh environ
// state from the DB. Note that we do *not* refresh `e`
// itself, as detailed in doc/hacking-state.txt
if env, err = env.st.Environment(); err != nil {
return nil, errors.Trace(err)
}
}
// It's nice to keep the actual destruction logic separate
// from the coordinating jiggery-pokery in this method.
ops, err := env.destroyOps()
if err == errEnvironNotAlive {
// Not a big deal in any way. This might be the second
// attempt, following up from a failure-that-actually-
// succeeded on the first; or it might be the first
// attempt, and we're observing that someone else
// already destroyed it; or any one of a bunch of other
// scenarios.
return nil, jujutxn.ErrNoOperations
} else if err != nil {
return nil, errors.Trace(err)
}
return ops, nil
}
return env.st.run(buildTxn)
}
// errEnvironNotAlive is a signal emitted from destroyOps to indicate
// that environment destruction is already underway. It should not
// escape this package.
var errEnvironNotAlive = errors.New("environment is no longer alive")
// destroyOps returns the txn operations necessary to begin environ
// destruction, or an error indicating why it can't.
func (e *Environment) destroyOps() ([]txn.Op, error) {
// Now this method can just always assume that e is up to date.
// Every check we make can be against memory state...
if e.Life() != Alive {
return nil, errEnvironNotAlive
}
uuid := e.UUID()
ops := []txn.Op{{
C: environmentsC,
Id: uuid,
// ...and corresponds to a txn Assert which ensures the
// checked state still appplies when the txn runs.
Assert: isEnvAliveDoc,
Update: bson.D{{"$set", bson.D{{"life", Dying}}}},
}}
// We can follow the same approach with the host/hosted bits:
if uuid == e.doc.ServerUUID {
// So if we're a host, we can read the db into memory to
// check for hosted envs...
if count, err := hostedEnvironCount(e.st); err != nil {
return nil, errors.Trace(err)
} else if count != 0 {
// ...and return an error in this one place...
return nil, errors.New("hosting %d other environments", count)
}
// ...or add an assertion to make sure the count is
// still 0.
ops = append(ops, assertNoHostedEnvironsOp())
} else {
// When we're destroying a hosted environment, no further
// checks are necessary -- we just need to make sure we
// update the refcount.
ops = append(ops, decHostedEnvironCountOp())
}
// Because txn operations execute in order, and may encounter
// arbitrarily long delays, we need to make sure every op
// causes a state change that's still consistent; so we make
// sure the cleanup op is the last thing that will execute.
cleanupOp := e.st.newCleanupOp(cleanupServicesForDyingEnvironment, uuid)
ops = append(ops, cleanupOp)
return ops, nil
}