solution: mark Upstream status right after WS connect/disconnect

This commit is contained in:
Igor Artamonov
2021-09-22 17:08:43 -04:00
parent 1f864bdbe3
commit bd8c0f7c19
7 changed files with 55 additions and 14 deletions

View File

@@ -61,7 +61,7 @@ abstract class DefaultUpstream(
return status.get().status return status.get().status
} }
fun setStatus(avail: UpstreamAvailability) { open fun setStatus(avail: UpstreamAvailability) {
status.updateAndGet { curr -> status.updateAndGet { curr ->
Status(curr.lag, avail, statusByLag(curr.lag, avail)) Status(curr.lag, avail, statusByLag(curr.lag, avail))
} }

View File

@@ -74,7 +74,8 @@ open class EthereumRpcUpstream(
open fun createHead(): Head { open fun createHead(): Head {
return if (ethereumWsFactory != null) { return if (ethereumWsFactory != null) {
val ws = ethereumWsFactory.create(null).apply { // do not set upstream to the WS, since it doesn't control the RPC upstream
val ws = ethereumWsFactory.create(null, null, null).apply {
connect() connect()
} }
val wsHead = EthereumWsHead(ws).apply { val wsHead = EthereumWsHead(ws).apply {

View File

@@ -32,7 +32,7 @@ import reactor.core.scheduler.Schedulers
import java.time.Duration import java.time.Duration
import java.util.concurrent.Executors import java.util.concurrent.Executors
class EthereumUpstreamValidator( open class EthereumUpstreamValidator(
private val upstream: EthereumUpstream, private val upstream: EthereumUpstream,
private val options: UpstreamsConfig.Options private val options: UpstreamsConfig.Options
) { ) {
@@ -43,7 +43,7 @@ class EthereumUpstreamValidator(
private val objectMapper: ObjectMapper = Global.objectMapper private val objectMapper: ObjectMapper = Global.objectMapper
fun validate(): Mono<UpstreamAvailability> { open fun validate(): Mono<UpstreamAvailability> {
return upstream return upstream
.getApi() .getApi()
.read(JsonRpcRequest("eth_syncing", listOf())) .read(JsonRpcRequest("eth_syncing", listOf()))

View File

@@ -21,6 +21,8 @@ import io.emeraldpay.dshackle.Global
import io.emeraldpay.dshackle.SilentException import io.emeraldpay.dshackle.SilentException
import io.emeraldpay.dshackle.config.AuthConfig import io.emeraldpay.dshackle.config.AuthConfig
import io.emeraldpay.dshackle.data.BlockContainer import io.emeraldpay.dshackle.data.BlockContainer
import io.emeraldpay.dshackle.upstream.DefaultUpstream
import io.emeraldpay.dshackle.upstream.UpstreamAvailability
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
import io.emeraldpay.dshackle.upstream.rpcclient.ResponseWSParser import io.emeraldpay.dshackle.upstream.rpcclient.ResponseWSParser
@@ -64,15 +66,17 @@ class EthereumWsFactory(
var basicAuth: AuthConfig.ClientBasicAuth? = null var basicAuth: AuthConfig.ClientBasicAuth? = null
fun create(rpcMetrics: RpcMetrics?): EthereumWs { fun create(upstream: DefaultUpstream?, validator: EthereumUpstreamValidator?, rpcMetrics: RpcMetrics?): EthereumWs {
return EthereumWs(uri, origin, basicAuth, rpcMetrics) return EthereumWs(uri, origin, basicAuth, rpcMetrics, upstream, validator)
} }
class EthereumWs( class EthereumWs(
private val uri: URI, private val uri: URI,
private val origin: URI, private val origin: URI,
private val basicAuth: AuthConfig.ClientBasicAuth?, private val basicAuth: AuthConfig.ClientBasicAuth?,
private val rpcMetrics: RpcMetrics? private val rpcMetrics: RpcMetrics?,
private val upstream: DefaultUpstream?,
private val validator: EthereumUpstreamValidator?
) : AutoCloseable { ) : AutoCloseable {
companion object { companion object {
@@ -154,6 +158,8 @@ class EthereumWsFactory(
connection = HttpClient.create() connection = HttpClient.create()
.doOnDisconnected { .doOnDisconnected {
log.info("Disconnected from $uri") log.info("Disconnected from $uri")
// mark upstream as UNAVAIL
upstream?.setStatus(UpstreamAvailability.UNAVAILABLE)
if (keepConnection) { if (keepConnection) {
tryReconnectLater() tryReconnectLater()
} }
@@ -203,6 +209,9 @@ class EthereumWsFactory(
//restart backoff after connection //restart backoff after connection
currentBackOff = reconnectBackoff.start() currentBackOff = reconnectBackoff.start()
//validate the connection, it can also be UNAVAIL if market as such after disconnect
validator?.validate()
val consumer = inbound val consumer = inbound
// Accept up to 15Mb messages, same config is used by Geth // Accept up to 15Mb messages, same config is used by Geth
.aggregateFrames(15 * 1024 * 1024) .aggregateFrames(15 * 1024 * 1024)

View File

@@ -53,6 +53,7 @@ class EthereumWsUpstream(
private val api: JsonRpcWsClient private val api: JsonRpcWsClient
private var validatorSubscription: Disposable? = null private var validatorSubscription: Disposable? = null
private val validator: EthereumUpstreamValidator
init { init {
val metricsTags = listOf( val metricsTags = listOf(
@@ -72,7 +73,9 @@ class EthereumWsUpstream(
.register(Metrics.globalRegistry) .register(Metrics.globalRegistry)
) )
connection = ethereumWsFactory.create(metrics) validator = EthereumUpstreamValidator(this, getOptions())
connection = ethereumWsFactory.create(this, validator, metrics)
head = EthereumWsHead(connection) head = EthereumWsHead(connection)
api = JsonRpcWsClient(connection) api = JsonRpcWsClient(connection)
} }
@@ -102,7 +105,6 @@ class EthereumWsUpstream(
head.start() head.start()
log.debug("Start validation for upstream ${this.getId()}") log.debug("Start validation for upstream ${this.getId()}")
val validator = EthereumUpstreamValidator(this, getOptions())
validatorSubscription = validator.start() validatorSubscription = validator.start()
.subscribe(this::setStatus) .subscribe(this::setStatus)
} }

View File

@@ -1,6 +1,8 @@
package io.emeraldpay.dshackle.upstream.ethereum package io.emeraldpay.dshackle.upstream.ethereum
import io.emeraldpay.dshackle.test.MockWSServer import io.emeraldpay.dshackle.test.MockWSServer
import io.emeraldpay.dshackle.upstream.DefaultUpstream
import io.emeraldpay.dshackle.upstream.UpstreamAvailability
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
import reactor.test.StepVerifier import reactor.test.StepVerifier
import spock.lang.Shared import spock.lang.Shared
@@ -28,7 +30,7 @@ class EthereumWsFactoryRealSpec extends Specification {
server = new MockWSServer(port) server = new MockWSServer(port)
server.start() server.start()
Thread.sleep(SLEEP) Thread.sleep(SLEEP)
conn = new EthereumWsFactory("ws://localhost:${port}".toURI(), "http://localhost:${port}".toURI()).create(null) conn = new EthereumWsFactory("ws://localhost:${port}".toURI(), "http://localhost:${port}".toURI()).create(null, null, null)
} }
def cleanup() { def cleanup() {
@@ -91,6 +93,33 @@ class EthereumWsFactoryRealSpec extends Specification {
act[0].value.contains("\"params\":[\"newHeads\"]") act[0].value.contains("\"params\":[\"newHeads\"]")
} }
def "Gets UNAVAIL status right after disconnect"() {
setup:
def up = Mock(DefaultUpstream)
conn = new EthereumWsFactory("ws://localhost:${port}".toURI(), "http://localhost:${port}".toURI()).create(up, null, null)
when:
conn.connect()
conn.reconnectIntervalSeconds = 10
Thread.sleep(SLEEP)
server.stop()
Thread.sleep(100)
then:
1 * up.setStatus(UpstreamAvailability.UNAVAILABLE)
}
def "Validates after connect"() {
setup:
def validator = Mock(EthereumUpstreamValidator)
conn = new EthereumWsFactory("ws://localhost:${port}".toURI(), "http://localhost:${port}".toURI()).create(null, validator, null)
when:
conn.connect()
Thread.sleep(100)
then:
1 * validator.validate()
}
def "Try to connects to server until it's available"() { def "Try to connects to server until it's available"() {
when: when:
server.stop() server.stop()

View File

@@ -53,7 +53,7 @@ class EthereumWsFactorySpec extends Specification {
def apiMock = TestingCommons.api() def apiMock = TestingCommons.api()
def wsApiMock = apiMock.asWebsocket() def wsApiMock = apiMock.asWebsocket()
def ws = wsf.create() def ws = wsf.create(null, null, null)
apiMock.answerOnce("eth_getBlockByHash", ["0x3ec2ebf5d0ec474d0ac6bc50d2770d8409ad76e119968e7919f85d5ec8915200", false], block) apiMock.answerOnce("eth_getBlockByHash", ["0x3ec2ebf5d0ec474d0ac6bc50d2770d8409ad76e119968e7919f85d5ec8915200", false], block)
@@ -74,7 +74,7 @@ class EthereumWsFactorySpec extends Specification {
def wsf = new EthereumWsFactory(new URI("http://localhost"), new URI("http://localhost")) def wsf = new EthereumWsFactory(new URI("http://localhost"), new URI("http://localhost"))
def apiMock = TestingCommons.api() def apiMock = TestingCommons.api()
def wsApiMock = apiMock.asWebsocket() def wsApiMock = apiMock.asWebsocket()
def ws = wsf.create() def ws = wsf.create(null, null, null)
def tx = new TransactionJson().tap { def tx = new TransactionJson().tap {
hash = TransactionId.from("0x3ec2ebf5d0ec474d0ac6bc50d2770d8409ad76e119968e7919f85d5ec8915200") hash = TransactionId.from("0x3ec2ebf5d0ec474d0ac6bc50d2770d8409ad76e119968e7919f85d5ec8915200")
@@ -99,7 +99,7 @@ class EthereumWsFactorySpec extends Specification {
def wsf = new EthereumWsFactory(new URI("http://localhost"), new URI("http://localhost")) def wsf = new EthereumWsFactory(new URI("http://localhost"), new URI("http://localhost"))
def apiMock = TestingCommons.api() def apiMock = TestingCommons.api()
def wsApiMock = apiMock.asWebsocket() def wsApiMock = apiMock.asWebsocket()
def ws = wsf.create() def ws = wsf.create(null, null, null)
apiMock.answerOnce("eth_getTransactionByHash", ["0x3ec2ebf5d0ec474d0ac6bc50d2770d8409ad76e119968e7919f85d5ec8915200"], null) apiMock.answerOnce("eth_getTransactionByHash", ["0x3ec2ebf5d0ec474d0ac6bc50d2770d8409ad76e119968e7919f85d5ec8915200"], null)
@@ -122,7 +122,7 @@ class EthereumWsFactorySpec extends Specification {
def wsf = new EthereumWsFactory(new URI("http://localhost"), new URI("http://localhost")) def wsf = new EthereumWsFactory(new URI("http://localhost"), new URI("http://localhost"))
def apiMock = TestingCommons.api() def apiMock = TestingCommons.api()
def wsApiMock = apiMock.asWebsocket() def wsApiMock = apiMock.asWebsocket()
def ws = wsf.create() def ws = wsf.create(null, null, null)
apiMock.answerOnce("eth_getTransactionByHash", ["0x3ec2ebf5d0ec474d0ac6bc50d2770d8409ad76e119968e7919f85d5ec8915200"], apiMock.answerOnce("eth_getTransactionByHash", ["0x3ec2ebf5d0ec474d0ac6bc50d2770d8409ad76e119968e7919f85d5ec8915200"],
new RpcResponseError(RpcResponseError.CODE_METHOD_NOT_EXIST, "test")) new RpcResponseError(RpcResponseError.CODE_METHOD_NOT_EXIST, "test"))