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
5package libkbfs
6
7import (
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
17type 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
33const ostTryingStateTimeout = 4 * time.Second
34
35type ostState int
36
37const (
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
73func (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
92func (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.
115type ostSideEffect int
116
117const (
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
131func 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
284func (ost *onlineStatusTracker) updateOnlineStatus(onlineStatus keybase1.KbfsOnlineStatus) {
285	ost.lock.Lock()
286	ost.currentStatus = onlineStatus
287	ost.lock.Unlock()
288	ost.onChange()
289}
290
291func (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
382func (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.
405func (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.
412func (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.
418func (ost *onlineStatusTracker) GetOnlineStatus() keybase1.KbfsOnlineStatus {
419	ost.lock.RLock()
420	defer ost.lock.RUnlock()
421	return ost.currentStatus
422}
423
424func 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
446func (ost *onlineStatusTracker) shutdown() {
447	ost.cancel()
448	ost.wg.Wait()
449}
450