1
// Copyright 2012, 2013 Canonical Ltd.
2
// Licensed under the AGPLv3, see LICENCE file for details.
13
"github.com/juju/errors"
14
"github.com/juju/names"
15
jujutxn "github.com/juju/txn"
16
"gopkg.in/juju/charm.v5"
18
"gopkg.in/mgo.v2/bson"
22
// relationKey returns a string describing the relation defined by
23
// endpoints, for use in various contexts (including error messages).
24
func relationKey(endpoints []Endpoint) string {
26
for _, ep := range endpoints {
31
for _, ep := range eps {
32
names = append(names, ep.String())
34
return strings.Join(names, " ")
37
// relationDoc is the internal representation of a Relation in MongoDB.
38
// Note the correspondence with RelationInfo in apiserver/params.
39
type relationDoc struct {
40
DocID string `bson:"_id"`
41
Key string `bson:"key"`
42
EnvUUID string `bson:"env-uuid"`
49
// Relation represents a relation between one or two service endpoints.
50
type Relation struct {
55
func newRelation(st *State, doc *relationDoc) *Relation {
62
func (r *Relation) String() string {
66
// Tag returns a name identifying the relation.
67
func (r *Relation) Tag() names.Tag {
68
return names.NewRelationTag(r.doc.Key)
71
// Refresh refreshes the contents of the relation from the underlying
72
// state. It returns an error that satisfies errors.IsNotFound if the
73
// relation has been removed.
74
func (r *Relation) Refresh() error {
75
relations, closer := r.st.getCollection(relationsC)
79
err := relations.FindId(r.doc.DocID).One(&doc)
80
if err == mgo.ErrNotFound {
81
return errors.NotFoundf("relation %v", r)
84
return fmt.Errorf("cannot refresh relation %v: %v", r, err)
86
if r.doc.Id != doc.Id {
87
// The relation has been destroyed and recreated. This is *not* the
88
// same relation; if we pretend it is, we run the risk of violating
89
// the lifecycle-only-advances guarantee.
90
return errors.NotFoundf("relation %v", r)
96
// Life returns the relation's current life state.
97
func (r *Relation) Life() Life {
101
// Destroy ensures that the relation will be removed at some point; if no units
102
// are currently in scope, it will be removed immediately.
103
func (r *Relation) Destroy() (err error) {
104
defer errors.DeferredAnnotatef(&err, "cannot destroy relation %q", r)
105
if len(r.doc.Endpoints) == 1 && r.doc.Endpoints[0].Role == charm.RolePeer {
106
return fmt.Errorf("is a peer relation")
110
// This is a white lie; the document might actually be removed.
114
rel := &Relation{r.st, r.doc}
115
// In this context, aborted transactions indicate that the number of units
116
// in scope have changed between 0 and not-0. The chances of 5 successive
117
// attempts each hitting this change -- which is itself an unlikely one --
118
// are considered to be extremely small.
119
buildTxn := func(attempt int) ([]txn.Op, error) {
121
if err := rel.Refresh(); errors.IsNotFound(err) {
122
return []txn.Op{}, nil
123
} else if err != nil {
127
ops, _, err := rel.destroyOps("")
128
if err == errAlreadyDying {
129
return nil, jujutxn.ErrNoOperations
130
} else if err != nil {
135
return rel.st.run(buildTxn)
138
var errAlreadyDying = stderrors.New("entity is already dying and cannot be destroyed")
140
// destroyOps returns the operations necessary to destroy the relation, and
141
// whether those operations will lead to the relation's removal. These
142
// operations may include changes to the relation's services; however, if
143
// ignoreService is not empty, no operations modifying that service will
145
func (r *Relation) destroyOps(ignoreService string) (ops []txn.Op, isRemove bool, err error) {
146
if r.doc.Life != Alive {
147
return nil, false, errAlreadyDying
149
if r.doc.UnitCount == 0 {
150
removeOps, err := r.removeOps(ignoreService, nil)
152
return nil, false, err
154
return removeOps, true, nil
159
Assert: bson.D{{"life", Alive}, {"unitcount", bson.D{{"$gt", 0}}}},
160
Update: bson.D{{"$set", bson.D{{"life", Dying}}}},
164
// removeOps returns the operations necessary to remove the relation. If
165
// ignoreService is not empty, no operations affecting that service will be
166
// included; if departingUnit is not nil, this implies that the relation's
167
// services may be Dying and otherwise unreferenced, and may thus require
168
// removal themselves.
169
func (r *Relation) removeOps(ignoreService string, departingUnit *Unit) ([]txn.Op, error) {
175
if departingUnit != nil {
176
relOp.Assert = bson.D{{"life", Dying}, {"unitcount", 1}}
178
relOp.Assert = bson.D{{"life", Alive}, {"unitcount", 0}}
180
ops := []txn.Op{relOp}
181
for _, ep := range r.doc.Endpoints {
182
if ep.ServiceName == ignoreService {
186
hasRelation := bson.D{{"relationcount", bson.D{{"$gt", 0}}}}
187
if departingUnit == nil {
188
// We're constructing a destroy operation, either of the relation
189
// or one of its services, and can therefore be assured that both
190
// services are Alive.
191
asserts = append(hasRelation, isAliveDoc...)
192
} else if ep.ServiceName == departingUnit.ServiceName() {
193
// This service must have at least one unit -- the one that's
194
// departing the relation -- so it cannot be ready for removal.
195
cannotDieYet := bson.D{{"unitcount", bson.D{{"$gt", 0}}}}
196
asserts = append(hasRelation, cannotDieYet...)
198
// This service may require immediate removal.
199
services, closer := r.st.getCollection(servicesC)
202
svc := &Service{st: r.st}
203
hasLastRef := bson.D{{"life", Dying}, {"unitcount", 0}, {"relationcount", 1}}
204
removable := append(bson.D{{"_id", ep.ServiceName}}, hasLastRef...)
205
if err := services.Find(removable).One(&svc.doc); err == nil {
206
ops = append(ops, svc.removeOps(hasLastRef)...)
208
} else if err != mgo.ErrNotFound {
211
// If not, we must check that this is still the case when the
212
// transaction is applied.
213
asserts = bson.D{{"$or", []bson.D{
215
{{"unitcount", bson.D{{"$gt", 0}}}},
216
{{"relationcount", bson.D{{"$gt", 1}}}},
219
ops = append(ops, txn.Op{
221
Id: r.st.docID(ep.ServiceName),
223
Update: bson.D{{"$inc", bson.D{{"relationcount", -1}}}},
226
cleanupOp := r.st.newCleanupOp(cleanupRelationSettings, fmt.Sprintf("r#%d#", r.Id()))
227
return append(ops, cleanupOp), nil
230
// Id returns the integer internal relation key. This is exposed
231
// because the unit agent needs to expose a value derived from this
232
// (as JUJU_RELATION_ID) to allow relation hooks to differentiate
233
// between relations with different services.
234
func (r *Relation) Id() int {
238
// Endpoint returns the endpoint of the relation for the named service.
239
// If the service is not part of the relation, an error will be returned.
240
func (r *Relation) Endpoint(serviceName string) (Endpoint, error) {
241
for _, ep := range r.doc.Endpoints {
242
if ep.ServiceName == serviceName {
246
return Endpoint{}, fmt.Errorf("service %q is not a member of %q", serviceName, r)
249
// Endpoints returns the endpoints for the relation.
250
func (r *Relation) Endpoints() []Endpoint {
251
return r.doc.Endpoints
254
// RelatedEndpoints returns the endpoints of the relation r with which
255
// units of the named service will establish relations. If the service
256
// is not part of the relation r, an error will be returned.
257
func (r *Relation) RelatedEndpoints(serviceName string) ([]Endpoint, error) {
258
local, err := r.Endpoint(serviceName)
262
role := counterpartRole(local.Role)
264
for _, ep := range r.doc.Endpoints {
266
eps = append(eps, ep)
270
return nil, fmt.Errorf("no endpoints of %q relate to service %q", r, serviceName)
275
// Unit returns a RelationUnit for the supplied unit.
276
func (r *Relation) Unit(u *Unit) (*RelationUnit, error) {
277
ep, err := r.Endpoint(u.doc.Service)
281
scope := []string{"r", strconv.Itoa(r.doc.Id)}
282
if ep.Scope == charm.ScopeContainer {
283
container := u.doc.Principal
285
container = u.doc.Name
287
scope = append(scope, container)
289
return &RelationUnit{
294
scope: strings.Join(scope, "#"),