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 }