github.com/cloud-green/juju@v0.0.0-20151002100041-a00291338d3d/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  
    14  // These constants define the field names and type values used by documents in
    15  // a lease collection. They *must* remain in sync with the bson marshalling
    16  // annotations in leaseDoc and clockDoc.
    17  const (
    18  	// fieldType and fieldNamespace identify the Type and Namespace fields in
    19  	// both leaseDoc and clockDoc.
    20  	fieldType      = "type"
    21  	fieldNamespace = "namespace"
    22  
    23  	// typeLease and typeClock are the acceptable values for fieldType.
    24  	typeLease = "lease"
    25  	typeClock = "clock"
    26  
    27  	// fieldLease* identify the fields in a leaseDoc.
    28  	fieldLeaseName   = "name"
    29  	fieldLeaseHolder = "holder"
    30  	fieldLeaseExpiry = "expiry"
    31  	fieldLeaseWriter = "writer"
    32  
    33  	// fieldClock* identify the fields in a clockDoc.
    34  	fieldClockWriters = "writers"
    35  )
    36  
    37  // toInt64 converts a local time.Time into a database value that doesn't
    38  // silently lose precision.
    39  func toInt64(t time.Time) int64 {
    40  	return t.UnixNano()
    41  }
    42  
    43  // toTime converts a toInt64 result, as loaded from the db, back to a time.Time.
    44  func toTime(v int64) time.Time {
    45  	return time.Unix(0, v)
    46  }
    47  
    48  // For simplicity's sake, we impose the same restrictions on all strings used
    49  // with the lease package: they may not be empty, and none of the following
    50  // characters are allowed.
    51  //   * '.' and '$' mean things to mongodb; we don't want to risk seeing them
    52  //     in key names.
    53  //   * '#' means something to the lease package and we don't want to risk
    54  //     confusing ourselves.
    55  //   * whitespace just seems like a bad idea.
    56  const badCharacters = ".$# \t\r\n"
    57  
    58  // validateString returns an error if the string is not valid.
    59  func validateString(s string) error {
    60  	if s == "" {
    61  		return errors.New("string is empty")
    62  	}
    63  	if strings.ContainsAny(s, badCharacters) {
    64  		return errors.New("string contains forbidden characters")
    65  	}
    66  	return nil
    67  }
    68  
    69  // leaseDocId returns the _id for the document holding details of the supplied
    70  // namespace and lease.
    71  func leaseDocId(namespace, lease string) string {
    72  	return fmt.Sprintf("%s#%s#%s#", typeLease, namespace, lease)
    73  }
    74  
    75  // leaseDoc is used to serialise lease entries.
    76  type leaseDoc struct {
    77  	// Id is always "<Type>#<Namespace>#<Name>#", and <Type> is always "lease",
    78  	// so that we can extract useful information from a stream of watcher events
    79  	// without incurring extra DB hits.
    80  	// Apart from checking validity on load, though, there's little reason
    81  	// to use Id elsewhere; Namespace and Name are the sources of truth.
    82  	Id        string `bson:"_id"`
    83  	Type      string `bson:"type"`
    84  	Namespace string `bson:"namespace"`
    85  	Name      string `bson:"name"`
    86  
    87  	// EnvUUID exists because state.multiEnvRunner can't handle structs
    88  	// without `bson:"env-uuid"` fields. It's not necessary for the logic
    89  	// in this package, though.
    90  	EnvUUID string `bson:"env-uuid"`
    91  
    92  	// Holder, Expiry, and Writer map directly to entry.
    93  	Holder string `bson:"holder"`
    94  	Expiry int64  `bson:"expiry"`
    95  	Writer string `bson:"writer"`
    96  }
    97  
    98  // validate returns an error if any fields are invalid or inconsistent.
    99  func (doc leaseDoc) validate() error {
   100  	if doc.Type != typeLease {
   101  		return errors.Errorf("invalid type %q", doc.Type)
   102  	}
   103  	// state.multiEnvRunner prepends environ ids in our documents, and
   104  	// state.envStateCollection does not strip them out.
   105  	if !strings.HasSuffix(doc.Id, leaseDocId(doc.Namespace, doc.Name)) {
   106  		return errors.Errorf("inconsistent _id")
   107  	}
   108  	if err := validateString(doc.Holder); err != nil {
   109  		return errors.Annotatef(err, "invalid holder")
   110  	}
   111  	if doc.Expiry == 0 {
   112  		return errors.Errorf("invalid expiry")
   113  	}
   114  	if err := validateString(doc.Writer); err != nil {
   115  		return errors.Annotatef(err, "invalid writer")
   116  	}
   117  	return nil
   118  }
   119  
   120  // entry returns the lease name and an entry corresponding to the document. If
   121  // the document cannot be validated, it returns an error.
   122  func (doc leaseDoc) entry() (string, entry, error) {
   123  	if err := doc.validate(); err != nil {
   124  		return "", entry{}, errors.Trace(err)
   125  	}
   126  	entry := entry{
   127  		holder: doc.Holder,
   128  		expiry: toTime(doc.Expiry),
   129  		writer: doc.Writer,
   130  	}
   131  	return doc.Name, entry, nil
   132  }
   133  
   134  // newLeaseDoc returns a valid lease document encoding the supplied lease and
   135  // entry in the supplied namespace, or an error.
   136  func newLeaseDoc(namespace, name string, entry entry) (*leaseDoc, error) {
   137  	doc := &leaseDoc{
   138  		Id:        leaseDocId(namespace, name),
   139  		Type:      typeLease,
   140  		Namespace: namespace,
   141  		Name:      name,
   142  		Holder:    entry.holder,
   143  		Expiry:    toInt64(entry.expiry),
   144  		Writer:    entry.writer,
   145  	}
   146  	if err := doc.validate(); err != nil {
   147  		return nil, errors.Trace(err)
   148  	}
   149  	return doc, nil
   150  }
   151  
   152  // clockDocId returns the _id for the document holding clock skew information
   153  // for clients that have written in the supplied namespace.
   154  func clockDocId(namespace string) string {
   155  	return fmt.Sprintf("%s#%s#", typeClock, namespace)
   156  }
   157  
   158  // clockDoc is used to synchronise clients.
   159  type clockDoc struct {
   160  	// Id is always "<Type>#<Namespace>#", and <Type> is always "clock", for
   161  	// consistency with leaseDoc and ease of querying within the collection.
   162  	Id        string `bson:"_id"`
   163  	Type      string `bson:"type"`
   164  	Namespace string `bson:"namespace"`
   165  
   166  	// EnvUUID exists because state.multiEnvRunner can't handle structs
   167  	// without `bson:"env-uuid"` fields. It's not necessary for the logic
   168  	// in this package, though.
   169  	EnvUUID string `bson:"env-uuid"`
   170  
   171  	// Writers holds a the latest acknowledged time for every known client.
   172  	Writers map[string]int64 `bson:"writers"`
   173  }
   174  
   175  // validate returns an error if any fields are invalid or inconsistent.
   176  func (doc clockDoc) validate() error {
   177  	if doc.Type != typeClock {
   178  		return errors.Errorf("invalid type %q", doc.Type)
   179  	}
   180  	// state.multiEnvRunner prepends environ ids in our documents, and
   181  	// state.envStateCollection does not strip them out.
   182  	if !strings.HasSuffix(doc.Id, clockDocId(doc.Namespace)) {
   183  		return errors.Errorf("inconsistent _id")
   184  	}
   185  	for writer, written := range doc.Writers {
   186  		if written == 0 {
   187  			return errors.Errorf("invalid time for writer %q", writer)
   188  		}
   189  	}
   190  	return nil
   191  }
   192  
   193  // skews returns clock skew information for all writers recorded in the
   194  // document, given that the document was read between the supplied local
   195  // times. It will return an error if the clock document is not valid, or
   196  // if the times don't make sense.
   197  func (doc clockDoc) skews(readAfter, readBefore time.Time) (map[string]Skew, error) {
   198  	if err := doc.validate(); err != nil {
   199  		return nil, errors.Trace(err)
   200  	}
   201  	if readBefore.Before(readAfter) {
   202  		return nil, errors.New("end of read window preceded beginning")
   203  	}
   204  	skews := make(map[string]Skew)
   205  	for writer, written := range doc.Writers {
   206  		skews[writer] = Skew{
   207  			LastWrite:  toTime(written),
   208  			ReadAfter:  readAfter,
   209  			ReadBefore: readBefore,
   210  		}
   211  	}
   212  	return skews, nil
   213  }
   214  
   215  // newClockDoc returns an empty clockDoc for the supplied namespace.
   216  func newClockDoc(namespace string) (*clockDoc, error) {
   217  	doc := &clockDoc{
   218  		Id:        clockDocId(namespace),
   219  		Type:      typeClock,
   220  		Namespace: namespace,
   221  		Writers:   make(map[string]int64),
   222  	}
   223  	if err := doc.validate(); err != nil {
   224  		return nil, errors.Trace(err)
   225  	}
   226  	return doc, nil
   227  }