github.com/makyo/juju@v0.0.0-20160425123129-2608902037e9/state/status.go (about) 1 // Copyright 2014 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package state 5 6 import ( 7 "time" 8 9 "github.com/juju/errors" 10 jujutxn "github.com/juju/txn" 11 "gopkg.in/mgo.v2" 12 "gopkg.in/mgo.v2/bson" 13 "gopkg.in/mgo.v2/txn" 14 15 "github.com/juju/juju/core/leadership" 16 "github.com/juju/juju/mongo" 17 "github.com/juju/juju/status" 18 ) 19 20 // statusDoc represents a entity status in Mongodb. The implicit 21 // _id field is explicitly set to the global key of the associated 22 // entity in the document's creation transaction, but omitted to allow 23 // direct use of the document in both create and update transactions. 24 type statusDoc struct { 25 ModelUUID string `bson:"model-uuid"` 26 Status status.Status `bson:"status"` 27 StatusInfo string `bson:"statusinfo"` 28 StatusData map[string]interface{} `bson:"statusdata"` 29 30 // Updated used to be a *time.Time that was not present on statuses dating 31 // from older versions of juju so this might be 0 for those cases. 32 Updated int64 `bson:"updated"` 33 34 // TODO(fwereade/wallyworld): lp:1479278 35 // NeverSet is a short-term hack to work around a misfeature in service 36 // status. To maintain current behaviour, we create service status docs 37 // (and only service status documents) with NeverSet true; and then, when 38 // reading them, if NeverSet is still true, we aggregate status from the 39 // units instead. 40 NeverSet bool `bson:"neverset"` 41 } 42 43 func unixNanoToTime(i int64) *time.Time { 44 t := time.Unix(0, i) 45 return &t 46 } 47 48 // mapKeys returns a copy of the supplied map, with all nested map[string]interface{} 49 // keys transformed by f. All other types are ignored. 50 func mapKeys(f func(string) string, input map[string]interface{}) map[string]interface{} { 51 result := make(map[string]interface{}) 52 for key, value := range input { 53 if submap, ok := value.(map[string]interface{}); ok { 54 value = mapKeys(f, submap) 55 } 56 result[f(key)] = value 57 } 58 return result 59 } 60 61 // escapeKeys is used to escape bad keys in StatusData. A statusDoc without 62 // escaped keys is broken. 63 func escapeKeys(input map[string]interface{}) map[string]interface{} { 64 return mapKeys(escapeReplacer.Replace, input) 65 } 66 67 // unescapeKeys is used to restore escaped keys from StatusData to their 68 // original values. 69 func unescapeKeys(input map[string]interface{}) map[string]interface{} { 70 return mapKeys(unescapeReplacer.Replace, input) 71 } 72 73 // getStatus retrieves the status document associated with the given 74 // globalKey and converts it to a StatusInfo. If the status document 75 // is not found, a NotFoundError referencing badge will be returned. 76 func getStatus(st *State, globalKey, badge string) (_ status.StatusInfo, err error) { 77 defer errors.DeferredAnnotatef(&err, "cannot get status") 78 statuses, closer := st.getCollection(statusesC) 79 defer closer() 80 81 var doc statusDoc 82 err = statuses.FindId(globalKey).One(&doc) 83 if err == mgo.ErrNotFound { 84 return status.StatusInfo{}, errors.NotFoundf(badge) 85 } else if err != nil { 86 return status.StatusInfo{}, errors.Trace(err) 87 } 88 89 return status.StatusInfo{ 90 Status: doc.Status, 91 Message: doc.StatusInfo, 92 Data: unescapeKeys(doc.StatusData), 93 Since: unixNanoToTime(doc.Updated), 94 }, nil 95 } 96 97 // setStatusParams configures a setStatus call. All parameters are presumed to 98 // be set to valid values unless otherwise noted. 99 type setStatusParams struct { 100 101 // badge is used to specialize any NotFound error emitted. 102 badge string 103 104 // globalKey uniquely identifies the entity to which the 105 globalKey string 106 107 // status is the status value. 108 status status.Status 109 110 // message is an optional string elaborating upon the status. 111 message string 112 113 // rawData is a map of arbitrary data elaborating upon the status and 114 // message. Its keys are assumed not to have been escaped. 115 rawData map[string]interface{} 116 117 // token, if present, must accept an *[]txn.Op passed to its Check method, 118 // and will prevent any change if it becomes invalid. 119 token leadership.Token 120 } 121 122 // setStatus inteprets the supplied params as documented on the type. 123 func setStatus(st *State, params setStatusParams) (err error) { 124 defer errors.DeferredAnnotatef(&err, "cannot set status") 125 126 // TODO(fwereade): this can/should probably be recording the time the 127 // status was *set*, not the time it happened to arrive in state. 128 // We should almost certainly be accepting StatusInfo in the exposed 129 // SetStatus methods, for symetry with the Status methods. 130 // TODO(fwereade): 2016-03-17 lp:1558657 131 now := time.Now().UnixNano() 132 doc := statusDoc{ 133 Status: params.status, 134 StatusInfo: params.message, 135 StatusData: escapeKeys(params.rawData), 136 Updated: now, 137 } 138 probablyUpdateStatusHistory(st, params.globalKey, doc) 139 140 // Set the authoritative status document, or fail trying. 141 buildTxn := updateStatusSource(st, params.globalKey, doc) 142 if params.token != nil { 143 buildTxn = buildTxnWithLeadership(buildTxn, params.token) 144 } 145 err = st.run(buildTxn) 146 if cause := errors.Cause(err); cause == mgo.ErrNotFound { 147 return errors.NotFoundf(params.badge) 148 } 149 return errors.Trace(err) 150 } 151 152 // updateStatusSource returns a transaction source that builds the operations 153 // necessary to set the supplied status (and to fail safely if leaked and 154 // executed late, so as not to overwrite more recent documents). 155 func updateStatusSource(st *State, globalKey string, doc statusDoc) jujutxn.TransactionSource { 156 update := bson.D{{"$set", &doc}} 157 return func(_ int) ([]txn.Op, error) { 158 txnRevno, err := st.readTxnRevno(statusesC, globalKey) 159 if err != nil { 160 return nil, errors.Trace(err) 161 } 162 assert := bson.D{{"txn-revno", txnRevno}} 163 return []txn.Op{{ 164 C: statusesC, 165 Id: globalKey, 166 Assert: assert, 167 Update: update, 168 }}, nil 169 } 170 } 171 172 // createStatusOp returns the operation needed to create the given status 173 // document associated with the given globalKey. 174 func createStatusOp(st *State, globalKey string, doc statusDoc) txn.Op { 175 return txn.Op{ 176 C: statusesC, 177 Id: st.docID(globalKey), 178 Assert: txn.DocMissing, 179 Insert: &doc, 180 } 181 } 182 183 // removeStatusOp returns the operation needed to remove the status 184 // document associated with the given globalKey. 185 func removeStatusOp(st *State, globalKey string) txn.Op { 186 return txn.Op{ 187 C: statusesC, 188 Id: st.docID(globalKey), 189 Remove: true, 190 } 191 } 192 193 type historicalStatusDoc struct { 194 ModelUUID string `bson:"model-uuid"` 195 GlobalKey string `bson:"globalkey"` 196 Status status.Status `bson:"status"` 197 StatusInfo string `bson:"statusinfo"` 198 StatusData map[string]interface{} `bson:"statusdata"` 199 200 // Updated might not be present on statuses copied by old versions of juju 201 // from yet older versions of juju. Do not dereference without checking. 202 // Updated *time.Time `bson:"updated"` 203 Updated int64 `bson:"updated"` 204 } 205 206 func probablyUpdateStatusHistory(st *State, globalKey string, doc statusDoc) { 207 historyDoc := &historicalStatusDoc{ 208 Status: doc.Status, 209 StatusInfo: doc.StatusInfo, 210 StatusData: doc.StatusData, // coming from a statusDoc, already escaped 211 Updated: doc.Updated, 212 GlobalKey: globalKey, 213 } 214 history, closer := st.getCollection(statusesHistoryC) 215 defer closer() 216 historyW := history.Writeable() 217 if err := historyW.Insert(historyDoc); err != nil { 218 logger.Errorf("failed to write status history: %v", err) 219 } 220 } 221 222 func statusHistory(st *State, globalKey string, size int) ([]status.StatusInfo, error) { 223 statusHistory, closer := st.getCollection(statusesHistoryC) 224 defer closer() 225 226 var docs []historicalStatusDoc 227 query := statusHistory.Find(bson.D{{"globalkey", globalKey}}) 228 err := query.Sort("-updated").Limit(size).All(&docs) 229 if err == mgo.ErrNotFound { 230 return []status.StatusInfo{}, errors.NotFoundf("status history") 231 } else if err != nil { 232 return []status.StatusInfo{}, errors.Annotatef(err, "cannot get status history") 233 } 234 235 results := make([]status.StatusInfo, len(docs)) 236 for i, doc := range docs { 237 results[i] = status.StatusInfo{ 238 Status: doc.Status, 239 Message: doc.StatusInfo, 240 Data: unescapeKeys(doc.StatusData), 241 Since: unixNanoToTime(doc.Updated), 242 } 243 } 244 return results, nil 245 } 246 247 // PruneStatusHistory removes status history entries until 248 // only the maxLogsPerEntity newest records per unit remain. 249 func PruneStatusHistory(st *State, maxLogsPerEntity int) error { 250 history, closer := st.getCollection(statusesHistoryC) 251 defer closer() 252 253 historyW := history.Writeable() 254 255 // TODO(fwereade): This is a very strange implementation. 256 // 257 // It goes to a lot of effort to keep a *different* span of history for 258 // each entity -- which effectively hides interaction history older than 259 // the recorded span of the most-frequently-updated object. 260 // 261 // It would be much less surprising -- and much more efficient -- to keep 262 // either a fixed total number of records, or a fixed total span of history, 263 // -- but either way we should be deleting only the oldest records at any 264 // given time. 265 globalKeys, err := getEntitiesWithStatuses(historyW) 266 if err != nil { 267 return errors.Trace(err) 268 } 269 for _, globalKey := range globalKeys { 270 keepUpTo, ok, err := getOldestTimeToKeep(historyW, globalKey, maxLogsPerEntity) 271 if err != nil { 272 return errors.Trace(err) 273 } 274 if !ok { 275 continue 276 } 277 _, err = historyW.RemoveAll(bson.D{ 278 {"globalkey", globalKey}, 279 {"updated", bson.M{"$lt": keepUpTo}}, 280 }) 281 if err != nil { 282 return errors.Trace(err) 283 } 284 } 285 return nil 286 } 287 288 // getOldestTimeToKeep returns the create time for the oldest 289 // status log to be kept. 290 func getOldestTimeToKeep(coll mongo.Collection, globalKey string, size int) (int64, bool, error) { 291 result := historicalStatusDoc{} 292 err := coll.Find(bson.D{{"globalkey", globalKey}}).Sort("-updated").Skip(size - 1).One(&result) 293 if err == mgo.ErrNotFound { 294 return -1, false, nil 295 } 296 if err != nil { 297 return -1, false, errors.Trace(err) 298 } 299 return result.Updated, true, nil 300 301 } 302 303 // getEntitiesWithStatuses returns the ids for all entities that 304 // have history entries 305 func getEntitiesWithStatuses(coll mongo.Collection) ([]string, error) { 306 var globalKeys []string 307 err := coll.Find(nil).Distinct("globalkey", &globalKeys) 308 if err != nil { 309 return nil, errors.Trace(err) 310 } 311 return globalKeys, nil 312 }