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 }