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.Debug("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
10 changes: 7 additions & 3 deletions transaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -4762,7 +4762,6 @@ func (tx *Transaction) shouldRetry(_ Executable, response interface{}) _Executio
StatusPlatformTransactionNotCreated: true,
StatusPlatformNotActive: true,
StatusBusy: true,
StatusThrottledAtConsensus: true,
}

if retryableStatuses[status] {
Expand Down Expand Up @@ -4899,12 +4898,17 @@ func (tx *Transaction) execute(client *Client, e TransactionInterface) (Transact
ValidateStatus: true,
}, err
}

originalTxID := tx.GetTransactionID()
e.regenerateID(client)
txBytes, _ := TransactionToBytes(e)
return TransactionResponse{
TransactionID: tx.GetTransactionID(),
TransactionID: originalTxID,
NodeID: resp.(TransactionResponse).NodeID,
Hash: resp.(TransactionResponse).Hash,
ValidateStatus: true,
// set the txBytes in the response, in case of throttle error in the receipt
// we can use this to re-submit the transaction
Transaction: txBytes,
}, 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) validateNetworkOnIDs(client *Client) error {
}

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:
return executionStateRetry
}

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

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) validateNetworkOnIDs(client *Client) error {
}

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
}

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