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  }