github.com/Pankov404/juju@v0.0.0-20150703034450-be266991dceb/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"` // TODO(fwereade) add index 84 Namespace string `bson:"namespace"` // TODO(fwereade) add index 85 Name string `bson:"name"` 86 87 // Holder, Expiry, and Writer map directly to entry. 88 Holder string `bson:"holder"` 89 Expiry int64 `bson:"expiry"` 90 Writer string `bson:"writer"` 91 } 92 93 // validate returns an error if any fields are invalid or inconsistent. 94 func (doc leaseDoc) validate() error { 95 if doc.Type != typeLease { 96 return errors.Errorf("invalid type %q", doc.Type) 97 } 98 if doc.Id != leaseDocId(doc.Namespace, doc.Name) { 99 return errors.Errorf("inconsistent _id") 100 } 101 if err := validateString(doc.Holder); err != nil { 102 return errors.Annotatef(err, "invalid holder") 103 } 104 if doc.Expiry == 0 { 105 return errors.Errorf("invalid expiry") 106 } 107 if err := validateString(doc.Writer); err != nil { 108 return errors.Annotatef(err, "invalid writer") 109 } 110 return nil 111 } 112 113 // entry returns the lease name and an entry corresponding to the document. If 114 // the document cannot be validated, it returns an error. 115 func (doc leaseDoc) entry() (string, entry, error) { 116 if err := doc.validate(); err != nil { 117 return "", entry{}, errors.Trace(err) 118 } 119 entry := entry{ 120 holder: doc.Holder, 121 expiry: toTime(doc.Expiry), 122 writer: doc.Writer, 123 } 124 return doc.Name, entry, nil 125 } 126 127 // newLeaseDoc returns a valid lease document encoding the supplied lease and 128 // entry in the supplied namespace, or an error. 129 func newLeaseDoc(namespace, name string, entry entry) (*leaseDoc, error) { 130 doc := &leaseDoc{ 131 Id: leaseDocId(namespace, name), 132 Type: typeLease, 133 Namespace: namespace, 134 Name: name, 135 Holder: entry.holder, 136 Expiry: toInt64(entry.expiry), 137 Writer: entry.writer, 138 } 139 if err := doc.validate(); err != nil { 140 return nil, errors.Trace(err) 141 } 142 return doc, nil 143 } 144 145 // clockDocId returns the _id for the document holding clock skew information 146 // for clients that have written in the supplied namespace. 147 func clockDocId(namespace string) string { 148 return fmt.Sprintf("%s#%s#", typeClock, namespace) 149 } 150 151 // clockDoc is used to synchronise clients. 152 type clockDoc struct { 153 // Id is always "<Type>#<Namespace>#", and <Type> is always "clock", for 154 // consistency with leaseDoc and ease of querying within the collection. 155 Id string `bson:"_id"` 156 Type string `bson:"type"` 157 Namespace string `bson:"namespace"` 158 159 // Writers holds a the latest acknowledged time for every known client. 160 Writers map[string]int64 `bson:"writers"` 161 } 162 163 // validate returns an error if any fields are invalid or inconsistent. 164 func (doc clockDoc) validate() error { 165 if doc.Type != typeClock { 166 return errors.Errorf("invalid type %q", doc.Type) 167 } 168 if doc.Id != clockDocId(doc.Namespace) { 169 return errors.Errorf("inconsistent _id") 170 } 171 for writer, written := range doc.Writers { 172 if written == 0 { 173 return errors.Errorf("invalid time for writer %q", writer) 174 } 175 } 176 return nil 177 } 178 179 // skews returns clock skew information for all writers recorded in the 180 // document, given that the document was read between the supplied local 181 // times. It will return an error if the clock document is not valid, or 182 // if the times don't make sense. 183 func (doc clockDoc) skews(readAfter, readBefore time.Time) (map[string]Skew, error) { 184 if err := doc.validate(); err != nil { 185 return nil, errors.Trace(err) 186 } 187 if readBefore.Before(readAfter) { 188 return nil, errors.New("end of read window preceded beginning") 189 } 190 skews := make(map[string]Skew) 191 for writer, written := range doc.Writers { 192 skews[writer] = Skew{ 193 LastWrite: toTime(written), 194 ReadAfter: readAfter, 195 ReadBefore: readBefore, 196 } 197 } 198 return skews, nil 199 } 200 201 // newClockDoc returns an empty clockDoc for the supplied namespace. 202 func newClockDoc(namespace string) (clockDoc, error) { 203 doc := clockDoc{ 204 Id: clockDocId(namespace), 205 Type: typeClock, 206 Namespace: namespace, 207 Writers: make(map[string]int64), 208 } 209 if err := doc.validate(); err != nil { 210 return clockDoc{}, errors.Trace(err) 211 } 212 return doc, nil 213 }