diff --git a/commonclient/waiter.go b/commonclient/waiter.go index 81041f2..2a86043 100644 --- a/commonclient/waiter.go +++ b/commonclient/waiter.go @@ -13,6 +13,12 @@ import ( const alreadyExistsError = "already exists" +// ContextWaiter is an interface that supports transaction awaiting with a context limit. +type ContextWaiter interface { + waiter.Waiter + WaitCtx(ctx context.Context, h util.Uint256, vub uint32, err error) (*state.AppExecResult, error) +} + type WaiterOptions struct { // IgnoreAlreadyExistsError controls behavior for "already exists" error: // - If set to true, it indicates that "already exists" error is not a problem, we should @@ -52,6 +58,19 @@ func (w *Waiter) Wait(h util.Uint256, vub uint32, err error) (*state.AppExecResu return w.examineExecResult(result, err) } +// WaitCtx allows to wait until transaction is accepted to the chain. +// Waiting is limited by the specified context. +func (w *Waiter) WaitCtx(ctx context.Context, h util.Uint256, vub uint32, err error) (*state.AppExecResult, error) { + if err != nil { + shouldIgnore := errIsAlreadyExists(err) && w.options.IgnoreAlreadyExistsError + if !shouldIgnore { + return nil, err + } + } + result, err := w.waiter.WaitAny(ctx, vub, h) + return w.examineExecResult(result, err) +} + // WaitAny waits until at least one of the specified transactions will be accepted // to the chain. func (w *Waiter) WaitAny(ctx context.Context, vub uint32, hashes ...util.Uint256) (*state.AppExecResult, error) { diff --git a/commonclient/waiter_test.go b/commonclient/waiter_test.go index 6e8c0ee..a614900 100644 --- a/commonclient/waiter_test.go +++ b/commonclient/waiter_test.go @@ -65,9 +65,11 @@ func (m *mockWaiter) WaitAny(ctx context.Context, vub uint32, hashes ...util.Uin func TestWaiter(t *testing.T) { txHash := util.Uint256{} + txHashes := []util.Uint256{txHash} + vub := uint32(100) - t.Run("ignore already exists error", func(t *testing.T) { + t.Run("wait ignores already exists error", func(t *testing.T) { sendErr := fmt.Errorf("transaction already exists") mw := &mockWaiter{} mw.On("Wait", txHash, vub, mock.Anything).Return(mw.successfulResult(txHash), nil) @@ -78,7 +80,7 @@ func TestWaiter(t *testing.T) { require.NoError(t, err) }) - t.Run("report already exists error", func(t *testing.T) { + t.Run("wait reports already exists error", func(t *testing.T) { sendErr := fmt.Errorf("transaction already exists") mw := &mockWaiter{} mw.On("Wait", txHash, vub, mock.Anything).Return(mw.successfulResult(txHash), nil) @@ -89,7 +91,7 @@ func TestWaiter(t *testing.T) { require.Error(t, err) }) - t.Run("report wait error when transaction error is ignored", func(t *testing.T) { + t.Run("wait reports wait error when transaction error is ignored", func(t *testing.T) { waitErr := fmt.Errorf("mock error") mw := &mockWaiter{} mw.On("Wait", txHash, vub, nil).Return(nil, waitErr) @@ -100,7 +102,7 @@ func TestWaiter(t *testing.T) { require.ErrorIs(t, err, waitErr) }) - t.Run("report wait error when transaction error is verified", func(t *testing.T) { + t.Run("wait reports wait error when transaction error is verified", func(t *testing.T) { waitErr := fmt.Errorf("mock error") mw := &mockWaiter{} mw.On("Wait", txHash, vub, nil).Return(nil, waitErr) @@ -111,7 +113,7 @@ func TestWaiter(t *testing.T) { require.ErrorIs(t, err, waitErr) }) - t.Run("ignore error from transaction", func(t *testing.T) { + t.Run("wait ignores error from transaction", func(t *testing.T) { txError := "mock error" mw := &mockWaiter{} mw.On("Wait", txHash, vub, nil).Return(mw.failedResult(txHash, txError), nil) @@ -122,7 +124,7 @@ func TestWaiter(t *testing.T) { require.NoError(t, err) }) - t.Run("examine error from transaction", func(t *testing.T) { + t.Run("wait examines error from transaction", func(t *testing.T) { txError := "mock error" mw := &mockWaiter{} mw.On("Wait", txHash, vub, nil).Return(mw.failedResult(txHash, txError), nil) @@ -132,4 +134,76 @@ func TestWaiter(t *testing.T) { require.ErrorContains(t, err, txError) }) + + t.Run("waitctx ignores already exists error", func(t *testing.T) { + sendErr := fmt.Errorf("transaction already exists") + mw := &mockWaiter{} + mw.On("WaitAny", mock.Anything, vub, txHashes).Return(mw.successfulResult(txHash), nil) + + waiter := NewWaiter(mw, WaiterOptions{IgnoreAlreadyExistsError: true}) + _, err := waiter.WaitCtx(context.Background(), txHash, vub, sendErr) + + require.NoError(t, err) + mw.AssertNotCalled(t, "Wait", mock.Anything, mock.Anything, mock.Anything) + }) + + t.Run("waitctx reports already exists error", func(t *testing.T) { + sendErr := fmt.Errorf("transaction already exists") + mw := &mockWaiter{} + mw.On("WaitAny", mock.Anything, vub, txHashes).Return(mw.successfulResult(txHash), nil) + + waiter := NewWaiter(mw, WaiterOptions{IgnoreAlreadyExistsError: false}) + _, err := waiter.WaitCtx(context.Background(), txHash, vub, sendErr) + + require.Error(t, err) + mw.AssertNotCalled(t, "Wait", mock.Anything, mock.Anything, mock.Anything) + }) + + t.Run("waitctx reports wait error when transaction error is ignored", func(t *testing.T) { + waitErr := fmt.Errorf("mock error") + mw := &mockWaiter{} + mw.On("WaitAny", mock.Anything, vub, txHashes).Return(nil, waitErr) + + waiter := NewWaiter(mw, WaiterOptions{VerifyExecResults: false}) + _, err := waiter.WaitCtx(context.Background(), txHash, vub, nil) + + require.ErrorIs(t, err, waitErr) + mw.AssertNotCalled(t, "Wait", mock.Anything, mock.Anything, mock.Anything) + }) + + t.Run("waitctx reports wait error when transaction error is verified", func(t *testing.T) { + waitErr := fmt.Errorf("mock error") + mw := &mockWaiter{} + mw.On("WaitAny", mock.Anything, vub, txHashes).Return(nil, waitErr) + + waiter := NewWaiter(mw, WaiterOptions{VerifyExecResults: true}) + _, err := waiter.WaitCtx(context.Background(), txHash, vub, nil) + + require.ErrorIs(t, err, waitErr) + mw.AssertNotCalled(t, "Wait", mock.Anything, mock.Anything, mock.Anything) + }) + + t.Run("waitctx ignores error from transaction", func(t *testing.T) { + txError := "mock error" + mw := &mockWaiter{} + mw.On("WaitAny", mock.Anything, vub, txHashes).Return(mw.failedResult(txHash, txError), nil) + + waiter := NewWaiter(mw, WaiterOptions{VerifyExecResults: false}) + _, err := waiter.WaitCtx(context.Background(), txHash, vub, nil) + + require.NoError(t, err) + mw.AssertNotCalled(t, "Wait", mock.Anything, mock.Anything, mock.Anything) + }) + + t.Run("waitctx examines error from transaction", func(t *testing.T) { + txError := "mock error" + mw := &mockWaiter{} + mw.On("WaitAny", mock.Anything, vub, txHashes).Return(mw.failedResult(txHash, txError), nil) + + waiter := NewWaiter(mw, WaiterOptions{VerifyExecResults: true}) + _, err := waiter.WaitCtx(context.Background(), txHash, vub, nil) + + require.ErrorContains(t, err, txError) + mw.AssertNotCalled(t, "Wait", mock.Anything, mock.Anything, mock.Anything) + }) } diff --git a/frostfsid/client/client.go b/frostfsid/client/client.go index 8588d53..c5b6bf2 100644 --- a/frostfsid/client/client.go +++ b/frostfsid/client/client.go @@ -1,6 +1,7 @@ package client import ( + "context" "fmt" "git.frostfs.info/TrueCloudLab/frostfs-contract/commonclient" @@ -11,7 +12,6 @@ import ( "github.com/nspcc-dev/neo-go/pkg/rpcclient/actor" "github.com/nspcc-dev/neo-go/pkg/rpcclient/notary" "github.com/nspcc-dev/neo-go/pkg/rpcclient/unwrap" - "github.com/nspcc-dev/neo-go/pkg/rpcclient/waiter" "github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag" "github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/vm/emit" @@ -23,7 +23,7 @@ import ( type ( Client struct { act *actor.Actor - waiter waiter.Waiter + waiter commonclient.ContextWaiter contract util.Uint160 } @@ -614,12 +614,13 @@ func (c Client) ListNonEmptyNamespaces() ([]string, error) { } // Wait waits until the specified transaction is accepted to the chain. +// Deprecated: use [Waiter] method instead. func (c Client) Wait(tx util.Uint256, vub uint32, err error) (*state.AppExecResult, error) { - return c.Waiter().Wait(tx, vub, err) + return c.Waiter().WaitCtx(context.TODO(), tx, vub, err) } -// Waiter returns underlying waiter.Waiter. -func (c Client) Waiter() waiter.Waiter { +// Waiter returns underlying waiter. +func (c Client) Waiter() commonclient.ContextWaiter { return c.waiter } diff --git a/tests/frostfsid_client_test.go b/tests/frostfsid_client_test.go index 6516e75..3a0b1cd 100644 --- a/tests/frostfsid_client_test.go +++ b/tests/frostfsid_client_test.go @@ -191,6 +191,7 @@ func TestFrostFSID_Client_NamespaceManagement(t *testing.T) { } func TestFrostFSID_Client_DefaultNamespace(t *testing.T) { + ctx := context.Background() ffsid, cancel := initFrostfsIFClientTest(t) defer cancel() @@ -198,7 +199,9 @@ func TestFrostFSID_Client_DefaultNamespace(t *testing.T) { subjKey, subjAddr := newKey(t) ffsid.a.await(ffsid.cli.CreateSubject(defaultNamespace, subjKey.PublicKey())) - groupID, err := ffsid.cli.ParseGroupID(ffsid.cli.Wait(ffsid.cli.CreateGroup(defaultNamespace, group))) + + txHash, vub, err := ffsid.cli.CreateGroup(defaultNamespace, group) + groupID, err := ffsid.cli.ParseGroupID(ffsid.cli.Waiter().WaitCtx(ctx, txHash, vub, err)) require.NoError(t, err) ffsid.a.await(ffsid.cli.AddSubjectToGroup(subjAddr, groupID)) @@ -226,6 +229,7 @@ func TestFrostFSID_Client_DefaultNamespace(t *testing.T) { } func TestFrostFSID_Client_GroupManagement(t *testing.T) { + ctx := context.Background() ffsid, cancel := initFrostfsIFClientTest(t) defer cancel() @@ -237,7 +241,8 @@ func TestFrostFSID_Client_GroupManagement(t *testing.T) { groupName := "group" groupID := int64(1) - actGroupID, err := ffsid.cli.ParseGroupID(ffsid.cli.Wait(ffsid.cli.CreateGroup(namespace, groupName))) + txHash, vub, err := ffsid.cli.CreateGroup(namespace, groupName) + actGroupID, err := ffsid.cli.ParseGroupID(ffsid.cli.Waiter().WaitCtx(ctx, txHash, vub, err)) require.NoError(t, err) require.Equal(t, groupID, actGroupID) @@ -538,11 +543,13 @@ func TestFrostFSID_Client_GetSubjectByName(t *testing.T) { } func TestFrostFSID_Client_GetGroupByName(t *testing.T) { + ctx := context.Background() ffsid, cancel := initFrostfsIFClientTest(t) defer cancel() groupName := "group" - actGroupID, err := ffsid.cli.ParseGroupID(ffsid.cli.Wait(ffsid.cli.CreateGroup(defaultNamespace, groupName))) + txHash, vub, err := ffsid.cli.CreateGroup(defaultNamespace, groupName) + actGroupID, err := ffsid.cli.ParseGroupID(ffsid.cli.Waiter().WaitCtx(ctx, txHash, vub, err)) require.NoError(t, err) group, err := ffsid.cli.GetGroupByName(defaultNamespace, groupName)