github.com/keybase/client/go@v0.0.0-20241007131713-f10651d043c8/chat/convloader.go (about) 1 package chat 2 3 import ( 4 "container/list" 5 "errors" 6 "sync" 7 "time" 8 9 "golang.org/x/net/context" 10 "golang.org/x/sync/errgroup" 11 12 "github.com/keybase/client/go/chat/globals" 13 "github.com/keybase/client/go/chat/storage" 14 "github.com/keybase/client/go/chat/types" 15 "github.com/keybase/client/go/chat/utils" 16 "github.com/keybase/client/go/libkb" 17 "github.com/keybase/client/go/protocol/chat1" 18 "github.com/keybase/client/go/protocol/gregor1" 19 "github.com/keybase/client/go/protocol/keybase1" 20 "github.com/keybase/clockwork" 21 ) 22 23 const ( 24 bgLoaderMaxAttempts = 10 25 bgLoaderInitDelay = 100 * time.Millisecond 26 bgLoaderErrDelay = 300 * time.Millisecond 27 ) 28 29 type clTask struct { 30 job types.ConvLoaderJob 31 attempt int 32 lastAttemptAt time.Time 33 } 34 35 type jobQueue struct { 36 sync.Mutex 37 queue *list.List 38 waitChs []chan struct{} 39 queueMap map[string]bool 40 maxSize int 41 } 42 43 func newJobQueue(maxSize int) *jobQueue { 44 return &jobQueue{ 45 queue: list.New(), 46 queueMap: make(map[string]bool), 47 maxSize: maxSize, 48 } 49 } 50 51 func (j *jobQueue) Wait() <-chan struct{} { 52 j.Lock() 53 defer j.Unlock() 54 if j.queue.Len() == 0 { 55 ch := make(chan struct{}) 56 j.waitChs = append(j.waitChs, ch) 57 return ch 58 } 59 ch := make(chan struct{}) 60 close(ch) 61 return ch 62 } 63 64 func (j *jobQueue) Push(task clTask) (queued bool, err error) { 65 j.Lock() 66 defer j.Unlock() 67 if j.queue.Len() >= j.maxSize { 68 return false, errors.New("job queue full") 69 } 70 defer func() { 71 if !queued { 72 return 73 } 74 // Notify waiters we have some stuff for them now 75 for _, w := range j.waitChs { 76 close(w) 77 } 78 j.waitChs = nil 79 }() 80 if task.job.Uniqueness == types.ConvLoaderGeneric && j.queueMap[task.job.String()] { 81 return false, nil 82 } 83 j.queueMap[task.job.String()] = true 84 for e := j.queue.Front(); e != nil; e = e.Next() { 85 eval := e.Value.(clTask) 86 if task.job.HigherPriorityThan(eval.job) { 87 j.queue.InsertBefore(task, e) 88 return true, nil 89 } 90 } 91 j.queue.PushBack(task) 92 return true, nil 93 } 94 95 func (j *jobQueue) PopFront() (res clTask, ok bool) { 96 j.Lock() 97 defer j.Unlock() 98 if j.queue.Len() == 0 { 99 return res, false 100 } 101 el := j.queue.Front() 102 res = el.Value.(clTask) 103 j.queue.Remove(el) 104 delete(j.queueMap, res.job.String()) 105 return res, true 106 } 107 108 type activeLoad struct { 109 Ctx context.Context 110 CancelFn context.CancelFunc 111 } 112 113 type BackgroundConvLoader struct { 114 globals.Contextified 115 utils.DebugLabeler 116 sync.Mutex 117 118 uid gregor1.UID 119 started bool 120 queue *jobQueue 121 stopCh chan struct{} 122 suspendCh chan chan struct{} 123 resumeCh chan struct{} 124 loadCh chan *clTask 125 identNotifier types.IdentifyNotifier 126 eg errgroup.Group 127 128 clock clockwork.Clock 129 resumeWait time.Duration 130 loadWait time.Duration 131 132 activeLoads map[string]activeLoad 133 suspendCount int 134 135 // for testing, make this and can check conv load successes 136 loads chan chat1.ConversationID 137 testingNameInfoSource types.NameInfoSource 138 appStateCh chan struct{} 139 } 140 141 var _ types.ConvLoader = (*BackgroundConvLoader)(nil) 142 143 func NewBackgroundConvLoader(g *globals.Context) *BackgroundConvLoader { 144 b := &BackgroundConvLoader{ 145 Contextified: globals.NewContextified(g), 146 DebugLabeler: utils.NewDebugLabeler(g.ExternalG(), "BackgroundConvLoader", false), 147 stopCh: make(chan struct{}), 148 suspendCh: make(chan chan struct{}, 10), 149 identNotifier: NewCachingIdentifyNotifier(g), 150 clock: clockwork.NewRealClock(), 151 resumeWait: time.Second, 152 loadWait: time.Second, 153 activeLoads: make(map[string]activeLoad), 154 } 155 b.identNotifier.ResetOnGUIConnect() 156 b.newQueue() 157 go func() { _ = b.monitorAppState() }() 158 159 return b 160 } 161 162 func (b *BackgroundConvLoader) addActiveLoadLocked(al activeLoad) (key string) { 163 key = libkb.RandStringB64(3) 164 b.activeLoads[key] = al 165 return key 166 } 167 168 func (b *BackgroundConvLoader) removeActiveLoadLocked(key string) { 169 delete(b.activeLoads, key) 170 } 171 172 func (b *BackgroundConvLoader) monitorAppState() error { 173 ctx := context.Background() 174 b.Debug(ctx, "monitorAppState: starting up") 175 176 suspended := false 177 state := keybase1.MobileAppState_FOREGROUND 178 for { 179 state = <-b.G().MobileAppState.NextUpdate(&state) 180 switch state { 181 case keybase1.MobileAppState_FOREGROUND, keybase1.MobileAppState_BACKGROUNDACTIVE: 182 b.Debug(ctx, "monitorAppState: active state: %v", state) 183 // Only resume if we had suspended earlier (frontend can spam us with these) 184 if suspended { 185 b.Debug(ctx, "monitorAppState: resuming load thread") 186 b.Resume(ctx) 187 suspended = false 188 } 189 case keybase1.MobileAppState_BACKGROUND: 190 b.Debug(ctx, "monitorAppState: backgrounded, suspending load thread") 191 if !suspended { 192 b.Suspend(ctx) 193 suspended = true 194 } 195 } 196 if b.appStateCh != nil { 197 b.appStateCh <- struct{}{} 198 } 199 } 200 } 201 202 func (b *BackgroundConvLoader) Start(ctx context.Context, uid gregor1.UID) { 203 b.Lock() 204 defer b.Unlock() 205 206 if b.G().GetEnv().GetDisableBgConvLoader() { 207 b.Debug(ctx, "BackgroundConvLoader disabled, aborting Start") 208 return 209 } 210 b.Debug(ctx, "Start") 211 if b.started { 212 close(b.stopCh) 213 b.stopCh = make(chan struct{}) 214 } 215 b.newQueue() 216 b.started = true 217 b.uid = uid 218 b.eg.Go(func() error { return b.loop(uid, b.stopCh) }) 219 b.eg.Go(func() error { return b.loadLoop(uid, b.stopCh) }) 220 } 221 222 func (b *BackgroundConvLoader) Stop(ctx context.Context) chan struct{} { 223 b.Lock() 224 defer b.Unlock() 225 b.Debug(ctx, "Stop") 226 b.cancelActiveLoadsLocked() 227 ch := make(chan struct{}) 228 if b.started { 229 b.started = false 230 close(b.stopCh) 231 b.stopCh = make(chan struct{}) 232 go func() { 233 _ = b.eg.Wait() 234 close(ch) 235 }() 236 } else { 237 close(ch) 238 } 239 return ch 240 } 241 242 type bgOperationKey int 243 244 var bgOpKey bgOperationKey 245 246 func (b *BackgroundConvLoader) makeConvLoaderContext(ctx context.Context) context.Context { 247 return context.WithValue(ctx, bgOpKey, true) 248 } 249 250 func (b *BackgroundConvLoader) isConvLoaderContext(ctx context.Context) bool { 251 val := ctx.Value(bgOpKey) 252 if _, ok := val.(bool); ok { 253 return true 254 } 255 return false 256 } 257 258 func (b *BackgroundConvLoader) setTestingNameInfoSource(ni types.NameInfoSource) { 259 b.Debug(context.TODO(), "setTestingNameInfoSource: setting to %T", ni) 260 b.testingNameInfoSource = ni 261 } 262 263 func (b *BackgroundConvLoader) Queue(ctx context.Context, job types.ConvLoaderJob) error { 264 // allow high priority to be queued even in the bkg loader context. Often times, this is something like 265 // an ephemeral purge which we don't want to block. 266 if job.Priority != types.ConvLoaderPriorityHighest && b.isConvLoaderContext(ctx) { 267 b.Debug(ctx, "Queue: refusing to queue in background loader context: convID: %s", job) 268 return nil 269 } 270 return b.enqueue(ctx, clTask{job: job}) 271 } 272 273 func (b *BackgroundConvLoader) cancelActiveLoadsLocked() (canceled bool) { 274 for _, activeLoad := range b.activeLoads { 275 select { 276 case <-activeLoad.Ctx.Done(): 277 b.Debug(activeLoad.Ctx, "Suspend: active load already canceled") 278 default: 279 b.Debug(activeLoad.Ctx, "Suspend: canceling active load") 280 activeLoad.CancelFn() 281 canceled = true 282 } 283 } 284 return canceled 285 } 286 287 func (b *BackgroundConvLoader) Suspend(ctx context.Context) (canceled bool) { 288 defer b.Trace(ctx, nil, "Suspend")() 289 b.Lock() 290 defer b.Unlock() 291 if !b.started { 292 return false 293 } 294 if b.suspendCount == 0 { 295 b.Debug(ctx, "Suspend: sending on suspendCh") 296 b.resumeCh = make(chan struct{}) 297 select { 298 case b.suspendCh <- b.resumeCh: 299 default: 300 b.Debug(ctx, "Suspend: failed to suspend loop") 301 } 302 } 303 b.suspendCount++ 304 return b.cancelActiveLoadsLocked() 305 } 306 307 func (b *BackgroundConvLoader) Resume(ctx context.Context) bool { 308 defer b.Trace(ctx, nil, "Resume")() 309 b.Lock() 310 defer b.Unlock() 311 if b.suspendCount > 0 { 312 b.suspendCount-- 313 if b.suspendCount == 0 && b.resumeCh != nil { 314 b.Debug(ctx, "Resume: closing resumeCh") 315 close(b.resumeCh) 316 return true 317 } 318 } 319 return false 320 } 321 322 func (b *BackgroundConvLoader) isSuspended() bool { 323 b.Lock() 324 defer b.Unlock() 325 return b.suspendCount > 0 326 } 327 328 func (b *BackgroundConvLoader) isRunning() bool { 329 b.Lock() 330 defer b.Unlock() 331 return b.started 332 } 333 334 func (b *BackgroundConvLoader) enqueue(ctx context.Context, task clTask) error { 335 b.Lock() 336 defer b.Unlock() 337 b.Debug(ctx, "enqueue: adding task: %s", task.job) 338 queued, err := b.queue.Push(task) 339 if err != nil { 340 return err 341 } 342 if !queued { 343 b.Debug(ctx, "enqueue: skipped queueing job: %s", task.job) 344 } 345 return nil 346 } 347 348 func (b *BackgroundConvLoader) loop(uid gregor1.UID, stopCh chan struct{}) error { 349 bgctx := context.Background() 350 b.Debug(bgctx, "loop: starting conv loader loop for %s", uid) 351 352 // waitForResume is called on suspend. It will wait for a resume event, and then pause 353 // for b.resumeWait amount of time. Returns false if the outer loop should shutdown. 354 waitForResume := func(ch chan struct{}) bool { 355 b.Debug(bgctx, "waitForResume: suspending loop") 356 select { 357 case <-ch: 358 case <-stopCh: 359 return false 360 } 361 b.clock.Sleep(libkb.RandomJitter(b.resumeWait)) 362 b.Debug(bgctx, "waitForResume: resuming loop") 363 return true 364 } 365 // On mobile fresh start, apply the foreground wait 366 if b.G().IsMobileAppType() { 367 b.Debug(bgctx, "loop: delaying startup since on mobile") 368 b.clock.Sleep(libkb.RandomJitter(b.resumeWait)) 369 } 370 371 // Main loop 372 for { 373 b.Debug(bgctx, "loop: waiting for job") 374 select { 375 case <-b.queue.Wait(): 376 task, ok := b.queue.PopFront() 377 if !ok { 378 continue 379 } 380 if task.job.ConvID.IsNil() { 381 // means we closed this channel 382 continue 383 } 384 // Wait for a small amount of time before loading, this way we aren't in a tight loop 385 // charging through conversations 386 duration := bgLoaderInitDelay 387 if task.attempt > 0 { 388 duration = bgLoaderErrDelay - time.Since(task.lastAttemptAt) 389 if duration < bgLoaderInitDelay { 390 duration = bgLoaderInitDelay 391 } 392 } 393 // Make sure we aren't suspended (also make sure we don't get shutdown). Charge through if 394 // neither have any data on them. 395 select { 396 case <-b.clock.After(duration): 397 case ch := <-b.suspendCh: 398 b.Debug(bgctx, "loop: pulled queue task, but suspended, so waiting") 399 if !waitForResume(ch) { 400 return nil 401 } 402 } 403 b.Debug(bgctx, "loop: pulled queued task: %s", task.job) 404 select { 405 case b.loadCh <- &task: 406 default: 407 b.Debug(bgctx, "loop: failed to dispatch load, queue full") 408 } 409 case ch := <-b.suspendCh: 410 b.Debug(bgctx, "loop: received suspend") 411 if !waitForResume(ch) { 412 return nil 413 } 414 case <-stopCh: 415 b.Debug(bgctx, "loop: shutting down for %s", uid) 416 return nil 417 } 418 } 419 } 420 421 func (b *BackgroundConvLoader) loadLoop(uid gregor1.UID, stopCh chan struct{}) error { 422 bgctx := context.Background() 423 b.Debug(bgctx, "loadLoop: starting for uid: %s", uid) 424 for { 425 select { 426 case task := <-b.loadCh: 427 switch { 428 case !b.isRunning(): 429 b.Debug(bgctx, "loadLoop: shutting down for %s", uid) 430 return nil 431 case b.isSuspended(): 432 b.Debug(bgctx, "loadLoop: suspended, re-enqueueing task: %s", task.job) 433 if err := b.enqueue(bgctx, *task); err != nil { 434 b.Debug(bgctx, "enqueue error %s", err) 435 } 436 default: 437 b.Debug(bgctx, "loadLoop: running task: %s", task.job) 438 nextTask := b.load(bgctx, *task, uid) 439 if nextTask != nil { 440 if err := b.enqueue(bgctx, *nextTask); err != nil { 441 b.Debug(bgctx, "enqueue error %s", err) 442 } 443 } 444 } 445 b.clock.Sleep(b.loadWait) 446 case <-stopCh: 447 b.Debug(bgctx, "loadLoop: shutting down for %s", uid) 448 return nil 449 } 450 } 451 } 452 453 func (b *BackgroundConvLoader) newQueue() { 454 b.queue = newJobQueue(1000) 455 b.loadCh = make(chan *clTask, 100) 456 } 457 458 func (b *BackgroundConvLoader) retriableError(err error) bool { 459 if IsOfflineError(err) != OfflineErrorKindOnline { 460 return true 461 } 462 if err == context.Canceled { 463 return true 464 } 465 switch err.(type) { 466 case storage.AbortedError: 467 return true 468 default: 469 return false 470 } 471 } 472 473 func (b *BackgroundConvLoader) IsBackgroundActive() bool { 474 b.Lock() 475 defer b.Unlock() 476 return len(b.activeLoads) > 0 477 } 478 479 func (b *BackgroundConvLoader) load(ictx context.Context, task clTask, uid gregor1.UID) *clTask { 480 defer b.Trace(ictx, nil, "load: %s", task.job)() 481 defer b.PerfTrace(ictx, nil, "load: %s", task.job)() 482 b.Lock() 483 var al activeLoad 484 al.Ctx, al.CancelFn = context.WithCancel( 485 globals.ChatCtx(b.makeConvLoaderContext(ictx), b.G(), keybase1.TLFIdentifyBehavior_CHAT_GUI, nil, 486 b.identNotifier)) 487 ctx := al.Ctx 488 alKey := b.addActiveLoadLocked(al) 489 b.Unlock() 490 if b.testingNameInfoSource != nil { 491 ctx = globals.CtxAddOverrideNameInfoSource(ctx, b.testingNameInfoSource) 492 b.Debug(ctx, "setting testing nameinfo source: %T", b.testingNameInfoSource) 493 } 494 defer func() { 495 b.Lock() 496 b.removeActiveLoadLocked(alKey) 497 al.CancelFn() 498 b.Unlock() 499 }() 500 501 job := task.job 502 query := &chat1.GetThreadQuery{MarkAsRead: false} 503 pagination := job.Pagination 504 if pagination == nil { 505 pagination = &chat1.Pagination{Num: 50} 506 } 507 var tv chat1.ThreadView 508 if pagination.Num > 0 { 509 var err error 510 tv, err = b.G().ConvSource.Pull(ctx, job.ConvID, uid, 511 chat1.GetThreadReason_BACKGROUNDCONVLOAD, nil, query, pagination) 512 if err != nil { 513 b.Debug(ctx, "load: ConvSource.Pull error: %s (%T)", err, err) 514 if b.retriableError(err) && task.attempt+1 < bgLoaderMaxAttempts { 515 b.Debug(ctx, "transient error, retrying") 516 task.attempt++ 517 task.lastAttemptAt = time.Now() 518 return &task 519 } 520 b.Debug(ctx, "load: failed to load job: %s", job) 521 return nil 522 } 523 b.Debug(ctx, "load: loaded job: %s", job) 524 } else { 525 b.Debug(ctx, "load: skipped job load because of 0 pagination") 526 } 527 if job.PostLoadHook != nil { 528 b.Debug(ctx, "load: invoking post load hook on job: %s", job) 529 job.PostLoadHook(ctx, tv, job) 530 } 531 532 // if testing, put the convID on the loads channel 533 if b.loads != nil { 534 b.Debug(ctx, "load: putting convID %s on loads chan", job.ConvID) 535 b.loads <- job.ConvID 536 } 537 return nil 538 } 539 540 func newConvLoaderPagebackHook(g *globals.Context, curCalls, maxCalls int) func(ctx context.Context, tv chat1.ThreadView, job types.ConvLoaderJob) { 541 return func(ctx context.Context, tv chat1.ThreadView, job types.ConvLoaderJob) { 542 if curCalls >= maxCalls || tv.Pagination == nil || tv.Pagination.Last { 543 g.GetLog().CDebugf(ctx, "newConvLoaderPagebackHook: bailing out: job: %s curcalls: %d p: %s", 544 job, curCalls, tv.Pagination) 545 return 546 } 547 job.Pagination.Next = tv.Pagination.Next 548 job.Pagination.Previous = nil 549 job.Priority = types.ConvLoaderPriorityLow 550 job.PostLoadHook = newConvLoaderPagebackHook(g, curCalls+1, maxCalls) 551 // Create a new context here so that we don't trip conv loader blocking rule 552 ctx = globals.BackgroundChatCtx(ctx, g) 553 if err := g.ConvLoader.Queue(ctx, job); err != nil { 554 g.GetLog().CDebugf(ctx, "newConvLoaderPagebackHook: failed to queue job: job: %s err: %s", 555 job, err) 556 } 557 } 558 }