github.com/axw/juju@v0.0.0-20161005053422-4bd6544d08d4/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/mongo/utils" 18 "github.com/juju/juju/status" 19 ) 20 21 // statusDoc represents a entity status in Mongodb. The implicit 22 // _id field is explicitly set to the global key of the associated 23 // entity in the document's creation transaction, but omitted to allow 24 // direct use of the document in both create and update transactions. 25 type statusDoc struct { 26 ModelUUID string `bson:"model-uuid"` 27 Status status.Status `bson:"status"` 28 StatusInfo string `bson:"statusinfo"` 29 StatusData map[string]interface{} `bson:"statusdata"` 30 31 // Updated used to be a *time.Time that was not present on statuses dating 32 // from older versions of juju so this might be 0 for those cases. 33 Updated int64 `bson:"updated"` 34 35 // TODO(fwereade/wallyworld): lp:1479278 36 // NeverSet is a short-term hack to work around a misfeature in service 37 // status. To maintain current behaviour, we create service status docs 38 // (and only service status documents) with NeverSet true; and then, when 39 // reading them, if NeverSet is still true, we aggregate status from the 40 // units instead. 41 NeverSet bool `bson:"neverset"` 42 } 43 44 func unixNanoToTime(i int64) *time.Time { 45 t := time.Unix(0, i) 46 return &t 47 } 48 49 // getStatus retrieves the status document associated with the given 50 // globalKey and converts it to a StatusInfo. If the status document 51 // is not found, a NotFoundError referencing badge will be returned. 52 func getStatus(st *State, globalKey, badge string) (_ status.StatusInfo, err error) { 53 defer errors.DeferredAnnotatef(&err, "cannot get status") 54 statuses, closer := st.getCollection(statusesC) 55 defer closer() 56 57 var doc statusDoc 58 err = statuses.FindId(globalKey).One(&doc) 59 if err == mgo.ErrNotFound { 60 return status.StatusInfo{}, errors.NotFoundf(badge) 61 } else if err != nil { 62 return status.StatusInfo{}, errors.Trace(err) 63 } 64 65 return status.StatusInfo{ 66 Status: doc.Status, 67 Message: doc.StatusInfo, 68 Data: utils.UnescapeKeys(doc.StatusData), 69 Since: unixNanoToTime(doc.Updated), 70 }, nil 71 } 72 73 // setStatusParams configures a setStatus call. All parameters are presumed to 74 // be set to valid values unless otherwise noted. 75 type setStatusParams struct { 76 77 // badge is used to specialize any NotFound error emitted. 78 badge string 79 80 // globalKey uniquely identifies the entity to which the 81 globalKey string 82 83 // status is the status value. 84 status status.Status 85 86 // message is an optional string elaborating upon the status. 87 message string 88 89 // rawData is a map of arbitrary data elaborating upon the status and 90 // message. Its keys are assumed not to have been escaped. 91 rawData map[string]interface{} 92 93 // token, if present, must accept an *[]txn.Op passed to its Check method, 94 // and will prevent any change if it becomes invalid. 95 token leadership.Token 96 97 // udpated, the time the status was set. 98 updated *time.Time 99 } 100 101 // setStatus inteprets the supplied params as documented on the type. 102 func setStatus(st *State, params setStatusParams) (err error) { 103 defer errors.DeferredAnnotatef(&err, "cannot set status") 104 105 doc := statusDoc{ 106 Status: params.status, 107 StatusInfo: params.message, 108 StatusData: utils.EscapeKeys(params.rawData), 109 Updated: params.updated.UnixNano(), 110 } 111 probablyUpdateStatusHistory(st, params.globalKey, doc) 112 113 // Set the authoritative status document, or fail trying. 114 buildTxn := updateStatusSource(st, params.globalKey, doc) 115 if params.token != nil { 116 buildTxn = buildTxnWithLeadership(buildTxn, params.token) 117 } 118 err = st.run(buildTxn) 119 if cause := errors.Cause(err); cause == mgo.ErrNotFound { 120 return errors.NotFoundf(params.badge) 121 } 122 return errors.Trace(err) 123 } 124 125 // updateStatusSource returns a transaction source that builds the operations 126 // necessary to set the supplied status (and to fail safely if leaked and 127 // executed late, so as not to overwrite more recent documents). 128 func updateStatusSource(st *State, globalKey string, doc statusDoc) jujutxn.TransactionSource { 129 update := bson.D{{"$set", &doc}} 130 return func(_ int) ([]txn.Op, error) { 131 txnRevno, err := st.readTxnRevno(statusesC, globalKey) 132 if err != nil { 133 return nil, errors.Trace(err) 134 } 135 assert := bson.D{{"txn-revno", txnRevno}} 136 return []txn.Op{{ 137 C: statusesC, 138 Id: globalKey, 139 Assert: assert, 140 Update: update, 141 }}, nil 142 } 143 } 144 145 // createStatusOp returns the operation needed to create the given status 146 // document associated with the given globalKey. 147 func createStatusOp(st *State, globalKey string, doc statusDoc) txn.Op { 148 return txn.Op{ 149 C: statusesC, 150 Id: st.docID(globalKey), 151 Assert: txn.DocMissing, 152 Insert: &doc, 153 } 154 } 155 156 // removeStatusOp returns the operation needed to remove the status 157 // document associated with the given globalKey. 158 func removeStatusOp(st *State, globalKey string) txn.Op { 159 return txn.Op{ 160 C: statusesC, 161 Id: st.docID(globalKey), 162 Remove: true, 163 } 164 } 165 166 type historicalStatusDoc struct { 167 ModelUUID string `bson:"model-uuid"` 168 GlobalKey string `bson:"globalkey"` 169 Status status.Status `bson:"status"` 170 StatusInfo string `bson:"statusinfo"` 171 StatusData map[string]interface{} `bson:"statusdata"` 172 173 // Updated might not be present on statuses copied by old versions of juju 174 // from yet older versions of juju. Do not dereference without checking. 175 // Updated *time.Time `bson:"updated"` 176 Updated int64 `bson:"updated"` 177 } 178 179 func probablyUpdateStatusHistory(st *State, globalKey string, doc statusDoc) { 180 historyDoc := &historicalStatusDoc{ 181 Status: doc.Status, 182 StatusInfo: doc.StatusInfo, 183 StatusData: doc.StatusData, // coming from a statusDoc, already escaped 184 Updated: doc.Updated, 185 GlobalKey: globalKey, 186 } 187 history, closer := st.getCollection(statusesHistoryC) 188 defer closer() 189 historyW := history.Writeable() 190 if err := historyW.Insert(historyDoc); err != nil { 191 logger.Errorf("failed to write status history: %v", err) 192 } 193 } 194 195 // statusHistoryArgs hold the arguments to call statusHistory. 196 type statusHistoryArgs struct { 197 st *State 198 globalKey string 199 filter status.StatusHistoryFilter 200 } 201 202 func statusHistory(args *statusHistoryArgs) ([]status.StatusInfo, error) { 203 filter := args.filter 204 if err := args.filter.Validate(); err != nil { 205 return nil, errors.Annotate(err, "validating arguments") 206 } 207 statusHistory, closer := args.st.getCollection(statusesHistoryC) 208 defer closer() 209 210 var ( 211 docs []historicalStatusDoc 212 query mongo.Query 213 ) 214 baseQuery := bson.M{"globalkey": args.globalKey} 215 if filter.Delta != nil { 216 delta := *filter.Delta 217 // TODO(perrito666) 2016-05-02 lp:1558657 218 updated := time.Now().Add(-delta) 219 baseQuery = bson.M{"updated": bson.M{"$gt": updated.UnixNano()}, "globalkey": args.globalKey} 220 } 221 if filter.Date != nil { 222 baseQuery = bson.M{"updated": bson.M{"$gt": filter.Date.UnixNano()}, "globalkey": args.globalKey} 223 } 224 query = statusHistory.Find(baseQuery).Sort("-updated") 225 if filter.Size > 0 { 226 query = query.Limit(filter.Size) 227 } 228 err := query.All(&docs) 229 230 if err == mgo.ErrNotFound { 231 return []status.StatusInfo{}, errors.NotFoundf("status history") 232 } else if err != nil { 233 return []status.StatusInfo{}, errors.Annotatef(err, "cannot get status history") 234 } 235 236 results := make([]status.StatusInfo, len(docs)) 237 for i, doc := range docs { 238 results[i] = status.StatusInfo{ 239 Status: doc.Status, 240 Message: doc.StatusInfo, 241 Data: utils.UnescapeKeys(doc.StatusData), 242 Since: unixNanoToTime(doc.Updated), 243 } 244 } 245 return results, nil 246 } 247 248 // PruneStatusHistory removes status history entries until 249 // only logs newer than <maxLogTime> remain and also ensures 250 // that the collection is smaller than <maxLogsMB> after the 251 // deletion. 252 func PruneStatusHistory(st *State, maxHistoryTime time.Duration, maxHistoryMB int) error { 253 if maxHistoryMB < 0 { 254 return errors.NotValidf("non-positive maxHistoryMB") 255 } 256 if maxHistoryTime < 0 { 257 return errors.NotValidf("non-positive maxHistoryTime") 258 } 259 if maxHistoryMB == 0 && maxHistoryTime == 0 { 260 return errors.NotValidf("backlog size and time constraints are both 0") 261 } 262 history, closer := st.getRawCollection(statusesHistoryC) 263 defer closer() 264 265 // Status Record Age 266 if maxHistoryTime > 0 { 267 t := st.clock.Now().Add(-maxHistoryTime) 268 _, err := history.RemoveAll(bson.D{ 269 {"updated", bson.M{"$lt": t.UnixNano()}}, 270 }) 271 if err != nil { 272 return errors.Trace(err) 273 } 274 } 275 if maxHistoryMB == 0 { 276 return nil 277 } 278 // Collection Size 279 collMB, err := getCollectionMB(history) 280 if err != nil { 281 return errors.Annotate(err, "retrieving status history collection size") 282 } 283 if collMB <= maxHistoryMB { 284 return nil 285 } 286 // TODO(perrito666) explore if there would be any beneffit from having the 287 // size limit be per model 288 count, err := history.Count() 289 if err == mgo.ErrNotFound || count <= 0 { 290 return nil 291 } 292 if err != nil { 293 return errors.Annotate(err, "counting status history records") 294 } 295 // We are making the assumption that status sizes can be averaged for 296 // large numbers and we will get a reasonable approach on the size. 297 // Note: Capped collections are not used for this because they, currently 298 // at least, lack a way to be resized and the size is expected to change 299 // as real life data of the history usage is gathered. 300 sizePerStatus := float64(collMB) / float64(count) 301 if sizePerStatus == 0 { 302 return errors.New("unexpected result calculating status history entry size") 303 } 304 deleteStatuses := count - int(float64(collMB-maxHistoryMB)/sizePerStatus) 305 result := historicalStatusDoc{} 306 err = history.Find(nil).Sort("-updated").Skip(deleteStatuses).One(&result) 307 if err != nil { 308 return errors.Trace(err) 309 } 310 _, err = history.RemoveAll(bson.D{ 311 {"updated", bson.M{"$lt": result.Updated}}, 312 }) 313 if err != nil { 314 return errors.Trace(err) 315 } 316 return nil 317 }