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  }