ethers.js ETIMEOUT

hop6·2023년 1월 6일
3
Error: missing revert data in call exception (error=
{"reason":"missing response","code":"SERVER_ERROR",
"requestBody":"{\"method\":\"eth_call\",\"params\":
[{\"to\":\"ADDRESS\",\"data\":\"DATA\"},\"latest\"],
\"id\":88,\"jsonrpc\":\"2.0\"}","requestMethod":"POST",
"serverError":{"errno":-60,"code":"ETIMEDOUT","syscall":"connect",
"address":"ADDRESS","port":8545},"url":"ENDPOINT"}, 
data="0x", code=CALL_EXCEPTION, version=providers/5.5.3)

ethers.js를 이용하여 컨트랙트 호출을 할때 ETIMEOUT 에러가 발생하는 현상이 있다. 나의 경우 주로 jsonrpc provider를 생성한 뒤, 많은 read 요청을 하면 반드시 발생했었다. 스케줄을 걸어놓고 하루 동안 생성된 블록에 대해 약 2분 단위로 데이터를 쿼리하는 작업이었는데, ETIMEOUT 에러가 나는 날에는 다시 스케줄을 돌려야 하는 번거로움이 있었다. 이를 해결하기 위해 ethers.js 내부 코드를 확인해 보았으나 원인을 찾을 수 없었다. 구글을 찾아보니 ethers.js에서 정의한 에러는 아니고 더 로우한 레이어의 커넥션 과정에서 발생하는 에러로 보였다.
회사에서는 Besu 클라이언트를 사용하는데 클라이언트가 많은 요청이 발생하는 경우 응답을 늦게 줘서 에러가 발생하고 있나 싶어 클라이언트 리소스 모니터를 확인해보아도 의미있는 데이터는 찾을 수 없었다.
원인을 찾는 것에 공수가 길어지다보니, 먼저 문제를 해결하기로 결정했다.

제일 단순한 방법은 provider를 재생성해주는 것이다. ETIMEOUT 에러는 '많은' 요청을 할 경우 발생했기 때문에 테스트를 한 뒤 에러가 발생하는 보수적인 횟수를 정하고 재생성 로직을 작성한다.

let provider = providerGenerator();
const refreshInterval = 300;
for (let i = 0; i < 100000; i++) {
     if (i % refreshInterval == 0) {
         provider = providerGenerator();
     }
  
  	 // logic ...
}

나는 주로 일회성 스크립트에서 이 방법을 사용한다.

그러나 서버의 경우 많은 요청을 하는 로직마다 재생성하는 코드를 추가해주어야 하고, 히스토리를 모르는 사람이 기능을 추가하거나 완전히 새로운 로직을 추가할 때 등 신경 써야하는 지점이 하나 더 생기게 된다.

https://github.com/ethers-io/ethers.js/issues/1053 해당 이슈에서는 ethers.js가 reconnect 옵션을 제공하지 않기 때문에 기능 추가에 대한 논의가 이루어졌지만, 백로그에 포함이 되어있으나 오랜 시간이 걸릴 것 같다라는 코멘트를 끝으로 현재까지도 제공하지 않고 있다. 때문에 다른 개발자들은 우회 방법에 대한 논의를 하고 있다. 그 중에 가장 깔끔하다고 생각되는 방법을 소개하려 한다.

web3 라이브러리에서는 reconnect 기능을 지원하기 때문에 이를 적절히 이용하자.

// web3 reconnect 예시
let w3_provider = new (Web3WsProvider as any)(url, {
        clientConfig: {
        keepalive: true,
        keepaliveInterval: 60000,
      },
      reconnect: {
        auto: true,
        delay: 1000,
        maxAttempts: 5,
        onTimeout: false
     }
});

이를 ethers.js provider로 래핑하면 reconnect 기능을 사용하면서도 ethers.js provider 기능을 사용할 수 있다.

let provider = new ethers.providers.Web3Provider(w3_provider)

Websocket을 사용하는 provider는 사용이 끝난 후에 destroy 호출하여 이더리움 노드에 커넥션을 정리해줘야 한다. 그러나 Web3Provider는 disconnect 메서드가 구현되어 있지 않다. 때문에 아래와 같은 방법을 사용하여 destroy 또한 가능하도록 하자.

interface Provider {
    provider: ethers.providers.Web3Provider;
	origin: Web3WsProvider.WebsocketProvider;
}

//
let wProvider: Provider = {
  provider: new ethers.providers.Web3Provider(w3_provider),
  origin: w3_provider
}

// Read 요청
for (let i = 0; i < 100000; i++) {
  // wProvider.provider.SOMETHING
}

// disconnection
wProvider.origin.disconnect();

또는...

class origin {
    private provider: Web3WsProvider.WebsocketProvider;

    constructor(provider: Web3WsProvider.WebsocketProvider){
        this.provider = provider;
    }

    disconnect() {
        this.provider.disconnect()
    }
}

interface Provider {
  provider: ethers.providers.Web3Provider;
  extend: origin;
}

let wProvider: Provider = {
  provider: new ethers.providers.Web3Provider(w3_provider),
  extend: new origin(w3_provider)
}

wProvider.extend.disconnect();

이런식으로 web3 provider에 대한 호출을 최소화하는 것도 괜찮아 보인다.

단 유의해야 할 점이 있다.
아래 코멘트는 이 방법을 직접 사용한 개발자로 보이는 사람의 후기이다.

그래서 websocket provider를 사용하여 연결 중단을 완화하기 위해 이 솔루션을 시도했습니다. 실제로 효과가 있었지만
이 provider 구현으로 전환한 후 요청 수가 10배 증가했습니다.

'block' 메서드를 수신한 다음 다른 HTTP 공급자를 사용하여 eth_getBlockByNumber를 호출합니다.

중단될 수 있는 InfuraWebsocket 공급자를 사용하여 24시간 동안 내 일일 요청 수는 10k였습니다. 이 provider 구현을 사용한 후 일일 한도인 100k에 도달했을 때 정말 놀랐습니다.

아무래도 계속하여 연결이 활성화되어 있는지 확인하기 때문에 트래픽이 증가할 것이다. 나는 회사 내의 노드에 붙어 작업하다 보니 트래픽 증가에 대한 부담은 크지 않았고 reconnection config에 대한 조사나 최적화는 따로 하지 않았다. 그러나 infura와 같은 rate limit이 적용되어 있는 서비스라면 reconnection config 옵션에 대해 더 파악하고 적절한 값을 찾아낸 후 사용할 필요가 있겠다.

0개의 댓글