Skip to content

Commit

Permalink
[api] add cache for ReadContract/State() (#2827)
Browse files Browse the repository at this point in the history
  • Loading branch information
dustinxie committed Oct 6, 2021
1 parent 7d9bf96 commit 77a5a15
Show file tree
Hide file tree
Showing 4 changed files with 199 additions and 17 deletions.
47 changes: 43 additions & 4 deletions api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ type Server struct {
grpcServer *grpc.Server
hasActionIndex bool
electionCommittee committee.Committee
readCache *ReadCache
}

// NewServer creates a new server
Expand Down Expand Up @@ -157,6 +158,7 @@ func NewServer(
chainListener: NewChainListener(),
gs: gasstation.NewGasStation(chain, sf.SimulateExecution, dao, cfg.API),
electionCommittee: apiCfg.electionCommittee,
readCache: NewReadCache(),
}
if _, ok := cfg.Plugins[config.GatewayPlugin]; ok {
svr.hasActionIndex = true
Expand All @@ -169,6 +171,9 @@ func NewServer(
grpc_prometheus.Register(svr.grpcServer)
reflection.Register(svr.grpcServer)

if err := svr.chainListener.AddResponder(svr.readCache); err != nil {
return nil, err
}
return svr, nil
}

Expand Down Expand Up @@ -456,6 +461,14 @@ func (api *Server) ReadContract(ctx context.Context, in *iotexapi.ReadContractRe
if err := sc.LoadProto(in.Execution); err != nil {
return nil, status.Error(codes.InvalidArgument, err.Error())
}
key := hash.Hash160b(append([]byte(sc.Contract()), sc.Data()...))
if d, ok := api.readCache.Get(key); ok {
res := iotexapi.ReadContractResponse{}
if err := proto.Unmarshal(d, &res); err == nil {
return &res, nil
}
}

if in.CallerAddress == action.EmptyAddress {
in.CallerAddress = address.ZeroAddress
}
Expand Down Expand Up @@ -487,10 +500,14 @@ func (api *Server) ReadContract(ctx context.Context, in *iotexapi.ReadContractRe
}
// ReadContract() is read-only, if no error returned, we consider it a success
receipt.Status = uint64(iotextypes.ReceiptStatus_Success)
return &iotexapi.ReadContractResponse{
res := iotexapi.ReadContractResponse{
Data: hex.EncodeToString(retval),
Receipt: receipt.ConvertToReceiptPb(),
}, nil
}
if d, err := proto.Marshal(&res); err == nil {
api.readCache.Put(key, d)
}
return &res, nil
}

// ReadState reads state on blockchain
Expand Down Expand Up @@ -972,6 +989,20 @@ func (api *Server) Stop() error {
}

func (api *Server) readState(ctx context.Context, p protocol.Protocol, height string, methodName []byte, arguments ...[]byte) ([]byte, uint64, error) {
key := ReadKey{
Name: p.Name(),
Height: height,
Method: methodName,
Args: arguments,
}
if d, ok := api.readCache.Get(key.Hash()); ok {
var h uint64
if height != "" {
h, _ = strconv.ParseUint(height, 0, 64)
}
return d, h, nil
}

// TODO: need to complete the context
tipHeight := api.bc.TipHeight()
ctx = protocol.WithBlockCtx(ctx, protocol.BlockCtx{
Expand All @@ -997,12 +1028,20 @@ func (api *Server) readState(ctx context.Context, p protocol.Protocol, height st
inputEpochNum := rp.GetEpochNum(inputHeight)
if inputEpochNum < tipEpochNum {
// old data, wrap to history state reader
return p.ReadState(ctx, factory.NewHistoryStateReader(api.sf, rp.GetEpochHeight(inputEpochNum)), methodName, arguments...)
d, h, err := p.ReadState(ctx, factory.NewHistoryStateReader(api.sf, rp.GetEpochHeight(inputEpochNum)), methodName, arguments...)
if err == nil {
api.readCache.Put(key.Hash(), d)
}
return d, h, err
}
}

// TODO: need to distinguish user error and system error
return p.ReadState(ctx, api.sf, methodName, arguments...)
d, h, err := p.ReadState(ctx, api.sf, methodName, arguments...)
if err == nil {
api.readCache.Put(key.Hash(), d)
}
return d, h, err
}

func (api *Server) getActionsFromIndex(totalActions, start, count uint64) (*iotexapi.GetActionsResponse, error) {
Expand Down
18 changes: 5 additions & 13 deletions api/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@ import (
"github.com/iotexproject/iotex-core/blockindex"
"github.com/iotexproject/iotex-core/config"
"github.com/iotexproject/iotex-core/db"
"github.com/iotexproject/iotex-core/gasstation"
"github.com/iotexproject/iotex-core/pkg/unit"
"github.com/iotexproject/iotex-core/pkg/version"
"github.com/iotexproject/iotex-core/state"
Expand Down Expand Up @@ -2078,6 +2077,7 @@ func TestServer_GetEpochMeta(t *testing.T) {
}).AnyTimes()
svr.bc = mbc
}
svr.readCache.Clear()
res, err := svr.GetEpochMeta(context.Background(), &iotexapi.GetEpochMetaRequest{EpochNumber: test.EpochNumber})
require.NoError(err)
require.Equal(test.epochData.Num, res.EpochData.Num)
Expand Down Expand Up @@ -2689,19 +2689,11 @@ func createServer(cfg config.Config, needActPool bool) (*Server, string, error)
}
}

svr := &Server{
bc: bc,
sf: sf,
dao: dao,
indexer: indexer,
bfIndexer: bfIndexer,
ap: ap,
cfg: cfg,
gs: gasstation.NewGasStation(bc, sf.SimulateExecution, dao, cfg.API),
registry: registry,
hasActionIndex: true,
svr, err := NewServer(cfg, bc, nil, sf, dao, indexer, bfIndexer, ap, registry)
if err != nil {
return nil, "", err
}

svr.hasActionIndex = true
return svr, bfIndexFile, nil
}

Expand Down
86 changes: 86 additions & 0 deletions api/read_cache.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package api

import (
"encoding/json"
"sync"

"github.com/iotexproject/go-pkgs/hash"
"go.uber.org/zap"

"github.com/iotexproject/iotex-core/blockchain/block"
"github.com/iotexproject/iotex-core/pkg/log"
)

type (
// ReadKey represents a read key
ReadKey struct {
Name string `json:"name,omitempty"`
Height string `json:"height,omitempty"`
Method []byte `json:"method,omitempty"`
Args [][]byte `json:"args,omitempty"`
}

// ReadCache stores read results
ReadCache struct {
total, hit int
lock sync.RWMutex
bins map[hash.Hash160][]byte
}
)

// Hash returns the hash of key's json string
func (k *ReadKey) Hash() hash.Hash160 {
b, _ := json.Marshal(k)
return hash.Hash160b(b)
}

// NewReadCache returns a new read cache
func NewReadCache() *ReadCache {
return &ReadCache{
bins: make(map[hash.Hash160][]byte),
}
}

// Get reads according to key
func (rc *ReadCache) Get(key hash.Hash160) ([]byte, bool) {
rc.lock.RLock()
defer rc.lock.RUnlock()

rc.total++
d, ok := rc.bins[key]
if !ok {
return nil, false
}
rc.hit++
if rc.hit%100 == 0 {
log.L().Info("API cache hit", zap.Int("total", rc.total), zap.Int("hit", rc.hit))
}
return d, true
}

// Put writes according to key
func (rc *ReadCache) Put(key hash.Hash160, value []byte) {
rc.lock.Lock()
rc.bins[key] = value
rc.lock.Unlock()
}

// Clear clears the cache
func (rc *ReadCache) Clear() {
rc.lock.Lock()
rc.bins = nil
rc.bins = make(map[hash.Hash160][]byte)
rc.lock.Unlock()
}

// Respond implements the Responder interface
func (rc *ReadCache) Respond(*block.Block) error {
// invalidate the cache at every new block
rc.Clear()
return nil
}

// Exit implements the Responder interface
func (rc *ReadCache) Exit() {
rc.Clear()
}
65 changes: 65 additions & 0 deletions api/read_cache_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package api

import (
"testing"

"github.com/iotexproject/go-pkgs/hash"
"github.com/stretchr/testify/require"
)

func TestReadKey(t *testing.T) {
r := require.New(t)

var keys []hash.Hash160
for _, v := range []ReadKey{
{"staking", "10", []byte("activeBuckets"), [][]byte{[]byte{0, 1}, []byte{2, 3, 4, 5, 6, 7, 8}}},
{"staking", "10", []byte("activeBuckets"), [][]byte{[]byte{0, 1, 2}, []byte{3, 4, 5, 6, 7, 8}}},
{"staking", "10", []byte("activeBuckets"), [][]byte{[]byte{0, 1, 2, 3}, []byte{4, 5, 6, 7, 8}}},
{"staking", "10", []byte("activeBuckets"), [][]byte{[]byte{0, 1, 2, 3, 4, 5}, []byte{6, 7, 8}}},
{"staking", "10", []byte("activeBuckets"), [][]byte{[]byte{0, 1, 2, 3, 4, 5, 6, 7}, []byte{8}}},
} {
keys = append(keys, v.Hash())
}

// all keys are different
for i := range keys {
k := keys[i]
for j := i + 1; j < len(keys); j++ {
r.NotEqual(k, keys[j])
}
}
}

func TestReadCache(t *testing.T) {
r := require.New(t)

c := NewReadCache()
rcTests := []struct {
k hash.Hash160
v []byte
}{
{hash.Hash160b([]byte{1}), []byte{1}},
{hash.Hash160b([]byte{2}), []byte{2}},
{hash.Hash160b([]byte{3}), []byte{1}},
{hash.Hash160b([]byte{4}), []byte{2}},
}
for _, v := range rcTests {
d, ok := c.Get(v.k)
r.False(ok)
r.Nil(d)
c.Put(v.k, v.v)
}

for _, v := range rcTests {
d, ok := c.Get(v.k)
r.True(ok)
r.Equal(v.v, d)
}

c.Clear()
for _, v := range rcTests {
d, ok := c.Get(v.k)
r.False(ok)
r.Nil(d)
}
}

0 comments on commit 77a5a15

Please sign in to comment.