github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/state/raftlease/target.go (about)

     1  // Copyright 2018 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package raftlease
     5  
     6  import (
     7  	"fmt"
     8  	"io"
     9  	"log"
    10  
    11  	"github.com/juju/errors"
    12  	"github.com/juju/loggo"
    13  	jujutxn "github.com/juju/txn"
    14  	"gopkg.in/mgo.v2"
    15  	"gopkg.in/mgo.v2/bson"
    16  	"gopkg.in/mgo.v2/txn"
    17  
    18  	"github.com/juju/juju/core/lease"
    19  	"github.com/juju/juju/core/raftlease"
    20  	"github.com/juju/juju/mongo"
    21  )
    22  
    23  const (
    24  	// fieldNamespace identifies the namespace field in a leaseHolderDoc.
    25  	fieldNamespace = "namespace"
    26  
    27  	// fieldModelUUID identifies the model UUID field in a leaseHolderDoc.
    28  	fieldModelUUID = "model-uuid"
    29  
    30  	// fieldHolder identifies the holder field in a leaseHolderDoc.
    31  	fieldHolder = "holder"
    32  )
    33  
    34  // logger is only used when we need to update the database from within
    35  // a trapdoor function.
    36  var logger = loggo.GetLogger("juju.state.raftlease")
    37  
    38  // leaseHolderDoc is used to serialise lease holder info.
    39  type leaseHolderDoc struct {
    40  	Id        string `bson:"_id"`
    41  	Namespace string `bson:"namespace"`
    42  	ModelUUID string `bson:"model-uuid"`
    43  	Lease     string `bson:"lease"`
    44  	Holder    string `bson:"holder"`
    45  }
    46  
    47  // leaseHolderDocId returns the _id for the document holding details of the supplied
    48  // namespace and lease.
    49  func leaseHolderDocId(namespace, modelUUID, lease string) string {
    50  	return fmt.Sprintf("%s:%s#%s#", modelUUID, namespace, lease)
    51  }
    52  
    53  // validate returns an error if any fields are invalid or inconsistent.
    54  func (doc leaseHolderDoc) validate() error {
    55  	if doc.Id != leaseHolderDocId(doc.Namespace, doc.ModelUUID, doc.Lease) {
    56  		return errors.Errorf("inconsistent _id")
    57  	}
    58  	if err := lease.ValidateString(doc.Holder); err != nil {
    59  		return errors.Annotatef(err, "invalid holder")
    60  	}
    61  	return nil
    62  }
    63  
    64  // newLeaseHolderDoc returns a valid lease document encoding the supplied lease and
    65  // entry in the supplied namespace, or an error.
    66  func newLeaseHolderDoc(namespace, modelUUID, name, holder string) (*leaseHolderDoc, error) {
    67  	doc := &leaseHolderDoc{
    68  		Id:        leaseHolderDocId(namespace, modelUUID, name),
    69  		Namespace: namespace,
    70  		ModelUUID: modelUUID,
    71  		Lease:     name,
    72  		Holder:    holder,
    73  	}
    74  	if err := doc.validate(); err != nil {
    75  		return nil, errors.Trace(err)
    76  	}
    77  	return doc, nil
    78  }
    79  
    80  // Mongo exposes MongoDB operations for use by the lease package.
    81  type Mongo interface {
    82  
    83  	// RunTransaction should probably delegate to a jujutxn.Runner's Run method.
    84  	RunTransaction(jujutxn.TransactionSource) error
    85  
    86  	// GetCollection should probably call the mongo.CollectionFromName func.
    87  	GetCollection(name string) (collection mongo.Collection, closer func())
    88  }
    89  
    90  // Logger allows us to report errors if we can't write to the database
    91  // for some reason.
    92  type Logger interface {
    93  	Errorf(string, ...interface{})
    94  }
    95  
    96  // NewNotifyTarget returns something that can be used to report lease
    97  // changes.
    98  func NewNotifyTarget(mongo Mongo, collection string, logDest io.Writer, errorLogger Logger) raftlease.NotifyTarget {
    99  	return &notifyTarget{
   100  		mongo:       mongo,
   101  		collection:  collection,
   102  		logger:      log.New(logDest, "", log.LstdFlags|log.Lmicroseconds|log.LUTC),
   103  		errorLogger: errorLogger,
   104  	}
   105  }
   106  
   107  // notifyTarget is a raftlease.NotifyTarget that updates the
   108  // information in mongo, as well as logging the lease changes.  Since
   109  // the callbacks it exposes aren't allowed to return errors, it takes
   110  // a logger for write errors as well as a destination for tracing
   111  // lease changes.
   112  type notifyTarget struct {
   113  	mongo       Mongo
   114  	collection  string
   115  	logger      *log.Logger
   116  	errorLogger Logger
   117  }
   118  
   119  func (t *notifyTarget) log(message string, args ...interface{}) {
   120  	err := t.logger.Output(2, fmt.Sprintf(message, args...))
   121  	if err != nil {
   122  		t.errorLogger.Errorf("couldn't write to lease log: %s", err.Error())
   123  	}
   124  }
   125  
   126  func buildClaimedOps(coll mongo.Collection, docId string, key lease.Key, holder string) ([]txn.Op, error) {
   127  	existingDoc, err := getRecord(coll, docId)
   128  	switch {
   129  	case err == mgo.ErrNotFound:
   130  		doc, err := newLeaseHolderDoc(
   131  			key.Namespace,
   132  			key.ModelUUID,
   133  			key.Lease,
   134  			holder,
   135  		)
   136  		if err != nil {
   137  			return nil, errors.Trace(err)
   138  		}
   139  		return []txn.Op{{
   140  			C:      coll.Name(),
   141  			Id:     docId,
   142  			Assert: txn.DocMissing,
   143  			Insert: doc,
   144  		}}, nil
   145  
   146  	case err != nil:
   147  		return nil, errors.Trace(err)
   148  
   149  	case existingDoc.Holder == holder:
   150  		return nil, jujutxn.ErrNoOperations
   151  
   152  	default:
   153  		return []txn.Op{{
   154  			C:  coll.Name(),
   155  			Id: docId,
   156  			Assert: bson.M{
   157  				fieldHolder: existingDoc.Holder,
   158  			},
   159  			Update: bson.M{
   160  				"$set": bson.M{
   161  					fieldHolder: holder,
   162  				},
   163  			},
   164  		}}, nil
   165  	}
   166  }
   167  
   168  func applyClaimed(mongo Mongo, collection string, docId string, key lease.Key, holder string) (bool, error) {
   169  	coll, closer := mongo.GetCollection(collection)
   170  	defer closer()
   171  	var writeNeeded bool
   172  	err := mongo.RunTransaction(func(int) ([]txn.Op, error) {
   173  		ops, err := buildClaimedOps(coll, docId, key, holder)
   174  		writeNeeded = len(ops) != 0
   175  		return ops, err
   176  	})
   177  	return writeNeeded, errors.Trace(err)
   178  }
   179  
   180  // Claimed is part of raftlease.NotifyTarget.
   181  func (t *notifyTarget) Claimed(key lease.Key, holder string) {
   182  	docId := leaseHolderDocId(key.Namespace, key.ModelUUID, key.Lease)
   183  	t.log("claimed %q for %q", docId, holder)
   184  	_, err := applyClaimed(t.mongo, t.collection, docId, key, holder)
   185  	if err != nil {
   186  		t.errorLogger.Errorf("couldn't claim lease %q for %q in db: %s", docId, holder, err.Error())
   187  		t.log("couldn't claim lease %q for %q in db: %s", docId, holder, err.Error())
   188  		return
   189  	}
   190  }
   191  
   192  // Expired is part of raftlease.NotifyTarget.
   193  func (t *notifyTarget) Expired(key lease.Key) {
   194  	coll, closer := t.mongo.GetCollection(t.collection)
   195  	defer closer()
   196  	docId := leaseHolderDocId(key.Namespace, key.ModelUUID, key.Lease)
   197  	t.log("expired %q", docId)
   198  	err := t.mongo.RunTransaction(func(_ int) ([]txn.Op, error) {
   199  		existingDoc, err := getRecord(coll, docId)
   200  		if err == mgo.ErrNotFound {
   201  			return nil, jujutxn.ErrNoOperations
   202  		}
   203  		if err != nil {
   204  			return nil, errors.Trace(err)
   205  		}
   206  		return []txn.Op{{
   207  			C:  t.collection,
   208  			Id: docId,
   209  			Assert: bson.M{
   210  				fieldHolder: existingDoc.Holder,
   211  			},
   212  			Remove: true,
   213  		}}, nil
   214  	})
   215  
   216  	if err != nil {
   217  		t.errorLogger.Errorf("couldn't expire lease %q in db: %s", docId, err.Error())
   218  		t.log("couldn't expire lease %q in db: %s", docId, err.Error())
   219  		return
   220  	}
   221  }
   222  
   223  // MakeTrapdoorFunc returns a raftlease.TrapdoorFunc for the specified
   224  // collection.
   225  func MakeTrapdoorFunc(mongo Mongo, collection string) raftlease.TrapdoorFunc {
   226  	return func(key lease.Key, holder string) lease.Trapdoor {
   227  		return func(attempt int, out interface{}) error {
   228  			outPtr, ok := out.(*[]txn.Op)
   229  			if !ok {
   230  				return errors.NotValidf("expected *[]txn.Op; %T", out)
   231  			}
   232  			if attempt != 0 {
   233  				// If the assertion failed it may be because a claim
   234  				// notify failed in the past due to the DB not being
   235  				// available. Sync the lease holder - this is safe to
   236  				// do because raft is the arbiter of who really holds
   237  				// the lease, and we check that the lease is held in
   238  				// buildTxnWithLeadership each time before collecting
   239  				// the assertion ops.
   240  				docId := leaseHolderDocId(key.Namespace, key.ModelUUID, key.Lease)
   241  				writeNeeded, err := applyClaimed(mongo, collection, docId, key, holder)
   242  				if err != nil {
   243  					return errors.Trace(err)
   244  				}
   245  				if writeNeeded {
   246  					logger.Infof("trapdoor claimed lease %q for %q", docId, holder)
   247  				}
   248  			}
   249  			*outPtr = []txn.Op{{
   250  				C: collection,
   251  				Id: leaseHolderDocId(
   252  					key.Namespace,
   253  					key.ModelUUID,
   254  					key.Lease,
   255  				),
   256  				Assert: bson.M{
   257  					fieldHolder: holder,
   258  				},
   259  			}}
   260  			return nil
   261  		}
   262  	}
   263  }
   264  
   265  func getRecord(coll mongo.Collection, docId string) (leaseHolderDoc, error) {
   266  	var doc leaseHolderDoc
   267  	err := coll.FindId(docId).One(&doc)
   268  	if err != nil {
   269  		return leaseHolderDoc{}, err
   270  	}
   271  	return doc, nil
   272  }
   273  
   274  // LeaseHolders returns a map of each lease and the holder in the
   275  // specified namespace and model.
   276  func LeaseHolders(mongo Mongo, collection, namespace, modelUUID string) (map[string]string, error) {
   277  	coll, closer := mongo.GetCollection(collection)
   278  	defer closer()
   279  
   280  	iter := coll.Find(bson.M{
   281  		fieldNamespace: namespace,
   282  		fieldModelUUID: modelUUID,
   283  	}).Iter()
   284  	results := make(map[string]string)
   285  	var doc leaseHolderDoc
   286  	for iter.Next(&doc) {
   287  		results[doc.Lease] = doc.Holder
   288  	}
   289  
   290  	if err := iter.Close(); err != nil {
   291  		return nil, errors.Trace(err)
   292  	}
   293  	return results, nil
   294  }