github.com/keybase/client/go@v0.0.0-20241007131713-f10651d043c8/chat/search/indexer.go (about) 1 package search 2 3 import ( 4 "context" 5 "fmt" 6 "sort" 7 "sync" 8 "time" 9 10 "github.com/keybase/client/go/chat/globals" 11 "github.com/keybase/client/go/chat/types" 12 "github.com/keybase/client/go/chat/utils" 13 "github.com/keybase/client/go/libkb" 14 "github.com/keybase/client/go/protocol/chat1" 15 "github.com/keybase/client/go/protocol/gregor1" 16 "github.com/keybase/client/go/protocol/keybase1" 17 "github.com/keybase/clockwork" 18 "golang.org/x/sync/errgroup" 19 ) 20 21 // If a conversation doesn't meet the minimum requirements, don't update the 22 // index realtime. The priority score emphasizes how much of the conversation 23 // is read, a prerequisite for searching. 24 const minPriorityScore = 10 25 26 type storageAdd struct { 27 ctx context.Context 28 convID chat1.ConversationID 29 msgs []chat1.MessageUnboxed 30 cb chan struct{} 31 } 32 33 type storageRemove struct { 34 ctx context.Context 35 convID chat1.ConversationID 36 msgs []chat1.MessageUnboxed 37 cb chan struct{} 38 } 39 40 type Indexer struct { 41 globals.Contextified 42 utils.DebugLabeler 43 sync.Mutex 44 45 // encrypted on-disk storage 46 store *store 47 pageSize int 48 stopCh chan struct{} 49 suspendCh chan chan struct{} 50 resumeCh chan struct{} 51 suspendCount int 52 resumeWait time.Duration 53 started bool 54 clock clockwork.Clock 55 eg errgroup.Group 56 uid gregor1.UID 57 storageCh chan interface{} 58 59 maxSyncConvs int 60 startSyncDelay time.Duration 61 selectiveSyncActiveMu sync.Mutex 62 selectiveSyncActive bool 63 flushDelay time.Duration 64 65 // for testing 66 consumeCh chan chat1.ConversationID 67 reindexCh chan chat1.ConversationID 68 syncLoopCh, cancelSyncCh, pokeSyncCh chan struct{} 69 } 70 71 var _ types.Indexer = (*Indexer)(nil) 72 73 func NewIndexer(g *globals.Context) *Indexer { 74 idx := &Indexer{ 75 Contextified: globals.NewContextified(g), 76 DebugLabeler: utils.NewDebugLabeler(g.ExternalG(), "Search.Indexer", false), 77 pageSize: defaultPageSize, 78 suspendCh: make(chan chan struct{}, 10), 79 resumeWait: time.Second, 80 cancelSyncCh: make(chan struct{}, 100), 81 pokeSyncCh: make(chan struct{}, 100), 82 clock: clockwork.NewRealClock(), 83 flushDelay: 15 * time.Second, 84 storageCh: make(chan interface{}, 100), 85 } 86 switch idx.G().GetAppType() { 87 case libkb.MobileAppType: 88 idx.SetMaxSyncConvs(maxSyncConvsMobile) 89 idx.startSyncDelay = startSyncDelayMobile 90 default: 91 idx.startSyncDelay = startSyncDelayDesktop 92 idx.SetMaxSyncConvs(maxSyncConvsDesktop) 93 } 94 return idx 95 } 96 97 func (idx *Indexer) SetStartSyncDelay(d time.Duration) { 98 idx.startSyncDelay = d 99 } 100 101 func (idx *Indexer) SetMaxSyncConvs(x int) { 102 idx.maxSyncConvs = x 103 } 104 105 func (idx *Indexer) SetPageSize(pageSize int) { 106 idx.pageSize = pageSize 107 } 108 109 func (idx *Indexer) SetConsumeCh(ch chan chat1.ConversationID) { 110 idx.consumeCh = ch 111 } 112 113 func (idx *Indexer) SetReindexCh(ch chan chat1.ConversationID) { 114 idx.reindexCh = ch 115 } 116 117 func (idx *Indexer) SetSyncLoopCh(ch chan struct{}) { 118 idx.syncLoopCh = ch 119 } 120 121 func (idx *Indexer) SetUID(uid gregor1.UID) { 122 idx.uid = uid 123 idx.store = newStore(idx.G(), uid) 124 } 125 126 func (idx *Indexer) StartFlushLoop() { 127 idx.Lock() 128 defer idx.Unlock() 129 if !idx.started { 130 idx.started = true 131 idx.stopCh = make(chan struct{}) 132 } 133 idx.eg.Go(func() error { return idx.flushLoop(idx.stopCh) }) 134 } 135 136 func (idx *Indexer) StartStorageLoop() { 137 idx.Lock() 138 defer idx.Unlock() 139 if !idx.started { 140 idx.started = true 141 idx.stopCh = make(chan struct{}) 142 } 143 idx.eg.Go(func() error { return idx.storageLoop(idx.stopCh) }) 144 } 145 146 func (idx *Indexer) StartSyncLoop() { 147 idx.Lock() 148 defer idx.Unlock() 149 if !idx.started { 150 idx.started = true 151 idx.stopCh = make(chan struct{}) 152 } 153 idx.eg.Go(func() error { return idx.SyncLoop(idx.stopCh) }) 154 } 155 156 func (idx *Indexer) SetFlushDelay(dur time.Duration) { 157 idx.flushDelay = dur 158 } 159 160 func (idx *Indexer) Start(ctx context.Context, uid gregor1.UID) { 161 defer idx.Trace(ctx, nil, "Start")() 162 idx.Lock() 163 defer idx.Unlock() 164 if idx.started { 165 return 166 } 167 idx.uid = uid 168 idx.store = newStore(idx.G(), uid) 169 idx.started = true 170 idx.stopCh = make(chan struct{}) 171 if !idx.G().IsMobileAppType() && !idx.G().GetEnv().GetDisableSearchIndexer() { 172 idx.eg.Go(func() error { return idx.SyncLoop(idx.stopCh) }) 173 } 174 idx.eg.Go(func() error { return idx.flushLoop(idx.stopCh) }) 175 idx.eg.Go(func() error { return idx.storageLoop(idx.stopCh) }) 176 } 177 178 func (idx *Indexer) CancelSync(ctx context.Context) { 179 idx.Debug(ctx, "CancelSync") 180 select { 181 case <-ctx.Done(): 182 case idx.cancelSyncCh <- struct{}{}: 183 default: 184 } 185 } 186 187 func (idx *Indexer) PokeSync(ctx context.Context) { 188 idx.Debug(ctx, "PokeSync") 189 select { 190 case <-ctx.Done(): 191 case idx.pokeSyncCh <- struct{}{}: 192 default: 193 } 194 } 195 196 func (idx *Indexer) SyncLoop(stopCh chan struct{}) error { 197 ctx := globals.ChatCtx(context.Background(), idx.G(), keybase1.TLFIdentifyBehavior_CHAT_GUI, nil, nil) 198 idx.Lock() 199 suspendCh := idx.suspendCh 200 idx.Unlock() 201 idx.Debug(ctx, "starting SelectiveSync bg loop") 202 203 ticker := libkb.NewBgTicker(time.Hour) 204 after := time.After(idx.startSyncDelay) 205 appState := keybase1.MobileAppState_FOREGROUND 206 netState := keybase1.MobileNetworkState_WIFI 207 var cancelFn context.CancelFunc 208 var l sync.Mutex 209 cancelSync := func() { 210 l.Lock() 211 defer l.Unlock() 212 if cancelFn != nil { 213 cancelFn() 214 cancelFn = nil 215 } 216 } 217 attemptSync := func(ctx context.Context) { 218 if netState.IsLimited() { 219 return 220 } 221 l.Lock() 222 defer l.Unlock() 223 if cancelFn != nil { 224 cancelFn() 225 } 226 ctx, cancelFn = context.WithCancel(ctx) 227 go func() { 228 idx.Debug(ctx, "running SelectiveSync") 229 if err := idx.SelectiveSync(ctx); err != nil { 230 idx.Debug(ctx, "unable to complete SelectiveSync: %v", err) 231 if idx.syncLoopCh != nil { 232 idx.syncLoopCh <- struct{}{} 233 } 234 } 235 l.Lock() 236 defer l.Unlock() 237 if cancelFn != nil { 238 cancelFn() 239 cancelFn = nil 240 } 241 }() 242 } 243 244 stopSync := func(ctx context.Context) { 245 idx.Debug(ctx, "stopping SelectiveSync bg loop") 246 cancelSync() 247 ticker.Stop() 248 } 249 defer func() { 250 idx.Debug(ctx, "shutting down SyncLoop") 251 }() 252 for { 253 select { 254 case <-idx.cancelSyncCh: 255 cancelSync() 256 case <-idx.pokeSyncCh: 257 attemptSync(ctx) 258 case <-after: 259 attemptSync(ctx) 260 case <-ticker.C: 261 attemptSync(ctx) 262 case appState = <-idx.G().MobileAppState.NextUpdate(&appState): 263 switch appState { 264 case keybase1.MobileAppState_FOREGROUND: 265 // if we enter any state besides foreground cancel any running syncs 266 default: 267 cancelSync() 268 } 269 case netState = <-idx.G().MobileNetState.NextUpdate(&netState): 270 if netState.IsLimited() { 271 // if we switch off of wifi cancel any running syncs 272 cancelSync() 273 } 274 case ch := <-suspendCh: 275 cancelSync() 276 // block until we are told to resume or stop. 277 select { 278 case <-ch: 279 time.Sleep(libkb.RandomJitter(idx.resumeWait)) 280 case <-idx.stopCh: 281 stopSync(ctx) 282 return nil 283 } 284 case <-stopCh: 285 stopSync(ctx) 286 return nil 287 } 288 } 289 } 290 291 func (idx *Indexer) Stop(ctx context.Context) chan struct{} { 292 defer idx.Trace(ctx, nil, "Stop")() 293 idx.Lock() 294 defer idx.Unlock() 295 ch := make(chan struct{}) 296 if idx.started { 297 idx.store.ClearMemory() 298 idx.started = false 299 close(idx.stopCh) 300 go func() { 301 idx.Debug(context.Background(), "Stop: waiting for shutdown") 302 _ = idx.eg.Wait() 303 idx.Debug(context.Background(), "Stop: shutdown complete") 304 close(ch) 305 }() 306 } else { 307 close(ch) 308 } 309 return ch 310 } 311 312 func (idx *Indexer) Suspend(ctx context.Context) bool { 313 defer idx.Trace(ctx, nil, "Suspend")() 314 idx.Lock() 315 defer idx.Unlock() 316 if !idx.started { 317 return false 318 } 319 if idx.suspendCount == 0 { 320 idx.Debug(ctx, "Suspend: sending on suspendCh") 321 idx.resumeCh = make(chan struct{}) 322 select { 323 case idx.suspendCh <- idx.resumeCh: 324 default: 325 idx.Debug(ctx, "Suspend: failed to suspend loop") 326 } 327 } 328 idx.suspendCount++ 329 return true 330 } 331 332 func (idx *Indexer) Resume(ctx context.Context) bool { 333 defer idx.Trace(ctx, nil, "Resume")() 334 idx.Lock() 335 defer idx.Unlock() 336 if idx.suspendCount > 0 { 337 idx.suspendCount-- 338 if idx.suspendCount == 0 && idx.resumeCh != nil { 339 close(idx.resumeCh) 340 return true 341 } 342 } 343 return false 344 } 345 346 // validBatch verifies the topic type is CHAT 347 func (idx *Indexer) validBatch(msgs []chat1.MessageUnboxed) bool { 348 if len(msgs) == 0 { 349 return false 350 } 351 352 for _, msg := range msgs { 353 switch msg.GetTopicType() { 354 case chat1.TopicType_CHAT: 355 return true 356 case chat1.TopicType_NONE: 357 continue 358 default: 359 return false 360 } 361 } 362 // if we only have TopicType_NONE, assume it's ok to return true so we 363 // document the seen ids properly. 364 return true 365 } 366 367 func (idx *Indexer) consumeResultsForTest(convID chat1.ConversationID, err error) { 368 if err == nil && idx.consumeCh != nil { 369 idx.consumeCh <- convID 370 } 371 } 372 373 func (idx *Indexer) storageDispatch(op interface{}) { 374 select { 375 case idx.storageCh <- op: 376 default: 377 idx.Debug(context.Background(), "storageDispatch: failed to dispatch storage operation") 378 } 379 } 380 381 func (idx *Indexer) storageLoop(stopCh chan struct{}) error { 382 ctx := context.Background() 383 idx.Debug(ctx, "storageLoop: starting") 384 for { 385 select { 386 case <-stopCh: 387 idx.Debug(ctx, "storageLoop: shutting down") 388 return nil 389 case iop := <-idx.storageCh: 390 switch op := iop.(type) { 391 case storageAdd: 392 err := idx.store.Add(op.ctx, op.convID, op.msgs) 393 if err != nil { 394 idx.Debug(op.ctx, "storageLoop: add failed: %s", err) 395 } 396 idx.consumeResultsForTest(op.convID, err) 397 close(op.cb) 398 case storageRemove: 399 err := idx.store.Remove(op.ctx, op.convID, op.msgs) 400 if err != nil { 401 idx.Debug(op.ctx, "storageLoop: remove failed: %s", err) 402 } 403 idx.consumeResultsForTest(op.convID, err) 404 close(op.cb) 405 } 406 } 407 } 408 } 409 410 func (idx *Indexer) flushLoop(stopCh chan struct{}) error { 411 ctx := context.Background() 412 idx.Debug(ctx, "flushLoop: starting") 413 for { 414 select { 415 case <-stopCh: 416 idx.Debug(ctx, "flushLoop: shutting down") 417 return nil 418 case <-idx.clock.After(idx.flushDelay): 419 if err := idx.store.Flush(); err != nil { 420 idx.Debug(ctx, "flushLoop: failed to flush: %s", err) 421 } 422 } 423 } 424 } 425 426 func (idx *Indexer) hasPriority(ctx context.Context, convID chat1.ConversationID) bool { 427 conv, err := utils.GetUnverifiedConv(ctx, idx.G(), idx.uid, convID, types.InboxSourceDataSourceLocalOnly) 428 if err != nil { 429 idx.Debug(ctx, "unable to fetch GetUnverifiedConv, continuing: %v", err) 430 return true 431 } else if score := utils.GetConvPriorityScore(conv); score < minPriorityScore { 432 idx.Debug(ctx, "%s does not meet minPriorityScore (%.2f < %d), aborting.", 433 utils.GetRemoteConvDisplayName(conv), score, minPriorityScore) 434 return false 435 } 436 return true 437 } 438 439 func (idx *Indexer) Add(ctx context.Context, convID chat1.ConversationID, 440 msgs []chat1.MessageUnboxed) (err error) { 441 idx.Lock() 442 if !idx.started { 443 idx.Unlock() 444 return nil 445 } 446 idx.Unlock() 447 _, err = idx.add(ctx, convID, msgs, false) 448 return err 449 } 450 451 func (idx *Indexer) add(ctx context.Context, convID chat1.ConversationID, 452 msgs []chat1.MessageUnboxed, force bool) (cb chan struct{}, err error) { 453 cb = make(chan struct{}) 454 if idx.G().GetEnv().GetDisableSearchIndexer() { 455 close(cb) 456 return cb, nil 457 } 458 if !idx.validBatch(msgs) { 459 close(cb) 460 return cb, nil 461 } 462 if !(force || idx.hasPriority(ctx, convID)) { 463 close(cb) 464 return cb, nil 465 } 466 467 defer idx.Trace(ctx, &err, 468 fmt.Sprintf("Indexer.Add conv: %v, msgs: %d, force: %v", 469 convID, len(msgs), force))() 470 idx.storageDispatch(storageAdd{ 471 ctx: globals.BackgroundChatCtx(ctx, idx.G()), 472 convID: convID, 473 msgs: msgs, 474 cb: cb, 475 }) 476 return cb, nil 477 } 478 479 func (idx *Indexer) Remove(ctx context.Context, convID chat1.ConversationID, 480 msgs []chat1.MessageUnboxed) (err error) { 481 idx.Lock() 482 if !idx.started { 483 idx.Unlock() 484 return nil 485 } 486 idx.Unlock() 487 _, err = idx.remove(ctx, convID, msgs, false) 488 return err 489 } 490 491 func (idx *Indexer) remove(ctx context.Context, convID chat1.ConversationID, 492 msgs []chat1.MessageUnboxed, force bool) (cb chan struct{}, err error) { 493 cb = make(chan struct{}) 494 if idx.G().GetEnv().GetDisableSearchIndexer() { 495 close(cb) 496 return cb, nil 497 } 498 if !idx.validBatch(msgs) { 499 close(cb) 500 return cb, nil 501 } 502 if !(force || idx.hasPriority(ctx, convID)) { 503 close(cb) 504 return cb, nil 505 } 506 507 defer idx.Trace(ctx, &err, 508 fmt.Sprintf("Indexer.Remove conv: %v, msgs: %d, force: %v", 509 convID, len(msgs), force))() 510 idx.storageDispatch(storageRemove{ 511 ctx: globals.BackgroundChatCtx(ctx, idx.G()), 512 convID: convID, 513 msgs: msgs, 514 cb: cb, 515 }) 516 return cb, nil 517 } 518 519 // reindexConv attempts to fill in any missing messages from the index. For a 520 // small number of messages we use the GetMessages api to fill in the holes. If 521 // our index is missing many messages, we page through and add batches of 522 // missing messages. 523 func (idx *Indexer) reindexConv(ctx context.Context, rconv types.RemoteConversation, 524 numJobs int, inboxIndexStatus *inboxIndexStatus) (completedJobs int, err error) { 525 conv := rconv.Conv 526 convID := conv.GetConvID() 527 md, err := idx.store.GetMetadata(ctx, convID) 528 if err != nil { 529 return 0, err 530 } 531 missingIDs := md.MissingIDForConv(conv) 532 if len(missingIDs) == 0 { 533 return 0, nil 534 } 535 minIdxID := missingIDs[0] 536 maxIdxID := missingIDs[len(missingIDs)-1] 537 538 defer idx.Trace(ctx, &err, 539 fmt.Sprintf("Indexer.reindex: conv: %v, minID: %v, maxID: %v, numMissing: %v", 540 utils.GetRemoteConvDisplayName(rconv), minIdxID, maxIdxID, len(missingIDs)))() 541 542 reason := chat1.GetThreadReason_INDEXED_SEARCH 543 if len(missingIDs) < idx.pageSize { 544 msgs, err := idx.G().ConvSource.GetMessages(ctx, rconv.GetConvID(), idx.uid, missingIDs, &reason, 545 nil, false) 546 if err != nil { 547 if utils.IsPermanentErr(err) { 548 return 0, err 549 } 550 return 0, nil 551 } 552 cb, err := idx.add(ctx, convID, msgs, true) 553 if err != nil { 554 return 0, err 555 } 556 <-cb 557 completedJobs++ 558 } else { 559 query := &chat1.GetThreadQuery{ 560 DisablePostProcessThread: true, 561 MarkAsRead: false, 562 } 563 for i := minIdxID; i < maxIdxID; i += chat1.MessageID(idx.pageSize) { 564 select { 565 case <-ctx.Done(): 566 return 0, ctx.Err() 567 default: 568 } 569 pagination := utils.MessageIDControlToPagination(ctx, idx.DebugLabeler, &chat1.MessageIDControl{ 570 Num: idx.pageSize, 571 Pivot: &i, 572 Mode: chat1.MessageIDControlMode_NEWERMESSAGES, 573 }, nil) 574 tv, err := idx.G().ConvSource.Pull(ctx, convID, idx.uid, reason, nil, query, pagination) 575 if err != nil { 576 if utils.IsPermanentErr(err) { 577 return 0, err 578 } 579 continue 580 } 581 cb, err := idx.add(ctx, convID, tv.Messages, true) 582 if err != nil { 583 return 0, err 584 } 585 <-cb 586 completedJobs++ 587 if numJobs > 0 && completedJobs >= numJobs { 588 break 589 } 590 if inboxIndexStatus != nil { 591 md, err := idx.store.GetMetadata(ctx, conv.GetConvID()) 592 if err != nil { 593 idx.Debug(ctx, "updateInboxIndex: unable to GetMetadata %v", err) 594 continue 595 } 596 inboxIndexStatus.addConv(md, conv) 597 percentIndexed, err := inboxIndexStatus.updateUI(ctx) 598 if err != nil { 599 idx.Debug(ctx, "unable to update ui %v", err) 600 } else { 601 idx.Debug(ctx, "%v is %d%% indexed, inbox is %d%% indexed", 602 utils.GetRemoteConvDisplayName(rconv), md.PercentIndexed(conv), percentIndexed) 603 } 604 } 605 } 606 } 607 if idx.reindexCh != nil { 608 idx.reindexCh <- convID 609 } 610 return completedJobs, nil 611 } 612 613 func (idx *Indexer) SearchableConvs(ctx context.Context, convID *chat1.ConversationID) (res []types.RemoteConversation, err error) { 614 convMap, err := idx.allConvs(ctx, convID) 615 if err != nil { 616 return res, err 617 } 618 return idx.convsPrioritySorted(ctx, convMap), nil 619 } 620 621 func (idx *Indexer) allConvs(ctx context.Context, convID *chat1.ConversationID) (map[chat1.ConvIDStr]types.RemoteConversation, error) { 622 // Find all conversations in our inbox 623 topicType := chat1.TopicType_CHAT 624 inboxQuery := &chat1.GetInboxQuery{ 625 ConvID: convID, 626 ComputeActiveList: false, 627 TopicType: &topicType, 628 Status: []chat1.ConversationStatus{ 629 chat1.ConversationStatus_UNFILED, 630 chat1.ConversationStatus_FAVORITE, 631 chat1.ConversationStatus_MUTED, 632 }, 633 MemberStatus: []chat1.ConversationMemberStatus{ 634 chat1.ConversationMemberStatus_ACTIVE, 635 chat1.ConversationMemberStatus_PREVIEW, 636 }, 637 SkipBgLoads: true, 638 } 639 select { 640 case <-ctx.Done(): 641 return nil, ctx.Err() 642 default: 643 } 644 inbox, err := idx.G().InboxSource.ReadUnverified(ctx, idx.uid, types.InboxSourceDataSourceAll, 645 inboxQuery) 646 if err != nil { 647 return nil, err 648 } 649 650 // convID -> remoteConv 651 convMap := make(map[chat1.ConvIDStr]types.RemoteConversation, len(inbox.ConvsUnverified)) 652 for _, conv := range inbox.ConvsUnverified { 653 if conv.Conv.GetFinalizeInfo() != nil { 654 continue 655 } 656 // Don't index any conversation if we are a RESTRICTEDBOT member, 657 // we won't have full access to the messages. We use 658 // UntrustedTeamRole here since the server could just deny serving 659 // us instead of lying about the role. 660 if conv.Conv.ReaderInfo != nil && conv.Conv.ReaderInfo.UntrustedTeamRole == keybase1.TeamRole_RESTRICTEDBOT { 661 continue 662 } 663 convMap[conv.ConvIDStr] = conv 664 } 665 return convMap, nil 666 } 667 668 func (idx *Indexer) convsPrioritySorted(ctx context.Context, 669 convMap map[chat1.ConvIDStr]types.RemoteConversation) (res []types.RemoteConversation) { 670 res = make([]types.RemoteConversation, len(convMap)) 671 index := 0 672 for _, conv := range convMap { 673 res[index] = conv 674 index++ 675 } 676 sort.Slice(res, func(i, j int) bool { 677 return utils.GetConvPriorityScore(convMap[res[i].ConvIDStr]) >= utils.GetConvPriorityScore(convMap[res[j].ConvIDStr]) 678 }) 679 return res 680 } 681 682 // Search tokenizes the given query and finds the intersection of all matches 683 // for each token, returning matches. 684 func (idx *Indexer) Search(ctx context.Context, query, origQuery string, 685 opts chat1.SearchOpts, hitUICh chan chat1.ChatSearchInboxHit, indexUICh chan chat1.ChatSearchIndexStatus) (res *chat1.ChatSearchInboxResults, err error) { 686 defer idx.Trace(ctx, &err, "Indexer.Search")() 687 defer func() { 688 // get a selective sync to run after the search completes even if we 689 // errored. 690 idx.PokeSync(ctx) 691 692 if hitUICh != nil { 693 close(hitUICh) 694 } 695 if indexUICh != nil { 696 close(indexUICh) 697 } 698 }() 699 if idx.G().GetEnv().GetDisableSearchIndexer() { 700 idx.Debug(ctx, "Search: Search indexer is disabled, results will be inaccurate.") 701 } 702 703 idx.CancelSync(ctx) 704 sess := newSearchSession(query, origQuery, idx.uid, hitUICh, indexUICh, idx, opts) 705 return sess.run(ctx) 706 } 707 708 func (idx *Indexer) IsBackgroundActive() bool { 709 idx.selectiveSyncActiveMu.Lock() 710 defer idx.selectiveSyncActiveMu.Unlock() 711 return idx.selectiveSyncActive 712 } 713 714 func (idx *Indexer) setSelectiveSyncActive(val bool) { 715 idx.selectiveSyncActiveMu.Lock() 716 defer idx.selectiveSyncActiveMu.Unlock() 717 idx.selectiveSyncActive = val 718 } 719 720 // SelectiveSync queues up a small number of jobs on the background loader 721 // periodically so our index can cover all conversations. The number of jobs 722 // varies between desktop and mobile so mobile can be more conservative. 723 func (idx *Indexer) SelectiveSync(ctx context.Context) (err error) { 724 defer idx.Trace(ctx, &err, "SelectiveSync")() 725 defer idx.PerfTrace(ctx, &err, "SelectiveSync")() 726 idx.setSelectiveSyncActive(true) 727 defer func() { idx.setSelectiveSyncActive(false) }() 728 729 convMap, err := idx.allConvs(ctx, nil) 730 if err != nil { 731 return err 732 } 733 734 // make sure the most recently read convs are fully indexed 735 convs := idx.convsPrioritySorted(ctx, convMap) 736 // number of batches of messages to fetch in total 737 numJobs := idx.maxSyncConvs 738 for _, conv := range convs { 739 select { 740 case <-ctx.Done(): 741 return ctx.Err() 742 default: 743 } 744 convID := conv.GetConvID() 745 md, err := idx.store.GetMetadata(ctx, convID) 746 if err != nil { 747 idx.Debug(ctx, "SelectiveSync: Unable to get md for conv: %v, %v", convID, err) 748 continue 749 } 750 if md.FullyIndexed(conv.Conv) { 751 continue 752 } 753 754 completedJobs, err := idx.reindexConv(ctx, conv, numJobs, nil) 755 if err != nil { 756 idx.Debug(ctx, "Unable to reindex conv: %v, %v", convID, err) 757 continue 758 } else if completedJobs == 0 { 759 continue 760 } 761 idx.Debug(ctx, "SelectiveSync: Indexed completed jobs %d", completedJobs) 762 numJobs -= completedJobs 763 if numJobs <= 0 { 764 break 765 } 766 } 767 return nil 768 } 769 770 // IndexInbox is only exposed in devel for debugging/profiling the indexing 771 // process. 772 func (idx *Indexer) IndexInbox(ctx context.Context) (res map[chat1.ConvIDStr]chat1.ProfileSearchConvStats, err error) { 773 defer idx.Trace(ctx, &err, "Indexer.IndexInbox")() 774 775 convMap, err := idx.allConvs(ctx, nil) 776 if err != nil { 777 return nil, err 778 } 779 // convID -> stats 780 res = map[chat1.ConvIDStr]chat1.ProfileSearchConvStats{} 781 for convIDStr, conv := range convMap { 782 idx.G().Log.CDebugf(ctx, "Indexing conv: %v", utils.GetRemoteConvDisplayName(conv)) 783 convStats, err := idx.indexConvWithProfile(ctx, conv) 784 if err != nil { 785 idx.G().Log.CDebugf(ctx, "Indexing errored for conv: %v, %v", 786 utils.GetRemoteConvDisplayName(conv), err) 787 } else { 788 idx.G().Log.CDebugf(ctx, "Indexing completed for conv: %v, stats: %+v", 789 utils.GetRemoteConvDisplayName(conv), convStats) 790 } 791 res[convIDStr] = convStats 792 } 793 return res, nil 794 } 795 796 func (idx *Indexer) indexConvWithProfile(ctx context.Context, conv types.RemoteConversation) (res chat1.ProfileSearchConvStats, err error) { 797 defer idx.Trace(ctx, &err, "Indexer.indexConvWithProfile")() 798 md, err := idx.store.GetMetadata(ctx, conv.GetConvID()) 799 if err != nil { 800 return res, err 801 } 802 defer func() { 803 res.ConvName = utils.GetRemoteConvDisplayName(conv) 804 if md != nil { 805 min, max := MinMaxIDs(conv.Conv) 806 res.MinConvID = min 807 res.MaxConvID = max 808 res.NumMissing = len(md.MissingIDForConv(conv.Conv)) 809 res.NumMessages = len(md.SeenIDs) 810 res.PercentIndexed = md.PercentIndexed(conv.Conv) 811 } 812 if err != nil { 813 814 res.Err = err.Error() 815 } 816 }() 817 818 startT := time.Now() 819 _, err = idx.reindexConv(ctx, conv, 0, nil) 820 if err != nil { 821 return res, err 822 } 823 res.DurationMsec = gregor1.ToDurationMsec(time.Since(startT)) 824 dbKey := metadataKey(idx.uid, conv.GetConvID()) 825 b, _, err := idx.G().LocalChatDb.GetRaw(dbKey) 826 if err != nil { 827 return res, err 828 } 829 res.IndexSizeDisk = len(b) 830 res.IndexSizeMem = md.Size() 831 return res, nil 832 } 833 834 func (idx *Indexer) FullyIndexed(ctx context.Context, convID chat1.ConversationID) (res bool, err error) { 835 defer idx.Trace(ctx, &err, "Indexer.FullyIndexed")() 836 conv, err := utils.GetUnverifiedConv(ctx, idx.G(), idx.uid, convID, types.InboxSourceDataSourceAll) 837 if err != nil { 838 return false, err 839 } 840 md, err := idx.store.GetMetadata(ctx, convID) 841 if err != nil { 842 return false, err 843 } 844 return md.FullyIndexed(conv.Conv), nil 845 } 846 847 func (idx *Indexer) PercentIndexed(ctx context.Context, convID chat1.ConversationID) (res int, err error) { 848 defer idx.Trace(ctx, &err, "Indexer.PercentIndexed")() 849 conv, err := utils.GetUnverifiedConv(ctx, idx.G(), idx.uid, convID, types.InboxSourceDataSourceAll) 850 if err != nil { 851 return 0, err 852 } 853 md, err := idx.store.GetMetadata(ctx, convID) 854 if err != nil { 855 return 0, err 856 } 857 return md.PercentIndexed(conv.Conv), nil 858 } 859 860 func (idx *Indexer) Clear(ctx context.Context, uid gregor1.UID, convID chat1.ConversationID) (err error) { 861 defer idx.Trace(ctx, &err, fmt.Sprintf("Indexer.Clear uid: %v convID: %v", uid, convID))() 862 idx.Lock() 863 defer idx.Unlock() 864 return idx.store.Clear(ctx, uid, convID) 865 } 866 867 func (idx *Indexer) OnDbNuke(mctx libkb.MetaContext) (err error) { 868 defer idx.Trace(mctx.Ctx(), &err, "Indexer.OnDbNuke")() 869 idx.Lock() 870 defer idx.Unlock() 871 if !idx.started { 872 return nil 873 } 874 idx.store.ClearMemory() 875 return nil 876 } 877 878 func (idx *Indexer) GetStoreHits(ctx context.Context, uid gregor1.UID, convID chat1.ConversationID, 879 query string) (res map[chat1.MessageID]chat1.EmptyStruct, err error) { 880 return idx.store.GetHits(ctx, convID, query) 881 }