Skip to content

Commit

Permalink
Use exponential backoff for QueryBuilder#execute
Browse files Browse the repository at this point in the history
while making sure that Receipt and Record queries additionally retry on OK and UNKNOWN response codes
  • Loading branch information
qtbeee committed Nov 15, 2019
1 parent c60a136 commit 2879677
Show file tree
Hide file tree
Showing 5 changed files with 45 additions and 40 deletions.
2 changes: 1 addition & 1 deletion Sources/Hedera/Backoff.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import Foundation

enum Backoff {
static let receiptInitialDelay: UInt32 = 1
static let initialDelay: UInt32 = 1
static let receiptRetryDelay: TimeInterval = 0.5

static func getDelayUs(startTime: Date, attempt: UInt8) -> UInt32? {
Expand Down
44 changes: 35 additions & 9 deletions Sources/Hedera/QueryBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -137,14 +137,11 @@ public class QueryBuilder<Response> {
do {
return try executeAsync().wait()
} catch {
// FIXME
return .failure(HederaError(message: "RPC error: \(error)"))
}
}

public func executeAsync() -> EventLoopFuture<Result<Response, HederaError>> {
let grpcClient = client.grpcClient(for: node)

if needsPayment && !header.hasPayment {
switch requestCost() {
case .success(let cost):
Expand All @@ -160,16 +157,45 @@ public class QueryBuilder<Response> {
}
}

return methodForQuery(grpcClient)(body, nil).response.map { (response) -> Result<Response, HederaError> in
let header = self.getResponseHeader(response)
if header.nodeTransactionPrecheckCode != .ok && header.nodeTransactionPrecheckCode != .success {
return .failure(HederaError(message: "Received error code: \(header.nodeTransactionPrecheckCode) while executing"))
} else {
return self.mapResponse(response)
return client.eventLoopGroup.next().submit {
let startTime = Date()
var attempt: UInt8 = 0

sleep(Backoff.initialDelay)

while(true) {
attempt += 1

let response = Result { try self.methodForQuery(self.client.grpcClient(for: self.node))(self.body, nil).response.wait() }
switch response {
case .success(let response):
let header = self.getResponseHeader(response)
let precheck = header.nodeTransactionPrecheckCode

if precheck == .ok || precheck == .success {
return self.mapResponse(response)

} else if self.shouldRetry(precheck) {
// stop trying if the delay will put us over `validDuration`
guard let delayUs = Backoff.getDelayUs(startTime: startTime, attempt: attempt) else {
return .failure(HederaError(message: "execute timed out"))
}

usleep(delayUs)
} else {
return .failure(HederaError(message: "preCheckCode was not OK: \(precheck)"))
}
case .failure(let error):
return .failure(HederaError(message: "\(error)"))
}
}
}
}

func shouldRetry(_ precheckCode: Proto_ResponseCodeEnum) -> Bool {
precheckCode == .busy
}

func mapResponse(_ response: Proto_Response) -> Result<Response, HederaError> {
fatalError("mapResponse member must be overridden")
}
Expand Down
31 changes: 1 addition & 30 deletions Sources/Hedera/Transaction.swift
Original file line number Diff line number Diff line change
Expand Up @@ -65,27 +65,6 @@ public class Transaction {
inner
}

// func executeAndWaitFor<T>(_ client: Client, mapResponse: (TransactionReceipt) -> T) -> Result<T, HederaError> {
// let startTime = Date()
// var attempt: UInt8 = 0

// // There's no point asking for the receipt of a transaction that failed to go through
// switch execute() {
// case .failure(let error):
// return .failure(error)
// default:
// break
// }

// sleep(receiptInitialDelay)

// while true {
//
// }
// }



// MARK: - Public API

public convenience init?(_ client: Client?, bytes: Data) {
Expand Down Expand Up @@ -144,19 +123,11 @@ public class Transaction {
return client.eventLoopGroup.next().makeFailedFuture(HederaError(message: "node ID for transaction not found in Client"))
}

// return methodForTransaction(client.grpcClient(for: node))(inner, nil).response.map { response -> Result<TransactionId, HederaError> in
// if response.nodeTransactionPrecheckCode == .ok {
// return .success(self.txId)
// } else {
// return .failure(HederaError(message: "preCheckCode was not OK: \(response.nodeTransactionPrecheckCode)"))
// }
// }

return client.eventLoopGroup.next().submit {
let startTime = Date()
var attempt: UInt8 = 0

sleep(Backoff.receiptInitialDelay)
sleep(Backoff.initialDelay)

while(true) {
attempt += 1
Expand Down
4 changes: 4 additions & 0 deletions Sources/Hedera/TransactionReceiptQuery.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ public class TransactionReceiptQuery: QueryBuilder<TransactionReceipt> {
return self
}

override func shouldRetry(_ precheckCode: Proto_ResponseCodeEnum) -> Bool {
precheckCode == .busy || precheckCode == .unknown || precheckCode == .ok
}

override func mapResponse(_ response: Proto_Response) -> Result<TransactionReceipt, HederaError> {
guard case .transactionGetReceipt(let response) = response.response else {
return .failure(HederaError(message: "query response is not of type transaction receipt"))
Expand Down
4 changes: 4 additions & 0 deletions Sources/Hedera/TransactionRecordQuery.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ public final class TransactionRecordQuery: QueryBuilder<TransactionRecord> {
return self
}

override func shouldRetry(_ precheckCode: Proto_ResponseCodeEnum) -> Bool {
precheckCode == .busy || precheckCode == .unknown || precheckCode == .ok
}

override func mapResponse(_ response: Proto_Response) -> Result<TransactionRecord, HederaError> {
guard case .transactionGetRecord(let response) = response.response else {
return .failure(HederaError(message: "query response was not of type 'transactionGetRecord'"))
Expand Down

0 comments on commit 2879677

Please sign in to comment.