Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Re-submit transaction in case of throttle at receipt #1072

Merged
merged 16 commits into from
Sep 25, 2024
1 change: 1 addition & 0 deletions errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ var errParameterNull = errors.New("the parameter can't be null")
var errNetworkNameMissing = errors.New("can't derive checksum for ID without knowing which _Network the ID is for")
var errChecksumMissing = errors.New("no checksum provided")
var errLockedSlice = errors.New("slice is locked")
var errNodeIsUnhealthy = errors.New("node is unhealthy")

type ErrInvalidNodeAccountIDSet struct {
NodeAccountID AccountID
Expand Down
8 changes: 4 additions & 4 deletions executable.go
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ func _Execute(client *Client, e Executable) (interface{}, error) {

if !node._IsHealthy() {
txLogger.Trace("node is unhealthy, waiting before continuing", "requestId", e.getLogID(e), "delay", node._Wait().String())
_DelayForAttempt(e.getLogID(e), currentBackoff, attempt, txLogger)
_DelayForAttempt(e.getLogID(e), currentBackoff, attempt, txLogger, errNodeIsUnhealthy)
continue
}

Expand Down Expand Up @@ -325,7 +325,7 @@ func _Execute(client *Client, e Executable) (interface{}, error) {
switch e.shouldRetry(e, resp) {
case executionStateRetry:
errPersistent = statusError
_DelayForAttempt(e.getLogID(e), currentBackoff, attempt, txLogger)
_DelayForAttempt(e.getLogID(e), currentBackoff, attempt, txLogger, errPersistent)
continue
case executionStateExpired:
if e.isTransaction() {
Expand Down Expand Up @@ -364,8 +364,8 @@ func _Execute(client *Client, e Executable) (interface{}, error) {
return &services.Response{}, errPersistent
}

func _DelayForAttempt(logID string, backoff time.Duration, attempt int64, logger Logger) {
logger.Trace("retrying request attempt", "requestId", logID, "delay", backoff, "attempt", attempt+1)
func _DelayForAttempt(logID string, backoff time.Duration, attempt int64, logger Logger, err error) {
logger.Trace("retrying request attempt", "requestId", logID, "delay", backoff, "attempt", attempt+1, "error", err)

time.Sleep(backoff)
}
Expand Down
1 change: 0 additions & 1 deletion query.go
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,6 @@ func (q *Query) shouldRetry(e Executable, response interface{}) _ExecutionState
StatusPlatformTransactionNotCreated: true,
StatusPlatformNotActive: true,
StatusBusy: true,
StatusThrottledAtConsensus: true,
}

if retryableStatuses[status] {
Expand Down
1 change: 1 addition & 0 deletions schedule_create_transaction_e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ func TestIntegrationScheduleCreateTransactionCanExecute(t *testing.T) {
require.NoError(t, err)

transactionReceipt, err := createResponse.SetValidateStatus(true).GetReceipt(env.Client)
require.NoError(t, err)

transactionID := TransactionIDGenerate(env.OperatorID)
newAccountID := *transactionReceipt.AccountID
Expand Down
29 changes: 26 additions & 3 deletions transaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -4578,6 +4578,16 @@
return i.ToBytes()
case *TransferTransaction:
return i.ToBytes()
case *TokenUpdateNfts:
return i.ToBytes()
case *TokenRejectTransaction:
return i.ToBytes()
case *TokenAirdropTransaction:
return i.ToBytes()
case *TokenCancelAirdropTransaction:
return i.ToBytes()
case *TokenClaimAirdropTransaction:
return i.ToBytes()

Check warning on line 4590 in transaction.go

View check run for this annotation

Codecov / codecov/patch

transaction.go#L4581-L4590

Added lines #L4581 - L4590 were not covered by tests
default:
return nil, errors.New("(BUG) non-exhaustive switch statement")
}
Expand Down Expand Up @@ -4749,6 +4759,16 @@
return i.Execute(client)
case *TransferTransaction:
return i.Execute(client)
case *TokenUpdateNfts:
return i.Execute(client)
case *TokenRejectTransaction:
return i.Execute(client)
case *TokenAirdropTransaction:
return i.Execute(client)
case *TokenCancelAirdropTransaction:
return i.Execute(client)
case *TokenClaimAirdropTransaction:
return i.Execute(client)

Check warning on line 4771 in transaction.go

View check run for this annotation

Codecov / codecov/patch

transaction.go#L4762-L4771

Added lines #L4762 - L4771 were not covered by tests
default:
return TransactionResponse{}, errors.New("(BUG) non-exhaustive switch statement")
}
Expand All @@ -4762,7 +4782,6 @@
StatusPlatformTransactionNotCreated: true,
StatusPlatformNotActive: true,
StatusBusy: true,
StatusThrottledAtConsensus: true,
}

if retryableStatuses[status] {
Expand Down Expand Up @@ -4899,12 +4918,16 @@
ValidateStatus: true,
}, err
}

originalTxID := tx.GetTransactionID()
e.regenerateID(client)
return TransactionResponse{
TransactionID: tx.GetTransactionID(),
TransactionID: originalTxID,
NodeID: resp.(TransactionResponse).NodeID,
Hash: resp.(TransactionResponse).Hash,
ValidateStatus: true,
// set the tx in the response, in case of throttle error in the receipt
// we can use this to re-submit the transaction
Transaction: e,
}, nil
}

Expand Down
43 changes: 11 additions & 32 deletions transaction_receipt_query.go
Original file line number Diff line number Diff line change
Expand Up @@ -242,46 +242,25 @@
}

func (q *TransactionReceiptQuery) shouldRetry(_ Executable, response interface{}) _ExecutionState {
receiptResponse := response.(*services.Response).GetTransactionGetReceipt()
header := receiptResponse.GetHeader()

status := Status(header.GetNodeTransactionPrecheckCode())

retryableHeaderStatuses := map[Status]bool{
StatusPlatformTransactionNotCreated: true,
StatusBusy: true,
StatusUnknown: true,
StatusReceiptNotFound: true,
StatusRecordNotFound: true,
StatusPlatformNotActive: true,
StatusThrottledAtConsensus: true,
}
status := Status(response.(*services.Response).GetTransactionGetReceipt().GetHeader().GetNodeTransactionPrecheckCode())

if retryableHeaderStatuses[status] {
switch status {
case StatusPlatformTransactionNotCreated, StatusBusy, StatusUnknown, StatusReceiptNotFound, StatusRecordNotFound, StatusPlatformNotActive:

Check warning on line 248 in transaction_receipt_query.go

View check run for this annotation

Codecov / codecov/patch

transaction_receipt_query.go#L248

Added line #L248 was not covered by tests
return executionStateRetry
}

if status != StatusOk {
case StatusOk:
break
default:
return executionStateError

Check warning on line 253 in transaction_receipt_query.go

View check run for this annotation

Codecov / codecov/patch

transaction_receipt_query.go#L252-L253

Added lines #L252 - L253 were not covered by tests
}

status = Status(receiptResponse.GetReceipt().GetStatus())

retryableReceiptStatuses := map[Status]bool{
StatusBusy: true,
StatusUnknown: true,
StatusOk: true,
StatusReceiptNotFound: true,
StatusRecordNotFound: true,
StatusPlatformNotActive: true,
StatusThrottledAtConsensus: true,
}
status = Status(response.(*services.Response).GetTransactionGetReceipt().GetReceipt().GetStatus())

if retryableReceiptStatuses[status] {
switch status {
case StatusBusy, StatusUnknown, StatusOk, StatusReceiptNotFound, StatusRecordNotFound, StatusPlatformNotActive:
return executionStateRetry
default:
return executionStateFinished
}

return executionStateFinished
}

func (q *TransactionReceiptQuery) getQueryResponse(response *services.Response) queryResponse {
Expand Down
47 changes: 0 additions & 47 deletions transaction_receipt_query_unit_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,53 +115,6 @@ func TestUnitTransactionReceiptQueryNothingSet(t *testing.T) {
balance.GetMaxQueryPayment()
}

func TestUnitTransactionReceiptThrottledAtConsensusGracefulHandling(t *testing.T) {
t.Parallel()

responses := [][]interface{}{{
&services.TransactionResponse{
NodeTransactionPrecheckCode: services.ResponseCodeEnum_OK,
},
&services.Response{
Response: &services.Response_TransactionGetReceipt{
TransactionGetReceipt: &services.TransactionGetReceiptResponse{
Header: &services.ResponseHeader{
Cost: 0,
ResponseType: services.ResponseType_ANSWER_ONLY,
},
Receipt: &services.TransactionReceipt{
Status: services.ResponseCodeEnum_THROTTLED_AT_CONSENSUS,
},
},
},
},
&services.Response{
Response: &services.Response_TransactionGetReceipt{
TransactionGetReceipt: &services.TransactionGetReceiptResponse{
Header: &services.ResponseHeader{
Cost: 0,
ResponseType: services.ResponseType_ANSWER_ONLY,
},
Receipt: &services.TransactionReceipt{
Status: services.ResponseCodeEnum_SUCCESS,
},
},
},
},
}}
client, server := NewMockClientAndServer(responses)
defer server.Close()
tx, err := NewTransferTransaction().
SetNodeAccountIDs([]AccountID{{Account: 3}}).
AddHbarTransfer(AccountID{Account: 2}, HbarFromTinybar(-1)).
AddHbarTransfer(AccountID{Account: 3}, HbarFromTinybar(1)).
Execute(client)
client.SetMaxAttempts(2)
require.NoError(t, err)
_, err = tx.SetValidateStatus(true).GetReceipt(client)
require.NoError(t, err)
}

func TestUnitTransactionPlatformNotActiveGracefulHandling(t *testing.T) {
t.Parallel()

Expand Down
51 changes: 15 additions & 36 deletions transaction_record_query.go
Original file line number Diff line number Diff line change
Expand Up @@ -247,50 +247,29 @@
}

func (q *TransactionRecordQuery) shouldRetry(_ Executable, response interface{}) _ExecutionState {
record := response.(*services.Response).GetTransactionGetRecord()
header := record.GetHeader()

status := Status(header.GetNodeTransactionPrecheckCode())

retryableHeaderStatuses := map[Status]bool{
StatusPlatformTransactionNotCreated: true,
StatusBusy: true,
StatusUnknown: true,
StatusReceiptNotFound: true,
StatusRecordNotFound: true,
StatusPlatformNotActive: true,
StatusThrottledAtConsensus: true,
}
status := Status(response.(*services.Response).GetTransactionGetRecord().GetHeader().GetNodeTransactionPrecheckCode())

if retryableHeaderStatuses[status] {
switch status {
case StatusPlatformTransactionNotCreated, StatusBusy, StatusUnknown, StatusReceiptNotFound, StatusRecordNotFound, StatusPlatformNotActive:
return executionStateRetry
case StatusOk:
if response.(*services.Response).GetTransactionGetRecord().GetHeader().ResponseType == services.ResponseType_COST_ANSWER {
return executionStateFinished
}
default:
return executionStateError
}

if status == StatusOk && header.ResponseType == services.ResponseType_COST_ANSWER {
return executionStateFinished
}

status = Status(record.GetTransactionRecord().GetReceipt().GetStatus())
status = Status(response.(*services.Response).GetTransactionGetRecord().GetTransactionRecord().GetReceipt().GetStatus())

retryableReceiptStatuses := map[Status]bool{
StatusBusy: true,
StatusUnknown: true,
StatusOk: true,
StatusReceiptNotFound: true,
StatusRecordNotFound: true,
StatusPlatformNotActive: true,
StatusThrottledAtConsensus: true,
}

if retryableReceiptStatuses[status] {
switch status {
case StatusBusy, StatusUnknown, StatusOk, StatusReceiptNotFound, StatusRecordNotFound, StatusPlatformNotActive:
return executionStateRetry
}

if status == StatusSuccess {
case StatusSuccess:
return executionStateFinished
default:
return executionStateError

Check warning on line 271 in transaction_record_query.go

View check run for this annotation

Codecov / codecov/patch

transaction_record_query.go#L270-L271

Added lines #L270 - L271 were not covered by tests
}

return executionStateError
}

func (q *TransactionRecordQuery) getQueryResponse(response *services.Response) queryResponse {
Expand Down
95 changes: 0 additions & 95 deletions transaction_record_query_unit_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,101 +123,6 @@ func TestUnitTransactionRecordQueryNothingSet(t *testing.T) {
require.Equal(t, Hbar{}, query.GetMaxQueryPayment())
}

func TestUnitTransactionRecordThrottledAtConsensusGracefulHandling(t *testing.T) {
t.Parallel()

responses := [][]interface{}{{
&services.TransactionResponse{
NodeTransactionPrecheckCode: services.ResponseCodeEnum_OK,
},
&services.Response{
Response: &services.Response_TransactionGetReceipt{
TransactionGetReceipt: &services.TransactionGetReceiptResponse{
Header: &services.ResponseHeader{
Cost: 0,
ResponseType: services.ResponseType_ANSWER_ONLY,
},
Receipt: &services.TransactionReceipt{
Status: services.ResponseCodeEnum_SUCCESS,
},
},
},
},
&services.Response{
Response: &services.Response_TransactionGetRecord{
TransactionGetRecord: &services.TransactionGetRecordResponse{
Header: &services.ResponseHeader{
Cost: 0,
ResponseType: services.ResponseType_ANSWER_ONLY,
},
TransactionRecord: &services.TransactionRecord{
Receipt: &services.TransactionReceipt{
Status: services.ResponseCodeEnum_THROTTLED_AT_CONSENSUS,
},
},
},
},
},
&services.Response{
Response: &services.Response_TransactionGetRecord{
TransactionGetRecord: &services.TransactionGetRecordResponse{
Header: &services.ResponseHeader{
Cost: 0,
ResponseType: services.ResponseType_ANSWER_ONLY,
},
TransactionRecord: &services.TransactionRecord{
Receipt: &services.TransactionReceipt{
Status: services.ResponseCodeEnum_SUCCESS,
},
},
},
},
},
&services.Response{
Response: &services.Response_TransactionGetRecord{
TransactionGetRecord: &services.TransactionGetRecordResponse{
Header: &services.ResponseHeader{
Cost: 0,
ResponseType: services.ResponseType_ANSWER_ONLY,
},
TransactionRecord: &services.TransactionRecord{
Receipt: &services.TransactionReceipt{
Status: services.ResponseCodeEnum_THROTTLED_AT_CONSENSUS,
},
},
},
},
},
&services.Response{
Response: &services.Response_TransactionGetRecord{
TransactionGetRecord: &services.TransactionGetRecordResponse{
Header: &services.ResponseHeader{
Cost: 0,
ResponseType: services.ResponseType_ANSWER_ONLY,
},
TransactionRecord: &services.TransactionRecord{
Receipt: &services.TransactionReceipt{
Status: services.ResponseCodeEnum_SUCCESS,
},
},
},
},
},
}}

client, server := NewMockClientAndServer(responses)
defer server.Close()
tx, err := NewTransferTransaction().
SetNodeAccountIDs([]AccountID{{Account: 3}}).
AddHbarTransfer(AccountID{Account: 2}, HbarFromTinybar(-1)).
AddHbarTransfer(AccountID{Account: 3}, HbarFromTinybar(1)).
Execute(client)
client.SetMaxAttempts(2)
require.NoError(t, err)
_, err = tx.SetValidateStatus(true).GetRecord(client)
require.NoError(t, err)
}

func TestUnitTransactionRecordPlatformNotActiveGracefulHandling(t *testing.T) {
t.Parallel()

Expand Down
Loading
Loading