github.com/wallyworld/juju@v0.0.0-20161013125918-6cf1bc9d917a/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  }