github.com/keybase/client/go@v0.0.0-20241007131713-f10651d043c8/offline/rpc_cache.go (about) 1 package offline 2 3 import ( 4 "crypto/sha256" 5 "encoding/hex" 6 "fmt" 7 "sync" 8 "time" 9 10 "github.com/keybase/client/go/encrypteddb" 11 "github.com/keybase/client/go/libkb" 12 "github.com/keybase/client/go/msgpack" 13 "github.com/keybase/client/go/protocol/keybase1" 14 "golang.org/x/net/context" 15 ) 16 17 type RPCCache struct { 18 sync.Mutex 19 edb *encrypteddb.EncryptedDB 20 } 21 22 const ( 23 bestEffortHandlerTimeout = 500 * time.Millisecond 24 ) 25 26 func newEncryptedDB(g *libkb.GlobalContext) *encrypteddb.EncryptedDB { 27 keyFn := func(ctx context.Context) ([32]byte, error) { 28 // Use EncryptionReasonChatLocalStorage for legacy reasons. This 29 // function used to use chat/storage.GetSecretBoxKey in the past, and 30 // we didn't want users to lose encrypted data after we switched to 31 // more generic encrypteddb.GetSecretBoxKey. 32 return encrypteddb.GetSecretBoxKey(ctx, g, 33 libkb.EncryptionReasonChatLocalStorage, "offline rpc cache") 34 } 35 dbFn := func(g *libkb.GlobalContext) *libkb.JSONLocalDb { 36 return g.LocalDb 37 } 38 return encrypteddb.New(g, dbFn, keyFn) 39 } 40 41 func NewRPCCache(g *libkb.GlobalContext) *RPCCache { 42 return &RPCCache{ 43 edb: newEncryptedDB(g), 44 } 45 } 46 47 type hashStruct struct { 48 UID keybase1.UID 49 RPCName string 50 Arg interface{} 51 } 52 53 func hash(rpcName string, uid keybase1.UID, arg interface{}) ([]byte, error) { 54 h := sha256.New() 55 raw, err := msgpack.Encode(hashStruct{uid, rpcName, arg}) 56 if err != nil { 57 return nil, err 58 } 59 _, err = h.Write(raw) 60 if err != nil { 61 return nil, err 62 } 63 return h.Sum(nil), nil 64 } 65 66 func dbKey(rpcName string, uid keybase1.UID, arg interface{}) (libkb.DbKey, 67 error) { 68 raw, err := hash(rpcName, uid, arg) 69 if err != nil { 70 return libkb.DbKey{}, err 71 } 72 return libkb.DbKey{ 73 Typ: libkb.DBOfflineRPC, 74 Key: hex.EncodeToString(raw[0:16]), 75 }, nil 76 77 } 78 79 type Version int 80 81 type Value struct { 82 Version Version 83 Data []byte 84 } 85 86 func (c *RPCCache) get(mctx libkb.MetaContext, version Version, rpcName string, encrypted bool, arg interface{}, res interface{}) (found bool, err error) { 87 defer mctx.Trace(fmt.Sprintf("RPCCache#get(%d, %s, %v, %+v)", version, rpcName, encrypted, arg), &err)() 88 c.Lock() 89 defer c.Unlock() 90 91 dbk, err := dbKey(rpcName, mctx.G().GetMyUID(), arg) 92 if err != nil { 93 return false, err 94 } 95 96 var value Value 97 if encrypted { 98 found, err = c.edb.Get(mctx.Ctx(), dbk, &value) 99 } else { 100 found, err = mctx.G().LocalDb.GetIntoMsgpack(&value, dbk) 101 } 102 103 if err != nil || !found { 104 return found, err 105 } 106 107 if value.Version != version { 108 mctx.Debug("Found the wrong version (%d != %d) so returning 'not found", value.Version, version) 109 return false, nil 110 } 111 112 err = msgpack.Decode(res, value.Data) 113 if err != nil { 114 return false, err 115 } 116 return true, nil 117 } 118 119 func (c *RPCCache) put(mctx libkb.MetaContext, version Version, rpcName string, encrypted bool, arg interface{}, res interface{}) (err error) { 120 defer mctx.Trace(fmt.Sprintf("RPCCache#put(%d, %s, %v, %+v)", version, rpcName, encrypted, arg), &err)() 121 c.Lock() 122 defer c.Unlock() 123 124 dbk, err := dbKey(rpcName, mctx.G().GetMyUID(), arg) 125 if err != nil { 126 return err 127 } 128 129 value := Value{Version: version} 130 value.Data, err = msgpack.Encode(res) 131 if err != nil { 132 return err 133 } 134 135 if encrypted { 136 err = c.edb.Put(mctx.Ctx(), dbk, value) 137 } else { 138 err = mctx.G().LocalDb.PutObjMsgpack(dbk, nil, value) 139 } 140 return err 141 } 142 143 // Serve an RPC out of the offline cache. The machinery only kicks 144 // into gear if the `oa` OfflineAvailability mode is set to 145 // BEST_EFFORT. If not, then just use the function `handler` which 146 // does the main work of handling the RPC. Note that `handler` must 147 // not modify anything in the caller's stack frame; it might be run in 148 // a background goroutine after this function returns, to populate the 149 // cache. `handler` also returns the return value for the RPC as an 150 // interface, so it can be inserted into the offline cache in the 151 // success case. We also pass this function a `version`, which will 152 // tell the cache-access machinery to fail if the wrong version of the 153 // data is cached. Next, we pass the `rpcName`, the argument, and the 154 // pointer to which the result is stored if we hit the cache. 155 // 156 // If this function doesn't return an error, and the returned `res` is 157 // nil, then `resPtr` will have been filled in already by the cache. 158 // Otherwise, `res` should be used by the caller as the response. 159 func (c *RPCCache) Serve(mctx libkb.MetaContext, oa keybase1.OfflineAvailability, version Version, rpcName string, encrypted bool, arg interface{}, resPtr interface{}, 160 handler func(mctx libkb.MetaContext) (interface{}, error)) (res interface{}, err error) { 161 162 if oa != keybase1.OfflineAvailability_BEST_EFFORT { 163 return handler(mctx) 164 } 165 mctx = mctx.WithLogTag("OFLN") 166 defer mctx.Trace(fmt.Sprintf("RPCCache#Serve(%d, %s, %v, %+v)", version, rpcName, encrypted, arg), &err)() 167 168 found, err := c.get(mctx, version, rpcName, encrypted, arg, resPtr) 169 if err != nil { 170 return nil, err 171 } 172 173 // If we know we're not connected, use the cache value right away. 174 // TODO: API calls shouldn't necessarily depend on the 175 // connectivity as measured by gregor. 176 if mctx.G().ConnectivityMonitor.IsConnected(mctx.Ctx()) == libkb.ConnectivityMonitorNo { 177 if !found { 178 return nil, libkb.OfflineError{} 179 } 180 return nil, nil // resPtr was filled in by get() 181 } 182 183 type handlerRes struct { 184 res interface{} 185 err error 186 } 187 resCh := make(chan handlerRes, 1) 188 189 // New goroutine needs a new metacontext, in case the original 190 // gets canceled after the timeout below. Preserve the log tags 191 // though. 192 newMctx := mctx.BackgroundWithLogTags() 193 194 // Launch a background goroutine to try to invoke the handler. 195 // Even if we hit a timeout below and return the cached value, 196 // this goroutine will keep going in an attempt to populate the 197 // cache on a slow network. 198 go func() { 199 res, err := handler(newMctx) 200 if err != nil { 201 resCh <- handlerRes{res, err} 202 return 203 } 204 tmp := c.put(newMctx, version, rpcName, encrypted, arg, res) 205 if tmp != nil { 206 newMctx.Warning("Error putting RPC to offline storage: %s", tmp.Error()) 207 } 208 resCh <- handlerRes{res, nil} 209 }() 210 211 var timerCh <-chan time.Time 212 if found { 213 // Use a quick timeout if there's an available cached value. 214 timerCh = mctx.G().Clock().After(bestEffortHandlerTimeout) 215 } else { 216 // Wait indefinitely on the handler if there's nothing in the cache. 217 timerCh = make(chan time.Time) 218 } 219 select { 220 case hr := <-resCh: 221 // Explicitly return hr.res rather than nil in the err != nil 222 // case, because some RPCs might depend on getting a result 223 // along with an error. 224 return hr.res, hr.err 225 case <-timerCh: 226 mctx.Debug("Timeout waiting for handler; using cached value instead") 227 return res, nil 228 case <-mctx.Ctx().Done(): 229 return nil, mctx.Ctx().Err() 230 } 231 }