github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/kbfs/libkbfs/online_status_tracker.go (about)

     1  // Copyright 2019 Keybase Inc. All rights reserved.
     2  // Use of this source code is governed by a BSD
     3  // license that can be found in the LICENSE file.
     4  
     5  package libkbfs
     6  
     7  import (
     8  	"fmt"
     9  	"sync"
    10  	"time"
    11  
    12  	"github.com/keybase/client/go/libkb"
    13  	"github.com/keybase/client/go/protocol/keybase1"
    14  	"golang.org/x/net/context"
    15  )
    16  
    17  type onlineStatusTracker struct {
    18  	cancel   func()
    19  	config   Config
    20  	onChange func()
    21  	vlog     *libkb.VDebugLog
    22  
    23  	lock          sync.RWMutex
    24  	currentStatus keybase1.KbfsOnlineStatus
    25  	userIsLooking map[string]bool
    26  
    27  	userIn  chan struct{}
    28  	userOut chan struct{}
    29  
    30  	wg *sync.WaitGroup
    31  }
    32  
    33  const ostTryingStateTimeout = 4 * time.Second
    34  
    35  type ostState int
    36  
    37  const (
    38  	_ ostState = iota
    39  	// We are connected to the mdserver, and user is looking at the Fs tab.
    40  	ostOnlineUserIn
    41  	// We are connected to the mdserver, and user is not looking at the Fs tab.
    42  	ostOnlineUserOut
    43  	// User is looking at the Fs tab. We are not connected to the mdserver, but
    44  	// we are showing a "trying" state in GUI. This usually lasts for
    45  	// ostTryingStateTimeout.
    46  	ostTryingUserIn
    47  	// User is not looking at the Fs tab. We are not connected to the mdserver,
    48  	// but we are telling GUI a "trying" state.
    49  	ostTryingUserOut
    50  	// User is looking at the Fs tab. We are disconnected from the mdserver and
    51  	// are telling GUI so.
    52  	ostOfflineUserIn
    53  	// User is not looking at the Fs tab. We are disconnected from the mdserver
    54  	// and are telling GUI so.
    55  	//
    56  	// Note that we can only go to ostOfflineUserOut from ostOfflineUserIn, but
    57  	// not from any other state. This is because when user is out we don't fast
    58  	// forward. Even if user has got good connection, we might still show as
    59  	// offline until user navigates into the Fs tab which triggers a fast
    60  	// forward and get us connected. If we were to show this state, user would
    61  	// see an offline screen flash for a second before actually getting
    62  	// connected every time they come back to the Fs tab with a previous bad
    63  	// (or lack of) connection, or even from backgrounded app.  So instead, in
    64  	// this case we just use the trying state which shows a slim (less
    65  	// invasive) banner saying we are trying to reconnect.  On the other hand,
    66  	// if user has seen the transition into offline, and user has remained
    67  	// disconnected, it'd be weird for them to see a "trying" state every time
    68  	// they switch away and back into the Fs tab. So in this case just keep the
    69  	// offline state, which is what ostOfflineUserOut is for.
    70  	ostOfflineUserOut
    71  )
    72  
    73  func (s ostState) String() string {
    74  	switch s {
    75  	case ostOnlineUserIn:
    76  		return "online-userIn"
    77  	case ostOnlineUserOut:
    78  		return "online-userOut"
    79  	case ostTryingUserIn:
    80  		return "trying-userIn"
    81  	case ostTryingUserOut:
    82  		return "trying-userOut"
    83  	case ostOfflineUserIn:
    84  		return "offline-userIn"
    85  	case ostOfflineUserOut:
    86  		return "offline-userOut"
    87  	default:
    88  		panic("unknown state")
    89  	}
    90  }
    91  
    92  func (s ostState) getOnlineStatus() keybase1.KbfsOnlineStatus {
    93  	switch s {
    94  	case ostOnlineUserIn:
    95  		return keybase1.KbfsOnlineStatus_ONLINE
    96  	case ostOnlineUserOut:
    97  		return keybase1.KbfsOnlineStatus_ONLINE
    98  	case ostTryingUserIn:
    99  		return keybase1.KbfsOnlineStatus_TRYING
   100  	case ostTryingUserOut:
   101  		return keybase1.KbfsOnlineStatus_TRYING
   102  	case ostOfflineUserIn:
   103  		return keybase1.KbfsOnlineStatus_OFFLINE
   104  	case ostOfflineUserOut:
   105  		return keybase1.KbfsOnlineStatus_OFFLINE
   106  	default:
   107  		panic("unknown state")
   108  	}
   109  }
   110  
   111  // ostSideEffect is a type for side effects that happens as a result of
   112  // transitions happening inside the FSM. These side effects describe what
   113  // should happen, but the FSM doesn't directly do them. The caller of outFsm
   114  // should make sure those actions are carried out.
   115  type ostSideEffect int
   116  
   117  const (
   118  	// ostResetTimer describes a side effect where the timer for transitioning
   119  	// from a "trying" state into a "offline" state should be reset and
   120  	// started.
   121  	ostResetTimer ostSideEffect = iota
   122  	// ostStopTimer describes a side effect where the timer for transitioning
   123  	// from a "trying" state into a "offline" state should be stopped.
   124  	ostStopTimer
   125  	// ostFastForward describes a side effect where we should fast forward the
   126  	// reconnecting backoff timer and attempt to connect to the mdserver right
   127  	// away.
   128  	ostFastForward
   129  )
   130  
   131  func ostFsm(
   132  	ctx context.Context,
   133  	wg *sync.WaitGroup,
   134  	vlog *libkb.VDebugLog,
   135  	initialState ostState,
   136  	// sideEffects carries events about side effects caused by the FSM
   137  	// transitions. Caller should handle these effects and make things actually
   138  	// happen.
   139  	sideEffects chan<- ostSideEffect,
   140  	// onlineStatusUpdates carries a special side effect for the caller to know
   141  	// when the onlineStatus changes.
   142  	onlineStatusUpdates chan<- keybase1.KbfsOnlineStatus,
   143  	// userIn is used to signify the FSM that user has just started looking at
   144  	// the Fs tab.
   145  	userIn <-chan struct{},
   146  	// userOut is used to signify the FSM that user has just switched away from
   147  	// the Fs tab.
   148  	userOut <-chan struct{},
   149  	// tryingTimerUp is used to signify the FSM that the timer for
   150  	// transitioning from a "trying" state to "offline" state is up.
   151  	tryingTimerUp <-chan struct{},
   152  	// connected is used to signify the FSM that we've just connected to the
   153  	// mdserver.
   154  	connected <-chan struct{},
   155  	// disconnected is used to signify the FSM that we've just lost connection to
   156  	// the mdserver.
   157  	disconnected <-chan struct{},
   158  ) {
   159  	defer wg.Done()
   160  
   161  	select {
   162  	case <-ctx.Done():
   163  		return
   164  	default:
   165  	}
   166  	vlog.CLogf(ctx, libkb.VLog1, "ostFsm initialState=%s", initialState)
   167  
   168  	state := initialState
   169  	for {
   170  		previousState := state
   171  
   172  		switch state {
   173  		case ostOnlineUserIn:
   174  			select {
   175  			case <-userIn:
   176  			case <-userOut:
   177  				state = ostOnlineUserOut
   178  			case <-tryingTimerUp:
   179  			case <-connected:
   180  			case <-disconnected:
   181  				state = ostTryingUserIn
   182  				sideEffects <- ostFastForward
   183  				sideEffects <- ostResetTimer
   184  
   185  			case <-ctx.Done():
   186  				return
   187  			}
   188  		case ostOnlineUserOut:
   189  			select {
   190  			case <-userIn:
   191  				state = ostOnlineUserIn
   192  			case <-userOut:
   193  			case <-tryingTimerUp:
   194  			case <-connected:
   195  			case <-disconnected:
   196  				state = ostTryingUserOut
   197  				// Don't start a timer as we don't want to transition into
   198  				// offline from trying when user is out. See comment for
   199  				// ostOfflineUserOut above.
   200  
   201  			case <-ctx.Done():
   202  				return
   203  			}
   204  		case ostTryingUserIn:
   205  			select {
   206  			case <-userIn:
   207  			case <-userOut:
   208  				state = ostTryingUserOut
   209  				// Stop the timer as we don't transition into offline when
   210  				// user is not looking.
   211  				sideEffects <- ostStopTimer
   212  			case <-tryingTimerUp:
   213  				state = ostOfflineUserIn
   214  			case <-connected:
   215  				state = ostOnlineUserIn
   216  			case <-disconnected:
   217  
   218  			case <-ctx.Done():
   219  				return
   220  			}
   221  		case ostTryingUserOut:
   222  			select {
   223  			case <-userIn:
   224  				state = ostTryingUserIn
   225  				sideEffects <- ostFastForward
   226  				sideEffects <- ostResetTimer
   227  			case <-userOut:
   228  			case <-tryingTimerUp:
   229  				// Don't transition into ostOfflineUserOut. See comment for
   230  				// offlienUserOut above.
   231  			case <-connected:
   232  				state = ostOnlineUserOut
   233  			case <-disconnected:
   234  
   235  			case <-ctx.Done():
   236  				return
   237  			}
   238  		case ostOfflineUserIn:
   239  			select {
   240  			case <-userIn:
   241  			case <-userOut:
   242  				state = ostOfflineUserOut
   243  			case <-tryingTimerUp:
   244  			case <-connected:
   245  				state = ostOnlineUserIn
   246  			case <-disconnected:
   247  
   248  			case <-ctx.Done():
   249  				return
   250  			}
   251  		case ostOfflineUserOut:
   252  			select {
   253  			case <-userIn:
   254  				state = ostOfflineUserIn
   255  				// Trigger fast forward but don't transition into "trying", to
   256  				// avoid flip-flopping.
   257  				sideEffects <- ostFastForward
   258  			case <-userOut:
   259  			case <-tryingTimerUp:
   260  			case <-connected:
   261  				state = ostOnlineUserOut
   262  			case <-disconnected:
   263  
   264  			case <-ctx.Done():
   265  				return
   266  			}
   267  
   268  		}
   269  
   270  		if previousState != state {
   271  			vlog.CLogf(ctx, libkb.VLog1, "ostFsm state=%s", state)
   272  			onlineStatus := state.getOnlineStatus()
   273  			if previousState.getOnlineStatus() != onlineStatus {
   274  				select {
   275  				case onlineStatusUpdates <- onlineStatus:
   276  				case <-ctx.Done():
   277  					return
   278  				}
   279  			}
   280  		}
   281  	}
   282  }
   283  
   284  func (ost *onlineStatusTracker) updateOnlineStatus(onlineStatus keybase1.KbfsOnlineStatus) {
   285  	ost.lock.Lock()
   286  	ost.currentStatus = onlineStatus
   287  	ost.lock.Unlock()
   288  	ost.onChange()
   289  }
   290  
   291  func (ost *onlineStatusTracker) run(ctx context.Context) {
   292  	defer ost.wg.Done()
   293  
   294  	for ost.config.KBFSOps() == nil {
   295  		time.Sleep(100 * time.Millisecond)
   296  	}
   297  
   298  	tryingStateTimer := time.NewTimer(time.Hour)
   299  	tryingStateTimer.Stop()
   300  
   301  	sideEffects := make(chan ostSideEffect)
   302  	onlineStatusUpdates := make(chan keybase1.KbfsOnlineStatus)
   303  	tryingTimerUp := make(chan struct{})
   304  	connected := make(chan struct{})
   305  	disconnected := make(chan struct{})
   306  
   307  	serviceErrors, invalidateChan := ost.config.KBFSOps().
   308  		StatusOfServices()
   309  
   310  	initialState := ostOfflineUserOut
   311  	if serviceErrors[MDServiceName] == nil {
   312  		initialState = ostOnlineUserOut
   313  	}
   314  
   315  	ost.wg.Add(1)
   316  	go ostFsm(ctx, ost.wg, ost.vlog,
   317  		initialState, sideEffects, onlineStatusUpdates,
   318  		ost.userIn, ost.userOut, tryingTimerUp, connected, disconnected)
   319  
   320  	ost.wg.Add(1)
   321  	// mdserver connection status watch routine
   322  	go func() {
   323  		defer ost.wg.Done()
   324  		invalidateChan := invalidateChan
   325  		var serviceErrors map[string]error
   326  		for {
   327  			select {
   328  			case <-invalidateChan:
   329  				serviceErrors, invalidateChan = ost.config.KBFSOps().
   330  					StatusOfServices()
   331  				if serviceErrors[MDServiceName] == nil {
   332  					connected <- struct{}{}
   333  				} else {
   334  					disconnected <- struct{}{}
   335  				}
   336  			case <-ctx.Done():
   337  				return
   338  			}
   339  		}
   340  	}()
   341  
   342  	for {
   343  		select {
   344  		case <-tryingStateTimer.C:
   345  			tryingTimerUp <- struct{}{}
   346  		case sideEffect := <-sideEffects:
   347  			switch sideEffect {
   348  			case ostResetTimer:
   349  				if !tryingStateTimer.Stop() {
   350  					select {
   351  					case <-tryingStateTimer.C:
   352  					default:
   353  					}
   354  				}
   355  				tryingStateTimer.Reset(ostTryingStateTimeout)
   356  			case ostStopTimer:
   357  				if !tryingStateTimer.Stop() {
   358  					<-tryingStateTimer.C
   359  					select {
   360  					case <-tryingStateTimer.C:
   361  					default:
   362  					}
   363  				}
   364  			case ostFastForward:
   365  				// This requires holding a lock and may block sometimes.
   366  				go ost.config.MDServer().FastForwardBackoff()
   367  			default:
   368  				panic(fmt.Sprintf("unknown side effect %d", sideEffect))
   369  			}
   370  		case onlineStatus := <-onlineStatusUpdates:
   371  			ost.updateOnlineStatus(onlineStatus)
   372  			ost.vlog.CLogf(ctx, libkb.VLog1, "ost onlineStatus=%d", onlineStatus)
   373  		case <-ctx.Done():
   374  			return
   375  		}
   376  	}
   377  }
   378  
   379  // TODO: we now have clientID in the subscriptionManager so it's not necessary
   380  // anymore for onlineStatusTracker to track it.
   381  
   382  func (ost *onlineStatusTracker) userInOut(clientID string, clientIsIn bool) {
   383  	ost.lock.Lock()
   384  	wasIn := len(ost.userIsLooking) != 0
   385  	if clientIsIn {
   386  		ost.userIsLooking[clientID] = true
   387  	} else {
   388  		delete(ost.userIsLooking, clientID)
   389  	}
   390  	isIn := len(ost.userIsLooking) != 0
   391  	ost.lock.Unlock()
   392  
   393  	if wasIn && !isIn {
   394  		ost.userOut <- struct{}{}
   395  	}
   396  
   397  	if !wasIn && isIn {
   398  		ost.userIn <- struct{}{}
   399  	}
   400  }
   401  
   402  // UserIn tells the onlineStatusTracker that user is looking at the Fs tab in
   403  // GUI. When user is looking at the Fs tab, the underlying RPC fast forwards
   404  // any backoff timer for reconnecting to the mdserver.
   405  func (ost *onlineStatusTracker) UserIn(ctx context.Context, clientID string) {
   406  	ost.userInOut(clientID, true)
   407  	ost.vlog.CLogf(ctx, libkb.VLog1, "UserIn clientID=%s", clientID)
   408  }
   409  
   410  // UserOut tells the onlineStatusTracker that user is not looking at the Fs
   411  // tab in GUI anymore.  GUI.
   412  func (ost *onlineStatusTracker) UserOut(ctx context.Context, clientID string) {
   413  	ost.userInOut(clientID, false)
   414  	ost.vlog.CLogf(ctx, libkb.VLog1, "UserOut clientID=%s", clientID)
   415  }
   416  
   417  // GetOnlineStatus implements the OnlineStatusTracker interface.
   418  func (ost *onlineStatusTracker) GetOnlineStatus() keybase1.KbfsOnlineStatus {
   419  	ost.lock.RLock()
   420  	defer ost.lock.RUnlock()
   421  	return ost.currentStatus
   422  }
   423  
   424  func newOnlineStatusTracker(
   425  	config Config, onChange func()) *onlineStatusTracker {
   426  	ctx, cancel := context.WithCancel(context.Background())
   427  	log := config.MakeLogger("onlineStatusTracker")
   428  	ost := &onlineStatusTracker{
   429  		cancel:        cancel,
   430  		config:        config,
   431  		onChange:      onChange,
   432  		currentStatus: keybase1.KbfsOnlineStatus_ONLINE,
   433  		vlog:          config.MakeVLogger(log),
   434  		userIsLooking: make(map[string]bool),
   435  		userIn:        make(chan struct{}),
   436  		userOut:       make(chan struct{}),
   437  		wg:            &sync.WaitGroup{},
   438  	}
   439  
   440  	ost.wg.Add(1)
   441  	go ost.run(ctx)
   442  
   443  	return ost
   444  }
   445  
   446  func (ost *onlineStatusTracker) shutdown() {
   447  	ost.cancel()
   448  	ost.wg.Wait()
   449  }