Fix reconnect to newHeads (#178)

* Fix reconnect to newHeads
This commit is contained in:
KirillPamPam
2023-03-21 16:41:47 +04:00
committed by GitHub
parent 9d81bbae65
commit 74d666a0bd
7 changed files with 74 additions and 11 deletions

View File

@@ -67,11 +67,6 @@ abstract class AbstractHead @JvmOverloads constructor(
} }
fun follow(source: Flux<BlockContainer>): Disposable { fun follow(source: Flux<BlockContainer>): Disposable {
if (completed) {
// if stream was already completed it cannot accept messages (with FAIL_TERMINATED), so needs to be recreated
stream = Sinks.many().multicast().directBestEffort<BlockContainer>()
completed = false
}
return source return source
.filter { .filter {
log.trace("Filtering block $upstreamId block $it") log.trace("Filtering block $upstreamId block $it")
@@ -85,9 +80,7 @@ abstract class AbstractHead @JvmOverloads constructor(
log.warn("Received signal $upstreamId $it unexpectedly - restart head") log.warn("Received signal $upstreamId $it unexpectedly - restart head")
lastHeadUpdated = 0L lastHeadUpdated = 0L
} else { } else {
log.warn("Received signal $upstreamId $it - stop emit new head") log.warn("Received signal $upstreamId $it, continue emit heads")
completed = true
stream.tryEmitComplete()
} }
} }
.subscribeOn(Schedulers.boundedElastic()) .subscribeOn(Schedulers.boundedElastic())
@@ -155,6 +148,9 @@ abstract class AbstractHead @JvmOverloads constructor(
future = null future = null
} }
override fun onNoHeadUpdates() {
}
override fun start() { override fun start() {
stopping = false stopping = false
log.debug("Start ${this.javaClass.simpleName} $upstreamId") log.debug("Start ${this.javaClass.simpleName} $upstreamId")
@@ -165,6 +161,7 @@ abstract class AbstractHead @JvmOverloads constructor(
delayed.set(delay > awaitHeadTimeoutMs) delayed.set(delay > awaitHeadTimeoutMs)
if (delayed.get()) { if (delayed.get()) {
log.warn("No head updates $upstreamId for $delay ms @ ${this.javaClass.simpleName}") log.warn("No head updates $upstreamId for $delay ms @ ${this.javaClass.simpleName}")
onNoHeadUpdates()
} }
}, 180, 30, TimeUnit.SECONDS }, 180, 30, TimeUnit.SECONDS
) )

View File

@@ -36,4 +36,7 @@ class EmptyHead : Head {
override fun stop() { override fun stop() {
} }
override fun onNoHeadUpdates() {
}
} }

View File

@@ -41,4 +41,6 @@ interface Head {
fun start() fun start()
fun stop() fun stop()
fun onNoHeadUpdates()
} }

View File

@@ -28,6 +28,7 @@ import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
import io.emeraldpay.etherjar.rpc.json.BlockJson import io.emeraldpay.etherjar.rpc.json.BlockJson
import io.emeraldpay.etherjar.rpc.json.TransactionRefJson import io.emeraldpay.etherjar.rpc.json.TransactionRefJson
import org.slf4j.LoggerFactory
import reactor.core.Disposable import reactor.core.Disposable
import reactor.core.publisher.Flux import reactor.core.publisher.Flux
import reactor.core.publisher.Mono import reactor.core.publisher.Mono
@@ -44,6 +45,10 @@ class EthereumWsHead(
private val skipEnhance: Boolean private val skipEnhance: Boolean
) : DefaultEthereumHead(upstreamId, forkChoice, blockValidator), Lifecycle { ) : DefaultEthereumHead(upstreamId, forkChoice, blockValidator), Lifecycle {
companion object {
private val log = LoggerFactory.getLogger(EthereumWsHead::class.java)
}
private var subscription: Disposable? = null private var subscription: Disposable? = null
override fun isRunning(): Boolean { override fun isRunning(): Boolean {
@@ -61,6 +66,11 @@ class EthereumWsHead(
this.subscription = super.follow(heads) this.subscription = super.follow(heads)
} }
override fun onNoHeadUpdates() {
log.warn("Restart ws head, upstreamId: $upstreamId")
start()
}
fun listenNewHeads(): Flux<BlockContainer> { fun listenNewHeads(): Flux<BlockContainer> {
return wsSubscriptions.subscribe("newHeads") return wsSubscriptions.subscribe("newHeads")
.map { .map {

View File

@@ -77,4 +77,9 @@ class EthereumHeadMock implements Head {
void stop() { void stop() {
} }
@Override
void onNoHeadUpdates() {
}
} }

View File

@@ -60,7 +60,7 @@ class AbstractHeadSpec extends Specification {
head.stop() head.stop()
source.tryEmitComplete() source.tryEmitComplete()
} }
.expectComplete() .thenCancel()
.verify(Duration.ofSeconds(1)) .verify(Duration.ofSeconds(1))
} }
@@ -85,7 +85,7 @@ class AbstractHeadSpec extends Specification {
head.stop() head.stop()
source.tryEmitComplete() source.tryEmitComplete()
} }
.expectComplete() .thenCancel()
.verify(Duration.ofSeconds(1)) .verify(Duration.ofSeconds(1))
} }
@@ -115,7 +115,7 @@ class AbstractHeadSpec extends Specification {
head.stop() head.stop()
source.tryEmitComplete() source.tryEmitComplete()
} }
.expectComplete() .thenCancel()
.verify(Duration.ofSeconds(1)) .verify(Duration.ofSeconds(1))
} }

View File

@@ -25,8 +25,11 @@ import io.emeraldpay.etherjar.domain.TransactionId
import io.emeraldpay.etherjar.rpc.json.BlockJson import io.emeraldpay.etherjar.rpc.json.BlockJson
import io.emeraldpay.etherjar.rpc.json.TransactionRefJson import io.emeraldpay.etherjar.rpc.json.TransactionRefJson
import reactor.core.publisher.Flux import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
import reactor.test.StepVerifier
import spock.lang.Specification import spock.lang.Specification
import java.time.Duration
import java.time.Instant import java.time.Instant
import java.time.temporal.ChronoUnit import java.time.temporal.ChronoUnit
@@ -71,4 +74,47 @@ class EthereumWsHeadSpec extends Specification {
headBlock headBlock
]) ])
} }
def "Restart ethereum ws head"() {
setup:
def block = new BlockJson<TransactionRefJson>()
block.timestamp = Instant.now().truncatedTo(ChronoUnit.SECONDS)
block.number = 103
block.hash = BlockHash.from("0x3ec2ebf5d0ec474d0ac6bc50d2770d8409ad76e119968e7919f85d5ec8915200")
def secondBlock = new BlockJson<TransactionRefJson>()
secondBlock.timestamp = Instant.now().truncatedTo(ChronoUnit.SECONDS)
secondBlock.number = 105
secondBlock.hash = BlockHash.from("0x29229361dc5aa1ec66c323dc7a299e2b61a8c8dd2a3522d41255ec10eca25dd8")
def firstHeadBlock = block.with {
Global.objectMapper.writeValueAsBytes(it)
}
def secondHeadBlock = secondBlock.with {
Global.objectMapper.writeValueAsBytes(it)
}
def apiMock = TestingCommons.api()
apiMock.answerOnce("eth_getBlockByHash", ["0x3ec2ebf5d0ec474d0ac6bc50d2770d8409ad76e119968e7919f85d5ec8915200", false], null)
apiMock.answerOnce("eth_getBlockByHash", ["0x29229361dc5aa1ec66c323dc7a299e2b61a8c8dd2a3522d41255ec10eca25dd8", false], null)
apiMock.answerOnce("eth_blockNumber", [], Mono.empty())
apiMock.answerOnce("eth_blockNumber", [], Mono.empty())
def ws = Mock(WsSubscriptions) {
2 * subscribe("newHeads") >>> [Flux.fromIterable([firstHeadBlock]), Flux.fromIterable([secondHeadBlock])]
}
def head = new EthereumWsHead("fake", new AlwaysForkChoice(), BlockValidator.ALWAYS_VALID, apiMock, ws, true)
when:
def act = head.getFlux()
then:
StepVerifier.create(act)
.then { head.start() }
.expectNext(BlockContainer.from(block))
.then { head.onNoHeadUpdates() }
.expectNext(BlockContainer.from(secondBlock))
.thenCancel()
.verify(Duration.ofSeconds(1))
}
} }