github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/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  }