Skip to content
This repository has been archived by the owner on Nov 8, 2023. It is now read-only.

Commit

Permalink
delete state tx isolation in mockstub (#63)
Browse files Browse the repository at this point in the history
* delete state tx isolation in mockstub
* state cache wrapper  
* mockStub dumps state date only if peer.Response is shim.OK
* state Clone method
  • Loading branch information
vitiko authored Jun 30, 2021
1 parent 7b4ba1e commit 6b3da10
Show file tree
Hide file tree
Showing 10 changed files with 473 additions and 65 deletions.
16 changes: 13 additions & 3 deletions router/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ package router
import (
"time"

"go.uber.org/zap"

"github.com/hyperledger/fabric-chaincode-go/pkg/cid"
"github.com/hyperledger/fabric-chaincode-go/shim"
"go.uber.org/zap"

"github.com/s7techlab/cckit/state"
)

Expand Down Expand Up @@ -107,7 +107,12 @@ func NewContext(stub shim.ChaincodeStubInterface, logger *zap.Logger) *context {
}

func (c *context) Clone() Context {
return NewContext(c.stub, c.logger)
ctx := NewContext(c.stub, c.logger)
if c.state != nil {
ctx.state = c.state.Clone()
}

return ctx
}

func (c *context) Stub() shim.ChaincodeStubInterface {
Expand Down Expand Up @@ -265,3 +270,8 @@ func (c *context) Get(key string) interface{} {
func (c *context) SetEvent(name string, payload interface{}) error {
return c.Event().Set(name, payload)
}

func ContextWithStateCache(ctx Context) Context {
clone := ctx.Clone()
return clone.UseState(state.WithCache(clone.State()))
}
50 changes: 43 additions & 7 deletions state/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ import (
"github.com/hyperledger/fabric-chaincode-go/shim"
"github.com/hyperledger/fabric-protos-go/ledger/queryresult"
"github.com/pkg/errors"
"github.com/s7techlab/cckit/convert"
"go.uber.org/zap"

"github.com/s7techlab/cckit/convert"
)

// HistoryEntry struct containing history information of a single entry
Expand Down Expand Up @@ -100,13 +101,21 @@ type State interface {
// ExistsPrivate returns entry existence in private state
// entry can be Key (string or []string) or type implementing Keyer interface
ExistsPrivate(collection string, entry interface{}) (bool, error)

// Clone state for next changing transformers, state access methods etc
Clone() State
}

type Impl struct {
stub shim.ChaincodeStubInterface
logger *zap.Logger
PutState func(string, []byte) error
GetState func(string) ([]byte, error)
stub shim.ChaincodeStubInterface
logger *zap.Logger

// wrappers for state access methods
PutState func(string, []byte) error
GetState func(string) ([]byte, error)
DelState func(string) error
GetStateByPartialCompositeKey func(objectType string, keys []string) (shim.StateQueryIteratorInterface, error)

StateKeyTransformer KeyTransformer
StateKeyReverseTransformer KeyTransformer
StateGetTransformer FromBytesTransformer
Expand Down Expand Up @@ -135,9 +144,36 @@ func NewState(stub shim.ChaincodeStubInterface, logger *zap.Logger) *Impl {
return stub.PutState(key, bb)
}

// DelState records the specified `key` to be deleted in the writeset of
// the transaction proposal.
i.DelState = func(key string) error {
return stub.DelState(key)
}

// GetStateByPartialCompositeKey queries the state in the ledger based on
// a given partial composite key
i.GetStateByPartialCompositeKey = func(objectType string, keys []string) (shim.StateQueryIteratorInterface, error) {
return stub.GetStateByPartialCompositeKey(objectType, keys)
}

return i
}

func (s *Impl) Clone() State {
return &Impl{
stub: s.stub,
logger: s.logger,
PutState: s.PutState,
GetState: s.GetState,
DelState: s.DelState,
GetStateByPartialCompositeKey: s.GetStateByPartialCompositeKey,
StateKeyTransformer: s.StateKeyTransformer,
StateKeyReverseTransformer: s.StateKeyReverseTransformer,
StateGetTransformer: s.StateGetTransformer,
StatePutTransformer: s.StatePutTransformer,
}
}

func (s *Impl) Logger() *zap.Logger {
return s.logger
}
Expand Down Expand Up @@ -296,7 +332,7 @@ func (s *Impl) createStateQueryIterator(namespace interface{}) (shim.StateQueryI
attrs = keyTransformed[1:]
}

return s.stub.GetStateByPartialCompositeKey(objectType, attrs)
return s.GetStateByPartialCompositeKey(objectType, attrs)
}

func (s *Impl) Keys(namespace interface{}) ([]string, error) {
Expand Down Expand Up @@ -396,7 +432,7 @@ func (s *Impl) Delete(entry interface{}) error {
}

s.logger.Debug(`state DELETE`, zap.String(`key`, key.String))
return s.stub.DelState(key.String)
return s.DelState(key.String)
}

func (s *Impl) UseKeyTransformer(kt KeyTransformer) State {
Expand Down
113 changes: 108 additions & 5 deletions state/state_cached.go
Original file line number Diff line number Diff line change
@@ -1,31 +1,134 @@
package state

import (
"sort"
"strings"

"github.com/hyperledger/fabric-chaincode-go/shim"
"github.com/hyperledger/fabric-protos-go/ledger/queryresult"
"github.com/pkg/errors"
)

type (
TxWriteSet map[string][]byte
TxDeleteSet map[string]interface{}

Cached struct {
State
TxCache map[string][]byte
TxWriteSet TxWriteSet
TxDeleteSet TxDeleteSet
}

CachedQueryIterator struct {
current int
closed bool
KVs []*queryresult.KV
}
)

// WithCached returns state with tx level state cache
func WithCache(ss State) *Cached {
s := ss.(*Impl)
cached := &Cached{
State: s,
TxCache: make(map[string][]byte),
State: s,
TxWriteSet: make(map[string][]byte),
TxDeleteSet: make(map[string]interface{}),
}

// PutState wrapper
s.PutState = func(key string, bb []byte) error {
cached.TxCache[key] = bb
cached.TxWriteSet[key] = bb
return s.stub.PutState(key, bb)
}

// GetState wrapper
s.GetState = func(key string) ([]byte, error) {
if bb, ok := cached.TxCache[key]; ok {
if bb, ok := cached.TxWriteSet[key]; ok {
return bb, nil
}

if _, ok := cached.TxDeleteSet[key]; ok {
return []byte{}, nil
}
return s.stub.GetState(key)
}

s.DelState = func(key string) error {
delete(cached.TxWriteSet, key)
cached.TxDeleteSet[key] = nil

return s.stub.DelState(key)
}

s.GetStateByPartialCompositeKey = func(objectType string, keys []string) (shim.StateQueryIteratorInterface, error) {
iterator, err := s.stub.GetStateByPartialCompositeKey(objectType, keys)
if err != nil {
return nil, err
}

prefix, err := s.stub.CreateCompositeKey(objectType, keys)
if err != nil {
return nil, err
}

return NewCachedQueryIterator(iterator, prefix, cached.TxWriteSet, cached.TxDeleteSet)
}

return cached
}

func NewCachedQueryIterator(iterator shim.StateQueryIteratorInterface, prefix string, writeSet TxWriteSet, deleteSet TxDeleteSet) (*CachedQueryIterator, error) {
queryIterator := &CachedQueryIterator{
current: -1,
}
for iterator.HasNext() {
kv, err := iterator.Next()
if err != nil {
return nil, err
}

if _, ok := deleteSet[kv.Key]; ok {
continue
}

queryIterator.KVs = append(queryIterator.KVs, kv)
}

for wroteKey, wroteValue := range writeSet {
if strings.HasPrefix(wroteKey, prefix) {
queryIterator.KVs = append(queryIterator.KVs, &queryresult.KV{
Namespace: "",
Key: wroteKey,
Value: wroteValue,
})
}
}

sort.Slice(queryIterator.KVs, func(i, j int) bool {
return queryIterator.KVs[i].Key < queryIterator.KVs[j].Key
})

return queryIterator, nil
}

func (i *CachedQueryIterator) Next() (*queryresult.KV, error) {
if !i.HasNext() {
return nil, errors.New(`no next items`)
}

i.current++
return i.KVs[i.current], nil
}

// HasNext returns true if the range query iterator contains additional keys
// and values.
func (i *CachedQueryIterator) HasNext() bool {
return i.current < len(i.KVs)-1
}

// Close closes the iterator. This should be called when done
// reading from the iterator to free up resources.
func (i *CachedQueryIterator) Close() error {
i.closed = true
return nil
}
48 changes: 48 additions & 0 deletions state/state_cached_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package state_test

import (
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"

"github.com/s7techlab/cckit/state/testdata"
testcc "github.com/s7techlab/cckit/testing"
expectcc "github.com/s7techlab/cckit/testing/expect"
)

const (
StateCachedChaincode = `state_cached`
)

var _ = Describe(`State caching`, func() {

//Create chaincode mocks
stateCachedCC := testcc.NewMockStub(StateCachedChaincode, testdata.NewStateCachedCC())

It("Read after write returns non empty entry", func() {
resp := expectcc.PayloadIs(stateCachedCC.Invoke(testdata.TxStateCachedReadAfterWrite), &testdata.Value{})
Expect(resp).To(Equal(testdata.KeyValue(testdata.Keys[0])))
})

It("Read after delete returns empty entry", func() {
resp := stateCachedCC.Invoke(testdata.TxStateCachedReadAfterDelete)
Expect(resp.Payload).To(Equal([]byte{}))
})

It("List after write returns list", func() {
resp := expectcc.PayloadIs(
stateCachedCC.Invoke(testdata.TxStateCachedListAfterWrite), &[]testdata.Value{}).([]testdata.Value)

// all key exists
Expect(resp).To(Equal([]testdata.Value{
testdata.KeyValue(testdata.Keys[0]), testdata.KeyValue(testdata.Keys[1]), testdata.KeyValue(testdata.Keys[2])}))
})

It("List after delete returns list without deleted item", func() {
resp := expectcc.PayloadIs(
stateCachedCC.Invoke(testdata.TxStateCachedListAfterDelete), &[]testdata.Value{}).([]testdata.Value)

// first key is deleted
Expect(resp).To(Equal([]testdata.Value{
testdata.KeyValue(testdata.Keys[1]), testdata.KeyValue(testdata.Keys[2])}))
})
})
6 changes: 3 additions & 3 deletions state/state_test.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
package state_test

import (
"encoding/json"
"testing"

. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"

"encoding/json"
"testing"

identitytestdata "github.com/s7techlab/cckit/identity/testdata"
"github.com/s7techlab/cckit/state"
"github.com/s7techlab/cckit/state/testdata"
Expand Down
Loading

0 comments on commit 6b3da10

Please sign in to comment.