[#902] morph: Avoid creating session in TestInvokeIterator

When the number of items to iterate over is less than 2048, there is no
need to create a session and consume resources.

Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
This commit is contained in:
Evgenii Stratonikov 2024-01-11 13:47:17 +03:00 committed by Evgenii Stratonikov
parent a2ab373a0a
commit be8607a1f6
2 changed files with 44 additions and 11 deletions

View file

@ -13,6 +13,7 @@ import (
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/metrics"
morphmetrics "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/morph/metrics"
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/util/logger"
"github.com/google/uuid"
lru "github.com/hashicorp/golang-lru/v2"
"github.com/nspcc-dev/neo-go/pkg/core/native/noderoles"
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
@ -24,8 +25,10 @@ import (
"github.com/nspcc-dev/neo-go/pkg/rpcclient/nep17"
"github.com/nspcc-dev/neo-go/pkg/rpcclient/rolemgmt"
"github.com/nspcc-dev/neo-go/pkg/rpcclient/unwrap"
"github.com/nspcc-dev/neo-go/pkg/smartcontract"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger"
"github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/vm"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
"github.com/nspcc-dev/neo-go/pkg/vm/vmstate"
"github.com/nspcc-dev/neo-go/pkg/wallet"
@ -204,16 +207,27 @@ func (c *Client) Invoke(contract util.Uint160, fee fixedn.Fixed8, method string,
return vub, nil
}
// defaultPrefetchBatchSize is the default number of items to prefetch.
// It is dependent on VM limits (2048 items on stack), the default works for simple items.
// For example, to iterate over 2 field structs, the limit should be divided by 3 = 1 (struct itself) + 2 (fields).
const defaultPrefetchBatchSize = vm.MaxStackSize - 16
// TestInvokeIterator invokes contract method returning an iterator and executes cb on each element.
// If cb returns an error, the session is closed and this error is returned as-is.
// If the remove neo-go node does not support sessions, `unwrap.ErrNoSessionID` is returned.
func (c *Client) TestInvokeIterator(cb func(stackitem.Item) error, contract util.Uint160, method string, args ...interface{}) error {
// batchSize is the number of items to prefetch: if the number of items in the iterator is less than batchSize, no session will be created.
// The default batchSize is 2000 (VM is limited by having 2048 items on stack, so if each iterator item is simple, 2000 items won't hit the limit).
func (c *Client) TestInvokeIterator(cb func(stackitem.Item) error, batchSize int, contract util.Uint160, method string, args ...interface{}) error {
start := time.Now()
success := false
defer func() {
c.metrics.ObserveInvoke("TestInvokeIterator", contract.String(), method, success, time.Since(start))
}()
if batchSize <= 0 {
batchSize = defaultPrefetchBatchSize
}
c.switchLock.RLock()
defer c.switchLock.RUnlock()
@ -221,35 +235,54 @@ func (c *Client) TestInvokeIterator(cb func(stackitem.Item) error, contract util
return ErrConnectionLost
}
val, err := c.rpcActor.Call(contract, method, args...)
script, err := smartcontract.CreateCallAndPrefetchIteratorScript(contract, method, batchSize, args...)
if err != nil {
return err
}
val, err := c.rpcActor.Run(script)
if err != nil {
return err
} else if val.State != HaltState {
return wrapFrostFSError(&notHaltStateError{state: val.State, exception: val.FaultException})
}
sid, r, err := unwrap.SessionIterator(val, err)
arr, sid, r, err := unwrap.ArrayAndSessionIterator(val, err)
if err != nil {
return err
}
for i := range arr {
if err := cb(arr[i]); err != nil {
return err
}
}
if (sid == uuid.UUID{}) {
success = true
return nil
}
defer func() {
_ = c.rpcActor.TerminateSession(sid)
}()
items, err := c.rpcActor.TraverseIterator(sid, &r, 0)
for err == nil && len(items) != 0 {
for i := range items {
if err = cb(items[i]); err != nil {
for {
items, err := c.rpcActor.TraverseIterator(sid, &r, batchSize)
if err != nil {
return err
}
}
items, err = c.rpcActor.TraverseIterator(sid, &r, 0)
}
success = err == nil
for i := range items {
if err := cb(items[i]); err != nil {
return err
}
}
if len(items) < batchSize {
break
}
}
success = true
return nil
}
// TestInvoke invokes contract method locally in neo-go node. This method should
// be used to read data from smart-contract.

View file

@ -41,7 +41,7 @@ func (c *Client) ContainersOf(idUser *user.ID) ([]cid.ID, error) {
}
cnrHash := c.client.ContractAddress()
err := c.client.Morph().TestInvokeIterator(cb, cnrHash, containersOfMethod, rawID)
err := c.client.Morph().TestInvokeIterator(cb, 0, cnrHash, containersOfMethod, rawID)
if err != nil {
if errors.Is(err, unwrap.ErrNoSessionID) {
return c.List(idUser)