github.com/makyo/juju@v0.0.0-20160425123129-2608902037e9/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  	"github.com/juju/juju/core/lease"
    14  )
    15  
    16  // These constants define the field names and type values used by documents in
    17  // a lease collection. They *must* remain in sync with the bson marshalling
    18  // annotations in leaseDoc and clockDoc.
    19  const (
    20  	// fieldType and fieldNamespace identify the Type and Namespace fields in
    21  	// both leaseDoc and clockDoc.
    22  	fieldType      = "type"
    23  	fieldNamespace = "namespace"
    24  
    25  	// typeLease and typeClock are the acceptable values for fieldType.
    26  	typeLease = "lease"
    27  	typeClock = "clock"
    28  
    29  	// fieldLease* identify the fields in a leaseDoc.
    30  	fieldLeaseHolder = "holder"
    31  	fieldLeaseExpiry = "expiry"
    32  	fieldLeaseWriter = "writer"
    33  
    34  	// fieldClock* identify the fields in a clockDoc.
    35  	fieldClockWriters = "writers"
    36  )
    37  
    38  // toInt64 converts a local time.Time into a database value that doesn't
    39  // silently lose precision.
    40  func toInt64(t time.Time) int64 {
    41  	return t.UnixNano()
    42  }
    43  
    44  // toTime converts a toInt64 result, as loaded from the db, back to a time.Time.
    45  func toTime(v int64) time.Time {
    46  	return time.Unix(0, v)
    47  }
    48  
    49  // leaseDocId returns the _id for the document holding details of the supplied
    50  // namespace and lease.
    51  func leaseDocId(namespace, lease string) string {
    52  	return fmt.Sprintf("%s#%s#%s#", typeLease, namespace, lease)
    53  }
    54  
    55  // leaseDoc is used to serialise lease entries.
    56  type leaseDoc struct {
    57  	// Id is always "<Type>#<Namespace>#<Name>#", and <Type> is always "lease",
    58  	// so that we can extract useful information from a stream of watcher events
    59  	// without incurring extra DB hits.
    60  	// Apart from checking validity on load, though, there's little reason
    61  	// to use Id elsewhere; Namespace and Name are the sources of truth.
    62  	Id        string `bson:"_id"`
    63  	Type      string `bson:"type"`
    64  	Namespace string `bson:"namespace"`
    65  	Name      string `bson:"name"`
    66  
    67  	// Holder, Expiry, and Writer map directly to entry.
    68  	Holder string `bson:"holder"`
    69  	Expiry int64  `bson:"expiry"`
    70  	Writer string `bson:"writer"`
    71  }
    72  
    73  // validate returns an error if any fields are invalid or inconsistent.
    74  func (doc leaseDoc) validate() error {
    75  	if doc.Type != typeLease {
    76  		return errors.Errorf("invalid type %q", doc.Type)
    77  	}
    78  	// state.multiModelRunner prepends environ ids in our documents, and
    79  	// state.modelStateCollection does not strip them out.
    80  	if !strings.HasSuffix(doc.Id, leaseDocId(doc.Namespace, doc.Name)) {
    81  		return errors.Errorf("inconsistent _id")
    82  	}
    83  	if err := lease.ValidateString(doc.Holder); err != nil {
    84  		return errors.Annotatef(err, "invalid holder")
    85  	}
    86  	if doc.Expiry == 0 {
    87  		return errors.Errorf("invalid expiry")
    88  	}
    89  	if err := lease.ValidateString(doc.Writer); err != nil {
    90  		return errors.Annotatef(err, "invalid writer")
    91  	}
    92  	return nil
    93  }
    94  
    95  // entry returns the lease name and an entry corresponding to the document. If
    96  // the document cannot be validated, it returns an error.
    97  func (doc leaseDoc) entry() (string, entry, error) {
    98  	if err := doc.validate(); err != nil {
    99  		return "", entry{}, errors.Trace(err)
   100  	}
   101  	entry := entry{
   102  		holder: doc.Holder,
   103  		expiry: toTime(doc.Expiry),
   104  		writer: doc.Writer,
   105  	}
   106  	return doc.Name, entry, nil
   107  }
   108  
   109  // newLeaseDoc returns a valid lease document encoding the supplied lease and
   110  // entry in the supplied namespace, or an error.
   111  func newLeaseDoc(namespace, name string, entry entry) (*leaseDoc, error) {
   112  	doc := &leaseDoc{
   113  		Id:        leaseDocId(namespace, name),
   114  		Type:      typeLease,
   115  		Namespace: namespace,
   116  		Name:      name,
   117  		Holder:    entry.holder,
   118  		Expiry:    toInt64(entry.expiry),
   119  		Writer:    entry.writer,
   120  	}
   121  	if err := doc.validate(); err != nil {
   122  		return nil, errors.Trace(err)
   123  	}
   124  	return doc, nil
   125  }
   126  
   127  // clockDocId returns the _id for the document holding clock skew information
   128  // for clients that have written in the supplied namespace.
   129  func clockDocId(namespace string) string {
   130  	return fmt.Sprintf("%s#%s#", typeClock, namespace)
   131  }
   132  
   133  // clockDoc is used to synchronise clients.
   134  type clockDoc struct {
   135  	// Id is always "<Type>#<Namespace>#", and <Type> is always "clock", for
   136  	// consistency with leaseDoc and ease of querying within the collection.
   137  	Id        string `bson:"_id"`
   138  	Type      string `bson:"type"`
   139  	Namespace string `bson:"namespace"`
   140  
   141  	// Writers holds the latest acknowledged time for every known client.
   142  	Writers map[string]int64 `bson:"writers"`
   143  }
   144  
   145  // validate returns an error if any fields are invalid or inconsistent.
   146  func (doc clockDoc) validate() error {
   147  	if doc.Type != typeClock {
   148  		return errors.Errorf("invalid type %q", doc.Type)
   149  	}
   150  	// state.multiModelRunner prepends environ ids in our documents, and
   151  	// state.modelStateCollection does not strip them out.
   152  	if !strings.HasSuffix(doc.Id, clockDocId(doc.Namespace)) {
   153  		return errors.Errorf("inconsistent _id")
   154  	}
   155  	for writer, written := range doc.Writers {
   156  		if written == 0 {
   157  			return errors.Errorf("invalid time for writer %q", writer)
   158  		}
   159  	}
   160  	return nil
   161  }
   162  
   163  // skews returns clock skew information for all writers recorded in the
   164  // document, given that the document was read between the supplied local
   165  // times. It will return an error if the clock document is not valid, or
   166  // if the times don't make sense.
   167  func (doc clockDoc) skews(beginning, end time.Time) (map[string]Skew, error) {
   168  	if err := doc.validate(); err != nil {
   169  		return nil, errors.Trace(err)
   170  	}
   171  	// beginning is expected to be earlier than end.
   172  	// If it isn't, it could be ntp rolling the clock back slowly, so we add
   173  	// a little wiggle room here.
   174  	if end.Before(beginning) {
   175  		// A later time, subtract an earlier time will give a positive duration.
   176  		difference := beginning.Sub(end)
   177  		if difference > 10*time.Millisecond {
   178  			return nil, errors.Errorf("end of read window preceded beginning (%s)", difference)
   179  
   180  		}
   181  		beginning = end
   182  	}
   183  	skews := make(map[string]Skew)
   184  	for writer, written := range doc.Writers {
   185  		skews[writer] = Skew{
   186  			LastWrite: toTime(written),
   187  			Beginning: beginning,
   188  			End:       end,
   189  		}
   190  	}
   191  	return skews, nil
   192  }
   193  
   194  // newClockDoc returns an empty clockDoc for the supplied namespace.
   195  func newClockDoc(namespace string) (*clockDoc, error) {
   196  	doc := &clockDoc{
   197  		Id:        clockDocId(namespace),
   198  		Type:      typeClock,
   199  		Namespace: namespace,
   200  		Writers:   make(map[string]int64),
   201  	}
   202  	if err := doc.validate(); err != nil {
   203  		return nil, errors.Trace(err)
   204  	}
   205  	return doc, nil
   206  }