github.com/mhilton/juju-juju@v0.0.0-20150901100907-a94dd2c73455/state/lease/schema.go (about) 1 // Copyright 2015 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package lease 5 6 import ( 7 "fmt" 8 "strings" 9 "time" 10 11 "github.com/juju/errors" 12 ) 13 14 // These constants define the field names and type values used by documents in 15 // a lease collection. They *must* remain in sync with the bson marshalling 16 // annotations in leaseDoc and clockDoc. 17 const ( 18 // fieldType and fieldNamespace identify the Type and Namespace fields in 19 // both leaseDoc and clockDoc. 20 fieldType = "type" 21 fieldNamespace = "namespace" 22 23 // typeLease and typeClock are the acceptable values for fieldType. 24 typeLease = "lease" 25 typeClock = "clock" 26 27 // fieldLease* identify the fields in a leaseDoc. 28 fieldLeaseName = "name" 29 fieldLeaseHolder = "holder" 30 fieldLeaseExpiry = "expiry" 31 fieldLeaseWriter = "writer" 32 33 // fieldClock* identify the fields in a clockDoc. 34 fieldClockWriters = "writers" 35 ) 36 37 // toInt64 converts a local time.Time into a database value that doesn't 38 // silently lose precision. 39 func toInt64(t time.Time) int64 { 40 return t.UnixNano() 41 } 42 43 // toTime converts a toInt64 result, as loaded from the db, back to a time.Time. 44 func toTime(v int64) time.Time { 45 return time.Unix(0, v) 46 } 47 48 // For simplicity's sake, we impose the same restrictions on all strings used 49 // with the lease package: they may not be empty, and none of the following 50 // characters are allowed. 51 // * '.' and '$' mean things to mongodb; we don't want to risk seeing them 52 // in key names. 53 // * '#' means something to the lease package and we don't want to risk 54 // confusing ourselves. 55 // * whitespace just seems like a bad idea. 56 const badCharacters = ".$# \t\r\n" 57 58 // validateString returns an error if the string is not valid. 59 func validateString(s string) error { 60 if s == "" { 61 return errors.New("string is empty") 62 } 63 if strings.ContainsAny(s, badCharacters) { 64 return errors.New("string contains forbidden characters") 65 } 66 return nil 67 } 68 69 // leaseDocId returns the _id for the document holding details of the supplied 70 // namespace and lease. 71 func leaseDocId(namespace, lease string) string { 72 return fmt.Sprintf("%s#%s#%s#", typeLease, namespace, lease) 73 } 74 75 // leaseDoc is used to serialise lease entries. 76 type leaseDoc struct { 77 // Id is always "<Type>#<Namespace>#<Name>#", and <Type> is always "lease", 78 // so that we can extract useful information from a stream of watcher events 79 // without incurring extra DB hits. 80 // Apart from checking validity on load, though, there's little reason 81 // to use Id elsewhere; Namespace and Name are the sources of truth. 82 Id string `bson:"_id"` 83 Type string `bson:"type"` 84 Namespace string `bson:"namespace"` 85 Name string `bson:"name"` 86 87 // EnvUUID exists because state.multiEnvRunner can't handle structs 88 // without `bson:"env-uuid"` fields. It's not necessary for the logic 89 // in this package, though. 90 EnvUUID string `bson:"env-uuid"` 91 92 // Holder, Expiry, and Writer map directly to entry. 93 Holder string `bson:"holder"` 94 Expiry int64 `bson:"expiry"` 95 Writer string `bson:"writer"` 96 } 97 98 // validate returns an error if any fields are invalid or inconsistent. 99 func (doc leaseDoc) validate() error { 100 if doc.Type != typeLease { 101 return errors.Errorf("invalid type %q", doc.Type) 102 } 103 // state.multiEnvRunner prepends environ ids in our documents, and 104 // state.envStateCollection does not strip them out. 105 if !strings.HasSuffix(doc.Id, leaseDocId(doc.Namespace, doc.Name)) { 106 return errors.Errorf("inconsistent _id") 107 } 108 if err := validateString(doc.Holder); err != nil { 109 return errors.Annotatef(err, "invalid holder") 110 } 111 if doc.Expiry == 0 { 112 return errors.Errorf("invalid expiry") 113 } 114 if err := validateString(doc.Writer); err != nil { 115 return errors.Annotatef(err, "invalid writer") 116 } 117 return nil 118 } 119 120 // entry returns the lease name and an entry corresponding to the document. If 121 // the document cannot be validated, it returns an error. 122 func (doc leaseDoc) entry() (string, entry, error) { 123 if err := doc.validate(); err != nil { 124 return "", entry{}, errors.Trace(err) 125 } 126 entry := entry{ 127 holder: doc.Holder, 128 expiry: toTime(doc.Expiry), 129 writer: doc.Writer, 130 } 131 return doc.Name, entry, nil 132 } 133 134 // newLeaseDoc returns a valid lease document encoding the supplied lease and 135 // entry in the supplied namespace, or an error. 136 func newLeaseDoc(namespace, name string, entry entry) (*leaseDoc, error) { 137 doc := &leaseDoc{ 138 Id: leaseDocId(namespace, name), 139 Type: typeLease, 140 Namespace: namespace, 141 Name: name, 142 Holder: entry.holder, 143 Expiry: toInt64(entry.expiry), 144 Writer: entry.writer, 145 } 146 if err := doc.validate(); err != nil { 147 return nil, errors.Trace(err) 148 } 149 return doc, nil 150 } 151 152 // clockDocId returns the _id for the document holding clock skew information 153 // for clients that have written in the supplied namespace. 154 func clockDocId(namespace string) string { 155 return fmt.Sprintf("%s#%s#", typeClock, namespace) 156 } 157 158 // clockDoc is used to synchronise clients. 159 type clockDoc struct { 160 // Id is always "<Type>#<Namespace>#", and <Type> is always "clock", for 161 // consistency with leaseDoc and ease of querying within the collection. 162 Id string `bson:"_id"` 163 Type string `bson:"type"` 164 Namespace string `bson:"namespace"` 165 166 // EnvUUID exists because state.multiEnvRunner can't handle structs 167 // without `bson:"env-uuid"` fields. It's not necessary for the logic 168 // in this package, though. 169 EnvUUID string `bson:"env-uuid"` 170 171 // Writers holds a the latest acknowledged time for every known client. 172 Writers map[string]int64 `bson:"writers"` 173 } 174 175 // validate returns an error if any fields are invalid or inconsistent. 176 func (doc clockDoc) validate() error { 177 if doc.Type != typeClock { 178 return errors.Errorf("invalid type %q", doc.Type) 179 } 180 // state.multiEnvRunner prepends environ ids in our documents, and 181 // state.envStateCollection does not strip them out. 182 if !strings.HasSuffix(doc.Id, clockDocId(doc.Namespace)) { 183 return errors.Errorf("inconsistent _id") 184 } 185 for writer, written := range doc.Writers { 186 if written == 0 { 187 return errors.Errorf("invalid time for writer %q", writer) 188 } 189 } 190 return nil 191 } 192 193 // skews returns clock skew information for all writers recorded in the 194 // document, given that the document was read between the supplied local 195 // times. It will return an error if the clock document is not valid, or 196 // if the times don't make sense. 197 func (doc clockDoc) skews(readAfter, readBefore time.Time) (map[string]Skew, error) { 198 if err := doc.validate(); err != nil { 199 return nil, errors.Trace(err) 200 } 201 if readBefore.Before(readAfter) { 202 return nil, errors.New("end of read window preceded beginning") 203 } 204 skews := make(map[string]Skew) 205 for writer, written := range doc.Writers { 206 skews[writer] = Skew{ 207 LastWrite: toTime(written), 208 ReadAfter: readAfter, 209 ReadBefore: readBefore, 210 } 211 } 212 return skews, nil 213 } 214 215 // newClockDoc returns an empty clockDoc for the supplied namespace. 216 func newClockDoc(namespace string) (*clockDoc, error) { 217 doc := &clockDoc{ 218 Id: clockDocId(namespace), 219 Type: typeClock, 220 Namespace: namespace, 221 Writers: make(map[string]int64), 222 } 223 if err := doc.validate(); err != nil { 224 return nil, errors.Trace(err) 225 } 226 return doc, nil 227 }