github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/teams/team_role_map.go (about)

     1  package teams
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"github.com/keybase/client/go/libkb"
     7  	"github.com/keybase/client/go/protocol/keybase1"
     8  	"sync"
     9  	"time"
    10  )
    11  
    12  type TeamRoleMapManager struct {
    13  	libkb.NoopNotifyListener
    14  	sync.Mutex
    15  	lastKnownVersion *keybase1.UserTeamVersion
    16  	state            *keybase1.TeamRoleMapStored
    17  	reachabilityCh   chan keybase1.Reachability
    18  }
    19  
    20  func NewTeamRoleMapManagerAndInstall(g *libkb.GlobalContext) {
    21  	r := NewTeamRoleMapManager()
    22  	g.SetTeamRoleMapManager(r)
    23  	if g.NotifyRouter != nil {
    24  		g.NotifyRouter.AddListener(r)
    25  	}
    26  }
    27  
    28  func NewTeamRoleMapManager() *TeamRoleMapManager {
    29  	return &TeamRoleMapManager{
    30  		reachabilityCh: make(chan keybase1.Reachability),
    31  	}
    32  }
    33  
    34  var _ libkb.TeamRoleMapManager = (*TeamRoleMapManager)(nil)
    35  
    36  // Reachability should be called whenever the reachability status of the app changes
    37  // (via NotifyRouter). If we happen to be waiting on a timer to do a refresh, then break
    38  // out and refresh it.
    39  func (t *TeamRoleMapManager) Reachability(r keybase1.Reachability) {
    40  	if r.Reachable == keybase1.Reachable_NO {
    41  		return
    42  	}
    43  	select {
    44  	case t.reachabilityCh <- r:
    45  	default:
    46  	}
    47  }
    48  
    49  func (t *TeamRoleMapManager) isFresh(m libkb.MetaContext, state *keybase1.TeamRoleMapStored) bool {
    50  	if t.lastKnownVersion != nil && *t.lastKnownVersion > state.Data.Version {
    51  		m.Debug("TeamRoleMap version is stale (%d > %d)", *t.lastKnownVersion, state.Data.Version)
    52  		return false
    53  	}
    54  	tm := state.CachedAt.Time()
    55  	diff := m.G().Clock().Now().Sub(tm)
    56  	if diff >= 48*time.Hour {
    57  		m.Debug("TeamRoleMap isn't fresh, it's %s old", diff)
    58  		return false
    59  	}
    60  	return true
    61  }
    62  
    63  func (t *TeamRoleMapManager) dbKey(mctx libkb.MetaContext, uid keybase1.UID) libkb.DbKey {
    64  	return libkb.DbKey{
    65  		Typ: libkb.DBTeamRoleMap,
    66  		Key: string(uid),
    67  	}
    68  }
    69  
    70  func (t *TeamRoleMapManager) wait(mctx libkb.MetaContext, dur time.Duration) {
    71  	select {
    72  	case <-mctx.G().Clock().After(dur):
    73  		mctx.Debug("Waited the full %s duration", dur)
    74  	case r := <-t.reachabilityCh:
    75  		mctx.Debug("short-circuited wait since we came back online (%s)", r.Reachable)
    76  	}
    77  }
    78  
    79  func (t *TeamRoleMapManager) loadFromDB(mctx libkb.MetaContext, uid keybase1.UID) (err error) {
    80  	var obj keybase1.TeamRoleMapStored
    81  	var found bool
    82  	found, err = mctx.G().LocalDb.GetInto(&obj, t.dbKey(mctx, uid))
    83  	if err != nil {
    84  		mctx.Debug("Error fetching TeamRoleMap from disk: %s", err)
    85  		return err
    86  	}
    87  	if !found {
    88  		mctx.Debug("No stored TeamRoleMap for %s", uid)
    89  		return nil
    90  	}
    91  	t.state = &obj
    92  	return nil
    93  }
    94  
    95  func (t *TeamRoleMapManager) storeToDB(mctx libkb.MetaContext, uid keybase1.UID) (err error) {
    96  	return mctx.G().LocalDb.PutObj(t.dbKey(mctx, uid), nil, *t.state)
    97  }
    98  
    99  func (t *TeamRoleMapManager) isLoadedAndFresh(mctx libkb.MetaContext) bool {
   100  	return t.state != nil && t.isFresh(mctx, t.state)
   101  }
   102  
   103  func backoffInitial(doBackoff bool) time.Duration {
   104  	if !doBackoff {
   105  		return time.Duration(0)
   106  	}
   107  	return 2 * time.Minute
   108  }
   109  
   110  func backoffIncrease(d time.Duration) time.Duration {
   111  	d = time.Duration(float64(d) * 1.25)
   112  	max := 10 * time.Minute
   113  	if d > max {
   114  		d = max
   115  	}
   116  	return d
   117  }
   118  
   119  func (t *TeamRoleMapManager) loadDelayedRetry(mctx libkb.MetaContext, backoff time.Duration) {
   120  	mctx = mctx.WithLogTag("TRMM-LDR")
   121  	var err error
   122  	defer mctx.Trace("TeamRoleMapManager#loadDelayedRetry", &err)()
   123  
   124  	if backoff == time.Duration(0) {
   125  		mctx.Debug("Not retrying, no backoff specified")
   126  		return
   127  	}
   128  
   129  	mctx.Debug("delayed retry: sleeping for %s backoff", backoff)
   130  	t.wait(mctx, backoff)
   131  
   132  	t.Lock()
   133  	defer t.Unlock()
   134  	if t.isLoadedAndFresh(mctx) {
   135  		mctx.Debug("delayed retry: TeamRoleMap was fresh, so nothing to do")
   136  		return
   137  	}
   138  
   139  	// Note that we are passing retryOnFail=true, meaning we're going to keep the retry attempt
   140  	// going if we fail again (but with a bigger backoff parameter).
   141  	err = t.load(mctx, backoffIncrease(backoff))
   142  	if err != nil {
   143  		mctx.Debug("delayed retry: exiting on error: %s", err)
   144  		return
   145  	}
   146  
   147  	// If we're here, it's because someone called #Get(_,true), meaning they wanted a retry
   148  	// on failure, and there was a failure. In that case, we call back to them
   149  	// (via sendNotification), and they will reattempt the Get(), hopefully succeeding this time.
   150  	t.sendNotification(mctx, t.state.Data.Version)
   151  }
   152  
   153  func (t *TeamRoleMapManager) sendNotification(mctx libkb.MetaContext, version keybase1.UserTeamVersion) {
   154  	mctx.G().NotifyRouter.HandleTeamRoleMapChanged(mctx.Ctx(), version)
   155  }
   156  
   157  func (t *TeamRoleMapManager) load(mctx libkb.MetaContext, retryOnFailBackoff time.Duration) (err error) {
   158  	uid := mctx.CurrentUID()
   159  	if uid.IsNil() {
   160  		return errors.New("cannot get TeamRoleMap for a logged out user")
   161  	}
   162  
   163  	if t.isLoadedAndFresh(mctx) {
   164  		return nil
   165  	}
   166  
   167  	if t.state == nil {
   168  		_ = t.loadFromDB(mctx, uid)
   169  	}
   170  
   171  	if t.isLoadedAndFresh(mctx) {
   172  		mctx.Debug("Loaded fresh TeamRoleMap from DB")
   173  		return nil
   174  	}
   175  
   176  	type apiResType struct {
   177  		keybase1.TeamRoleMapAndVersion
   178  		libkb.AppStatusEmbed
   179  	}
   180  	var apiRes apiResType
   181  
   182  	arg := libkb.NewAPIArg("team/for_user")
   183  	arg.SessionType = libkb.APISessionTypeREQUIRED
   184  	arg.RetryCount = 3
   185  	arg.AppStatusCodes = []int{libkb.SCOk, libkb.SCNoUpdate}
   186  	arg.Args = libkb.HTTPArgs{
   187  		"compact": libkb.B{Val: true},
   188  	}
   189  	var currVersion keybase1.UserTeamVersion
   190  	if t.state != nil {
   191  		currVersion = t.state.Data.Version
   192  		arg.Args["user_team_version"] = libkb.I{Val: int(currVersion)}
   193  	}
   194  
   195  	err = mctx.G().API.GetDecode(mctx, arg, &apiRes)
   196  
   197  	if err != nil {
   198  		mctx.Debug("failed to TeamRoleMap from server: %s", err)
   199  		go t.loadDelayedRetry(mctx.BackgroundWithLogTags(), retryOnFailBackoff)
   200  		return err
   201  	}
   202  
   203  	if apiRes.Status.Code == libkb.SCNoUpdate {
   204  		t.lastKnownVersion = &currVersion
   205  		mctx.Debug("TeamRoleMap was fresh at version %d", currVersion)
   206  		return nil
   207  	}
   208  
   209  	t.state = &keybase1.TeamRoleMapStored{
   210  		Data:     apiRes.TeamRoleMapAndVersion,
   211  		CachedAt: keybase1.ToTime(mctx.G().Clock().Now()),
   212  	}
   213  	t.lastKnownVersion = &t.state.Data.Version
   214  	_ = t.storeToDB(mctx, uid)
   215  	mctx.Debug("Updating TeamRoleMap to version %d", t.state.Data.Version)
   216  
   217  	return nil
   218  }
   219  
   220  // Get is called from the frontend to refresh its view of the TeamRoleMap state. The unfortunate
   221  // case is when the
   222  func (t *TeamRoleMapManager) Get(mctx libkb.MetaContext, retryOnFail bool) (res keybase1.TeamRoleMapAndVersion, err error) {
   223  	defer mctx.Trace("TeamRoleMapManager#Get", &err)()
   224  	t.Lock()
   225  	defer t.Unlock()
   226  	err = t.load(mctx, backoffInitial(retryOnFail))
   227  	if err != nil {
   228  		return res, err
   229  	}
   230  	return t.state.Data, nil
   231  }
   232  
   233  func (t *TeamRoleMapManager) Update(mctx libkb.MetaContext, version keybase1.UserTeamVersion) (err error) {
   234  	defer mctx.Trace(fmt.Sprintf("TeamRoleMapManager#Update(%d)", version), &err)()
   235  	t.Lock()
   236  	defer t.Unlock()
   237  	t.lastKnownVersion = &version
   238  	if t.isLoadedAndFresh(mctx) {
   239  		mctx.Debug("Swallowing update for TeamRoleMap to version %d, since it's already loaded and fresh", version)
   240  		return nil
   241  	}
   242  	t.sendNotification(mctx, version)
   243  	return t.load(mctx, backoffInitial(false))
   244  }
   245  
   246  func (t *TeamRoleMapManager) FlushCache() {
   247  	t.Lock()
   248  	defer t.Unlock()
   249  	t.state = nil
   250  }
   251  
   252  // Query the state of the team role list manager -- only should be used in testing, as it's
   253  // not exposed in the geenric libkb.TeamRoleMapManager interface. For testing, we want to see if
   254  // the update path is actually updating the team, so we want to be able to query what's in the manager
   255  // without going to disk or network if it's stale.
   256  func (t *TeamRoleMapManager) Query() *keybase1.TeamRoleMapStored {
   257  	t.Lock()
   258  	defer t.Unlock()
   259  	if t.state == nil {
   260  		return nil
   261  	}
   262  	tmp := t.state.DeepCopy()
   263  	return &tmp
   264  }