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 }