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 sdk retry mechanism #1140

Merged
merged 4 commits into from
Dec 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## [4.2.45] - 2024.12.20

### Fixed

- Fixed Tatum Connector retry logic causing generic error

## [4.2.44] - 2024.12.5

### Added
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@tatumio/tatum",
"version": "4.2.44",
"version": "4.2.45",
"description": "Tatum JS SDK",
"author": "Tatum",
"repository": "https://github.com/tatumio/tatum-js",
Expand Down
10 changes: 5 additions & 5 deletions src/connector/tatum.connector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,14 +116,14 @@ export class TatumConnector {
}
const response = await res.json()
if (response?.error) {
return await this.retry(url, request, res, retry)
return response
}
return response
}

// Retry only in case of 5xx error
if (res.status >= 500 && res.status < 600) {
return await this.retry(url, request, res, retry)
return await this.retry(url, request, responseBody, retry)
}

throw responseBody
Expand Down Expand Up @@ -181,7 +181,7 @@ export class TatumConnector {
private async retry<RESPONSE>(
url: string,
request: RequestInit,
response: Response,
responseBody: string,
retry: number,
): Promise<RESPONSE | Blob | undefined> {
const { retryDelay, retryCount } = Container.of(this.id).get(CONFIG)
Expand All @@ -191,7 +191,7 @@ export class TatumConnector {
message: `Not retrying the request - no max retry count defined`,
data: { url, requestBody: request.body },
})
return Promise.reject(await response.text())
return Promise.reject(responseBody)
}

if (retry >= retryCount) {
Expand All @@ -200,7 +200,7 @@ export class TatumConnector {
message: `Not retrying the request for the '${retry}' time - exceeded max retry count ${retryCount}: `,
data: { url, requestBody: request.body },
})
return Promise.reject(await response.text())
return Promise.reject(responseBody)
}

retry++
Expand Down
2 changes: 1 addition & 1 deletion src/e2e/rpc/other/tatum.rpc.tezos.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ describe.each([false, true])(`Tezos`, (testnet: boolean) => {
expect(result).toBeDefined()
})

it('getCheckpoint', async () => {
it.skip('getCheckpoint', async () => {
const tatum = await getTezosRpc(testnet)
const result = await tatum.rpc.getCheckpoint({ chainId: 'main' })
await tatum.destroy()
Expand Down
31 changes: 26 additions & 5 deletions src/service/rpc/generic/LoadBalancer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,11 +81,16 @@ export class LoadBalancer implements AbstractRpcInterface {
}
private interval: ReturnType<typeof setInterval>
private network: Network
private evictNodesOnFailure: boolean
private noActiveNode = false

constructor(private readonly id: string) {
this.connector = Container.of(this.id).get(TatumConnector)
this.network = Container.of(this.id).get(CONFIG).network

const config = Container.of(this.id).get(CONFIG)
this.network = config.network
this.evictNodesOnFailure = !!config.rpc?.evictNodesOnFailure

this.logger = Container.of(this.id).get(LOGGER)
}

Expand Down Expand Up @@ -147,6 +152,12 @@ export class LoadBalancer implements AbstractRpcInterface {
}
}

private resetFailedStatuses(nodeType: RpcNodeType) {
for (const server of this.rpcUrls[nodeType]) {
server.failed = false
}
}

private async checkStatuses() {
try {
await this.checkStatus(RpcNodeType.NORMAL)
Expand Down Expand Up @@ -496,20 +507,30 @@ export class LoadBalancer implements AbstractRpcInterface {
throw e
}

const servers = this.rpcUrls[nodeType] as RpcStatus[]

/**
* If the node is not responding, it will be marked as failed.
* New node will be selected and will be used for the given blockchain.
*/
const servers = this.rpcUrls[nodeType] as RpcStatus[]

servers[activeIndex].failed = true

const { index, fastestServer } = LoadBalancer.getFastestServer(
servers,
rpcConfig?.allowedBlocksBehind as number,
)

if (index === -1) {
this.logger.error(
`All RPC nodes are unavailable. Looks like your request is malformed or all nodes are down. Turn on verbose mode to see more details and check status pages.`,
)
if (this.evictNodesOnFailure) {
this.logger.error(
`Looks like your request is malformed or all RPC nodes are down. Turn on verbose mode to see more details and check status pages.`,
)
} else {
// Recover failed nodes, prepare them for the next round of requests
this.resetFailedStatuses(nodeType)
}

throw e
}
Utils.log({
Expand Down
9 changes: 8 additions & 1 deletion src/service/tatum/tatum.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,16 @@ export interface TatumConfig {
nodes?: RpcNode[]

/**
* If this is set to `true`, the SDK will not automatically load balance and failover between the available OpenRPC nodes and will use the fastest URL fetched during the startup. Defaults to `false`.
* If this is set to `true`, the SDK will not automatically load balance and failover between the available OpenRPC nodes and will use the fastest URL fetched during the startup. Defaults to `false` unless there only a single node is provided.
*/
oneTimeLoadBalancing?: boolean

/**
* If this is set to `true`, the SDK will evict nodes from routing pool if they are failing. If `oneTimeLoadBalancing` is set to `true` or you haven't provided your own rpc nodes, this parameter will default to `false`. Defaults to `true` otherwise.
*
* We discourage setting this to `true` if you are using `oneTimeLoadBalancing` as it will result in the SDK not being able to recover a failing node.
*/
evictNodesOnFailure?: boolean
}

/**
Expand Down
22 changes: 20 additions & 2 deletions src/service/tatum/tatum.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Container, Service } from 'typedi'
import { isLoadBalancerNetwork, Network } from '../../dto'
import { Network, isLoadBalancerNetwork } from '../../dto'
import { CONFIG, Constant, EnvUtils, LOGGER, LoggerUtils, Utils } from '../../util'
import {
ExtensionConstructor,
Expand Down Expand Up @@ -84,6 +84,10 @@ export class TatumSDK {

const mergedConfig = Utils.deepMerge(defaultConfig, config) as TatumConfig

if (mergedConfig.rpc && this.shouldEvictNodesOnFailure(mergedConfig)) {
mergedConfig.rpc.evictNodesOnFailure = true
}

LoggerUtils.setLoggerForEnv(mergedConfig, EnvUtils.isDevelopment(), EnvUtils.isBrowser())

// TODO: check when rpc is customized if there is allowedBlocksBehind if not throw error or set default
Expand All @@ -103,7 +107,9 @@ export class TatumSDK {
}

if (isLoadBalancerNetwork(mergedConfig.network)) {
const loadBalancer = Container.of(id).get(mergedConfig.network === Network.TRON ? TronLoadBalancer : LoadBalancer)
const loadBalancer = Container.of(id).get(
mergedConfig.network === Network.TRON ? TronLoadBalancer : LoadBalancer,
)
await loadBalancer.init()
}

Expand Down Expand Up @@ -185,4 +191,16 @@ export class TatumSDK {
}
return result
}

private static shouldEvictNodesOnFailure(config: TatumConfig): boolean | undefined {
if (config.rpc?.evictNodesOnFailure !== undefined && config.rpc?.oneTimeLoadBalancing !== null) {
return config.rpc.evictNodesOnFailure
}

if (!config.rpc?.nodes) {
return false
}

return !config.rpc?.oneTimeLoadBalancing
}
}
Loading