diff --git a/api/api.go b/api/api.go index e1c9554ba8..d60bb51c78 100644 --- a/api/api.go +++ b/api/api.go @@ -112,6 +112,7 @@ type Server struct { grpcServer *grpc.Server hasActionIndex bool electionCommittee committee.Committee + readCache *ReadCache } // NewServer creates a new server @@ -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 @@ -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 } @@ -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 } @@ -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 @@ -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{ @@ -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) { diff --git a/api/api_test.go b/api/api_test.go index dd02329cb1..5eaf8f34ca 100644 --- a/api/api_test.go +++ b/api/api_test.go @@ -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" @@ -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) @@ -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 } diff --git a/api/read_cache.go b/api/read_cache.go new file mode 100644 index 0000000000..f005e14309 --- /dev/null +++ b/api/read_cache.go @@ -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() +} diff --git a/api/read_cache_test.go b/api/read_cache_test.go new file mode 100644 index 0000000000..d297dcd9c6 --- /dev/null +++ b/api/read_cache_test.go @@ -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) + } +}