/
githubmirror
/
client
Обзор
Документация
Войти
/
githubmirror
/
client
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
go/offline/rpc_cache.go
231 строка
6 KB
chrisnojima-zoom
Version 670 - clean2 (#29122)
08 июн 2026, 19:31
Не верифицирован
08 июн 2026, 19:31
1943197
Код
Авторство
О чём код?
package offline import ( "context" "crypto/sha256" "encoding/hex" "fmt" "sync" "time" "github.com/keybase/client/go/encrypteddb" "github.com/keybase/client/go/libkb" "github.com/keybase/client/go/msgpack" "github.com/keybase/client/go/protocol/keybase1" ) type RPCCache struct { sync.Mutex edb *encrypteddb.EncryptedDB } const ( bestEffortHandlerTimeout = 500 * time.Millisecond ) func newEncryptedDB(g *libkb.GlobalContext) *encrypteddb.EncryptedDB { keyFn := func(ctx context.Context) ([32]byte, error) { // Use EncryptionReasonChatLocalStorage for legacy reasons. This // function used to use chat/storage.GetSecretBoxKey in the past, and // we didn't want users to lose encrypted data after we switched to // more generic encrypteddb.GetSecretBoxKey. return encrypteddb.GetSecretBoxKey(ctx, g, libkb.EncryptionReasonChatLocalStorage, "offline rpc cache") } dbFn := func(g *libkb.GlobalContext) *libkb.JSONLocalDb { return g.LocalDb } return encrypteddb.New(g, dbFn, keyFn) } func NewRPCCache(g *libkb.GlobalContext) *RPCCache { return &RPCCache{ edb: newEncryptedDB(g), } } type hashStruct struct { UID keybase1.UID RPCName string Arg any } func hash(rpcName string, uid keybase1.UID, arg any) ([]byte, error) { h := sha256.New() raw, err := msgpack.Encode(hashStruct{uid, rpcName, arg}) if err != nil { return nil, err } _, err = h.Write(raw) if err != nil { return nil, err } return h.Sum(nil), nil } func dbKey(rpcName string, uid keybase1.UID, arg any) (libkb.DbKey, error, ) { raw, err := hash(rpcName, uid, arg) if err != nil { return libkb.DbKey{}, err } return libkb.DbKey{ Typ: libkb.DBOfflineRPC, Key: hex.EncodeToString(raw[0:16]), }, nil } type Version int type Value struct { Version Version Data []byte } func (c *RPCCache) get(mctx libkb.MetaContext, version Version, rpcName string, encrypted bool, arg any, res any) (found bool, err error) { defer mctx.Trace(fmt.Sprintf("RPCCache#get(%d, %s, %v, %+v)", version, rpcName, encrypted, arg), &err)() c.Lock() defer c.Unlock() dbk, err := dbKey(rpcName, mctx.G().GetMyUID(), arg) if err != nil { return false, err } var value Value if encrypted { found, err = c.edb.Get(mctx.Ctx(), dbk, &value) } else { found, err = mctx.G().LocalDb.GetIntoMsgpack(&value, dbk) } if err != nil || !found { return found, err } if value.Version != version { mctx.Debug("Found the wrong version (%d != %d) so returning 'not found", value.Version, version) return false, nil } err = msgpack.Decode(res, value.Data) if err != nil { return false, err } return true, nil } func (c *RPCCache) put(mctx libkb.MetaContext, version Version, rpcName string, encrypted bool, arg any, res any) (err error) { defer mctx.Trace(fmt.Sprintf("RPCCache#put(%d, %s, %v, %+v)", version, rpcName, encrypted, arg), &err)() c.Lock() defer c.Unlock() dbk, err := dbKey(rpcName, mctx.G().GetMyUID(), arg) if err != nil { return err } value := Value{Version: version} value.Data, err = msgpack.Encode(res) if err != nil { return err } if encrypted { err = c.edb.Put(mctx.Ctx(), dbk, value) } else { err = mctx.G().LocalDb.PutObjMsgpack(dbk, nil, value) } return err } // Serve an RPC out of the offline cache. The machinery only kicks // into gear if the `oa` OfflineAvailability mode is set to // BEST_EFFORT. If not, then just use the function `handler` which // does the main work of handling the RPC. Note that `handler` must // not modify anything in the caller's stack frame; it might be run in // a background goroutine after this function returns, to populate the // cache. `handler` also returns the return value for the RPC as an // interface, so it can be inserted into the offline cache in the // success case. We also pass this function a `version`, which will // tell the cache-access machinery to fail if the wrong version of the // data is cached. Next, we pass the `rpcName`, the argument, and the // pointer to which the result is stored if we hit the cache. // // If this function doesn't return an error, and the returned `res` is // nil, then `resPtr` will have been filled in already by the cache. // Otherwise, `res` should be used by the caller as the response. func (c *RPCCache) Serve(mctx libkb.MetaContext, oa keybase1.OfflineAvailability, version Version, rpcName string, encrypted bool, arg any, resPtr any, handler func(mctx libkb.MetaContext) (any, error), ) (res any, err error) { if oa != keybase1.OfflineAvailability_BEST_EFFORT { return handler(mctx) } mctx = mctx.WithLogTag("OFLN") defer mctx.Trace(fmt.Sprintf("RPCCache#Serve(%d, %s, %v, %+v)", version, rpcName, encrypted, arg), &err)() found, err := c.get(mctx, version, rpcName, encrypted, arg, resPtr) if err != nil { return nil, err } // If we know we're not connected, use the cache value right away. // TODO: API calls shouldn't necessarily depend on the // connectivity as measured by gregor. if mctx.G().ConnectivityMonitor.IsConnected(mctx.Ctx()) == libkb.ConnectivityMonitorNo { if !found { return nil, libkb.OfflineError{} } return nil, nil // resPtr was filled in by get() } type handlerRes struct { res any err error } resCh := make(chan handlerRes, 1) // New goroutine needs a new metacontext, in case the original // gets canceled after the timeout below. Preserve the log tags // though. newMctx := mctx.BackgroundWithLogTags() // Launch a background goroutine to try to invoke the handler. // Even if we hit a timeout below and return the cached value, // this goroutine will keep going in an attempt to populate the // cache on a slow network. go func() { res, err := handler(newMctx) if err != nil { resCh <- handlerRes{res, err} return } tmp := c.put(newMctx, version, rpcName, encrypted, arg, res) if tmp != nil { newMctx.Warning("Error putting RPC to offline storage: %s", tmp.Error()) } resCh <- handlerRes{res, nil} }() var timerCh <-chan time.Time if found { // Use a quick timeout if there's an available cached value. timerCh = mctx.G().Clock().After(bestEffortHandlerTimeout) } else { // Wait indefinitely on the handler if there's nothing in the cache. timerCh = make(chan time.Time) } select { case hr := <-resCh: // Explicitly return hr.res rather than nil in the err != nil // case, because some RPCs might depend on getting a result // along with an error. return hr.res, hr.err case <-timerCh: mctx.Debug("Timeout waiting for handler; using cached value instead") return res, nil case <-mctx.Ctx().Done(): return nil, mctx.Ctx().Err() } }