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

     1  // Copyright 2017 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  package presence
     4  
     5  import (
     6  	"math/rand"
     7  	"strings"
     8  	"sync"
     9  	"time"
    10  
    11  	"github.com/juju/collections/set"
    12  	"github.com/juju/errors"
    13  	"gopkg.in/mgo.v2"
    14  	"gopkg.in/mgo.v2/bson"
    15  	"gopkg.in/tomb.v2"
    16  )
    17  
    18  const (
    19  	maxBatch         = 1000
    20  	defaultSyncDelay = 10 * time.Millisecond
    21  )
    22  
    23  type slot struct {
    24  	Slot  int64
    25  	Alive map[string]uint64
    26  }
    27  
    28  type singlePing struct {
    29  	Slot      int64
    30  	ModelUUID string
    31  	FieldKey  string
    32  	FieldBit  uint64
    33  }
    34  
    35  // NewPingBatcher creates a worker that will batch ping requests and prepare them
    36  // for insertion into the Pings collection. Pass in the base "presence" collection.
    37  // flushInterval is how often we will write the contents to the database.
    38  // It should be shorter than the 30s slot window for us to not cause active
    39  // pingers to show up as missing. The current default is 1s as it provides a good
    40  // balance of significant-batching-for-performance while still having responsiveness
    41  // to agents coming alive.
    42  // Note that we don't strictly sync on flushInterval times, but use a range of
    43  // times around that interval to avoid having all ping batchers get synchronized
    44  // and still be issuing all requests concurrently.
    45  func NewPingBatcher(base *mgo.Collection, flushInterval time.Duration) *PingBatcher {
    46  	var pings *mgo.Collection
    47  	if base != nil {
    48  		pings = pingsC(base)
    49  	}
    50  	pb := &PingBatcher{
    51  		pings:         pings,
    52  		pending:       make(map[string]slot),
    53  		flushInterval: flushInterval,
    54  		pingChan:      make(chan singlePing),
    55  		syncChan:      make(chan chan struct{}),
    56  		syncDelay:     defaultSyncDelay,
    57  		rand:          rand.New(rand.NewSource(time.Now().UnixNano())),
    58  	}
    59  	pb.useInc = checkMongoVersion(base)
    60  	pb.start()
    61  	return pb
    62  }
    63  
    64  // NewDeadPingBatcher returns a PingBatcher that is already stopped with an error.
    65  func NewDeadPingBatcher(err error) *PingBatcher {
    66  	// we never start the loop, so the timeout doesn't matter.
    67  	pb := &PingBatcher{}
    68  	pb.tomb.Kill(err)
    69  	return pb
    70  }
    71  
    72  // PingBatcher aggregates several pingers to update the database on a fixed schedule.
    73  type PingBatcher struct {
    74  
    75  	// pings is the collection where we record our information
    76  	pings *mgo.Collection
    77  
    78  	// pending is the list of pings that have not been written to the database yet
    79  	pending map[string]slot
    80  
    81  	// pingCount is how many pings we've received that we have not flushed
    82  	pingCount uint64
    83  
    84  	// flushInterval is the nominal amount of time where we will automatically flush
    85  	flushInterval time.Duration
    86  
    87  	// rand is a random source used to vary our nominal flushInterval
    88  	rand *rand.Rand
    89  
    90  	// tomb is used to track a request to shutdown this worker
    91  	tomb tomb.Tomb
    92  
    93  	// pingChan is where requests from Ping() are brought into the main loop
    94  	pingChan chan singlePing
    95  
    96  	// syncChan is where explicit requests to flush come in
    97  	syncChan chan chan struct{}
    98  
    99  	// syncDelay is the time we will wait before triggering a flush after a
   100  	// sync request comes in. We don't do it immediately so that many agents
   101  	// waking all issuing their initial request still don't flood the database
   102  	// with separate requests, but we do respond faster than normal.
   103  	syncDelay time.Duration
   104  
   105  	// awaitingSync is the slice of requests that are waiting for flush to finish
   106  	awaitingSync []chan struct{}
   107  
   108  	// flushMutex ensures only one concurrent flush is done
   109  	flushMutex sync.Mutex
   110  
   111  	// useInc is set to True if we discover the mongo version doesn't support $bit and upsert correctly.
   112  	// see https://bugs.launchpad.net/juju/+bug/1699678
   113  	useInc bool
   114  }
   115  
   116  // Start the worker loop.
   117  func (pb *PingBatcher) start() {
   118  	pb.tomb.Go(func() error {
   119  		err := pb.loop()
   120  		cause := errors.Cause(err)
   121  		// tomb expects ErrDying or ErrStillAlive as
   122  		// exact values, so we need to log and unwrap
   123  		// the error first.
   124  		if err != nil && cause != tomb.ErrDying {
   125  			logger.Infof("ping batching loop failed: %v", err)
   126  		}
   127  		return cause
   128  	})
   129  }
   130  
   131  // Kill is part of the worker.Worker interface.
   132  func (pb *PingBatcher) Kill() {
   133  	pb.tomb.Kill(nil)
   134  }
   135  
   136  // Wait returns when the Pinger has stopped, and returns the first error
   137  // it encountered.
   138  func (pb *PingBatcher) Wait() error {
   139  	return pb.tomb.Wait()
   140  }
   141  
   142  // Stop this PingBatcher, part of the extended Worker interface.
   143  func (pb *PingBatcher) Stop() error {
   144  	if err := pb.tomb.Err(); err != tomb.ErrStillAlive {
   145  		return err
   146  	}
   147  	pb.tomb.Kill(nil)
   148  	err := pb.tomb.Wait()
   149  	return errors.Trace(err)
   150  }
   151  
   152  // nextSleep determines how long we should wait before flushing our state to the database.
   153  // We use a range of time around the requested 'flushInterval', so that we avoid having
   154  // all requests to the database happen at exactly the same time across machines.
   155  func (pb *PingBatcher) nextSleep(r *rand.Rand) time.Duration {
   156  	sleepMin := float64(pb.flushInterval) * 0.8
   157  	sleepRange := float64(pb.flushInterval) * 0.4
   158  	offset := r.Int63n(int64(sleepRange))
   159  	return time.Duration(int64(sleepMin) + offset)
   160  }
   161  
   162  func checkMongoVersion(coll *mgo.Collection) bool {
   163  	if coll == nil {
   164  		logger.Debugf("using $inc operations with unknown mongo version")
   165  		return true
   166  	}
   167  	buildInfo, err := coll.Database.Session.BuildInfo()
   168  	if err != nil {
   169  		logger.Debugf("using $inc operations with unknown mongo version")
   170  		return true
   171  	}
   172  	// useInc is set to true if we discover the database is <2.6.
   173  	// Really old mongo (2.?) didn't support $bit at all, and in Mongo 2.4,
   174  	// it did not handle Upsert and $bit operations correctly.
   175  	// (see https://bugs.launchpad.net/juju/+bug/1699678)
   176  	if len(buildInfo.VersionArray) < 2 {
   177  		// Something weird, just fallback to safe mode
   178  		logger.Debugf("using $inc operations with misunderstood Mongo version: %s", buildInfo.Version)
   179  		return true
   180  	}
   181  	if buildInfo.VersionArray[0] >= 3 ||
   182  		(buildInfo.VersionArray[0] == 2 && buildInfo.VersionArray[1] >= 6) {
   183  		logger.Debugf("using $bit operations with Mongo %s", buildInfo.Version)
   184  		return false
   185  	} else {
   186  		logger.Debugf("using $inc operations with Mongo %s", buildInfo.Version)
   187  		return true
   188  	}
   189  }
   190  
   191  func (pb *PingBatcher) loop() error {
   192  	flushTimeout := time.After(pb.nextSleep(pb.rand))
   193  	var syncTimeout <-chan time.Time
   194  	for {
   195  		doflush := func() error {
   196  			syncTimeout = nil
   197  			err := pb.flush()
   198  			flushTimeout = time.After(pb.nextSleep(pb.rand))
   199  			return errors.Trace(err)
   200  		}
   201  		select {
   202  		case <-pb.tomb.Dying():
   203  			// We were asked to shut down. Make sure we flush
   204  			if err := pb.flush(); err != nil {
   205  				return errors.Trace(err)
   206  			}
   207  			return errors.Trace(tomb.ErrDying)
   208  		case singlePing := <-pb.pingChan:
   209  			pb.handlePing(singlePing)
   210  		case syncReq := <-pb.syncChan:
   211  			// Flush is requested synchronously.
   212  			// The caller passes in a channel we can close so that
   213  			// they know when we have finished flushing.
   214  			// We also know that any "Ping()" requests that have
   215  			// returned will have been handled before Flush()
   216  			// because they are all serialized in this loop.
   217  			// We need to guard access to pb.awaitingSync as tests
   218  			// poke this asynchronously.
   219  			pb.flushMutex.Lock()
   220  			pb.awaitingSync = append(pb.awaitingSync, syncReq)
   221  			pb.flushMutex.Unlock()
   222  			if syncTimeout == nil {
   223  				syncTimeout = time.After(pb.syncDelay)
   224  			}
   225  		case <-syncTimeout:
   226  			// Golang says I can't use 'fallthrough' here, but I
   227  			// want to do exactly the same thing if either of the channels trigger
   228  			// fallthrough
   229  			if err := doflush(); err != nil {
   230  				return errors.Trace(err)
   231  			}
   232  		case <-flushTimeout:
   233  			if err := doflush(); err != nil {
   234  				return errors.Trace(err)
   235  			}
   236  		}
   237  	}
   238  }
   239  
   240  // Ping should be called by a Pinger when it is ready to update its time slot.
   241  // It passes in all of the pre-resolved information (what exact field bit is
   242  // being set), rather than the higher level "I'm pinging for this Agent".
   243  // Internally, we synchronize with the main worker loop. Which means that this
   244  // function will return once the main loop recognizes that we have a ping request
   245  // but it will not have updated its internal structures, and certainly not the database.
   246  func (pb *PingBatcher) Ping(modelUUID string, slot int64, fieldKey string, fieldBit uint64) error {
   247  	ping := singlePing{
   248  		Slot:      slot,
   249  		ModelUUID: modelUUID,
   250  		FieldKey:  fieldKey,
   251  		FieldBit:  fieldBit,
   252  	}
   253  	select {
   254  	case pb.pingChan <- ping:
   255  		return nil
   256  	case <-pb.tomb.Dying():
   257  		err := pb.tomb.Err()
   258  		if err == nil {
   259  			return errors.Errorf("PingBatcher is stopped")
   260  		}
   261  		return errors.Trace(err)
   262  	}
   263  }
   264  
   265  // Sync schedules a flush of the current state to the database.
   266  // This is not immediate, but actually within a short timeout so that many calls
   267  // to sync in a short time frame will only trigger one write to the database.
   268  func (pb *PingBatcher) Sync() error {
   269  	request := make(chan struct{})
   270  	select {
   271  	case pb.syncChan <- request:
   272  		select {
   273  		case <-request:
   274  			return nil
   275  		case <-pb.tomb.Dying():
   276  			break
   277  		}
   278  	case <-pb.tomb.Dying():
   279  		break
   280  	}
   281  	if err := pb.tomb.Err(); err == nil {
   282  		return errors.Errorf("PingBatcher is stopped")
   283  	} else {
   284  		return err
   285  	}
   286  }
   287  
   288  // handlePing is where we actually update our internal structures after we
   289  // get a ping request.
   290  func (pb *PingBatcher) handlePing(ping singlePing) {
   291  	docId := docIDInt64(ping.ModelUUID, ping.Slot)
   292  	cur, slotExists := pb.pending[docId]
   293  	if !slotExists {
   294  		cur.Alive = make(map[string]uint64)
   295  		cur.Slot = ping.Slot
   296  		pb.pending[docId] = cur
   297  	}
   298  	alive := cur.Alive
   299  	alive[ping.FieldKey] |= ping.FieldBit
   300  	pb.pingCount++
   301  }
   302  
   303  func (pb *PingBatcher) upsertFieldsUsingInc(slt slot) bson.D {
   304  	var incFields bson.D
   305  	for fieldKey, value := range slt.Alive {
   306  		incFields = append(incFields, bson.DocElem{Name: "alive." + fieldKey, Value: value})
   307  	}
   308  	return bson.D{
   309  		{"$set", bson.D{{"slot", slt.Slot}}},
   310  		{"$inc", incFields},
   311  	}
   312  }
   313  
   314  func (pb *PingBatcher) upsertFieldsUsingBit(slt slot) bson.D {
   315  	var fields bson.D
   316  	for fieldKey, value := range slt.Alive {
   317  		fields = append(fields, bson.DocElem{Name: "alive." + fieldKey, Value: bson.M{"or": value}})
   318  	}
   319  	return bson.D{
   320  		{"$set", bson.D{{"slot", slt.Slot}}},
   321  		{"$bit", fields},
   322  	}
   323  }
   324  
   325  // flush pushes the internal state to the database. Note that if the database
   326  // updates fail, we will still wipe our internal state as it is unsafe to
   327  // publish the same updates to the same slots.
   328  func (pb *PingBatcher) flush() error {
   329  	pb.flushMutex.Lock()
   330  	defer pb.flushMutex.Unlock()
   331  
   332  	awaiting := pb.awaitingSync
   333  	pb.awaitingSync = nil
   334  	// We are doing a flush, make sure everyone waiting is told that it has been done
   335  	defer func() {
   336  		for _, waiting := range awaiting {
   337  			close(waiting)
   338  		}
   339  	}()
   340  	if pb.pingCount == 0 {
   341  		return nil
   342  	}
   343  	uuids := set.NewStrings()
   344  	// We treat all of these as 'consumed'. Even if the query fails, it is
   345  	// not safe to ever $inc the same fields a second time, so we just move on.
   346  	next := pb.pending
   347  	pingCount := pb.pingCount
   348  	pb.pending = make(map[string]slot)
   349  	pb.pingCount = 0
   350  	session := pb.pings.Database.Session.Copy()
   351  	defer session.Close()
   352  	pings := pb.pings.With(session)
   353  	docCount := 0
   354  	fieldCount := 0
   355  	t := time.Now()
   356  	for docId, slot := range next {
   357  		docCount++
   358  		fieldCount += len(slot.Alive)
   359  		var update bson.D
   360  		if pb.useInc {
   361  			update = pb.upsertFieldsUsingInc(slot)
   362  		} else {
   363  			update = pb.upsertFieldsUsingBit(slot)
   364  		}
   365  		// Note: UpsertId already handles hitting the DuplicateKey error internally
   366  		// We also just Upsert directly instead of using Bulk because for now each PingBatcher is actually
   367  		// only used by 1 model. Given 30s slots, we only ever hit 1 or 2 documents being updated at the same
   368  		// time. If we switch to sharing batchers between models, then it might make more sense to use bulk updates
   369  		// but then we need to handle when we get Duplicate Key errors during update.
   370  		_, err := pings.UpsertId(docId, update)
   371  		if err != nil {
   372  			return errors.Trace(err)
   373  		}
   374  		if logger.IsTraceEnabled() {
   375  			// the rest of Pings records the first 6 characters of
   376  			// model-uuids, so we include that here if we are TRACEing.
   377  			uuids.Add(docId[:6])
   378  		}
   379  	}
   380  	// usually we should only be processing 1 slot
   381  	logger.Tracef("%p [%v] recorded %d pings for %d ping slot(s) and %d fields in %.3fs",
   382  		pb, strings.Join(uuids.SortedValues(), ", "), pingCount, docCount, fieldCount, time.Since(t).Seconds())
   383  	return nil
   384  }