Resubscribe to newHeads immediately after reconnection (#189)

* Resubscribe to newHeads immediately after reconnection
This commit is contained in:
KirillPamPam
2023-03-30 13:32:02 +04:00
committed by GitHub
parent dcf360a7d7
commit 97fa616e19
14 changed files with 216 additions and 21 deletions

View File

@@ -26,6 +26,7 @@ import io.emeraldpay.etherjar.rpc.json.BlockJson
import io.emeraldpay.etherjar.rpc.json.TransactionRefJson
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
import reactor.core.publisher.Sinks
import reactor.test.StepVerifier
import spock.lang.Specification
@@ -60,7 +61,9 @@ class EthereumWsHeadSpec extends Specification {
def apiMock = TestingCommons.api()
apiMock.answerOnce("eth_getBlockByHash", ["0x3ec2ebf5d0ec474d0ac6bc50d2770d8409ad76e119968e7919f85d5ec8915200", false], block)
def ws = Mock(WsSubscriptions)
def ws = Mock(WsSubscriptions) {
1 * it.connectionInfoFlux() >> Flux.empty()
}
def head = new EthereumWsHead("fake", new AlwaysForkChoice(), BlockValidator.ALWAYS_VALID, apiMock, ws, false)
@@ -73,9 +76,9 @@ class EthereumWsHeadSpec extends Specification {
act.transactions[0].toHexWithPrefix() == "0x29229361dc5aa1ec66c323dc7a299e2b61a8c8dd2a3522d41255ec10eca25dd8"
act.transactions[1].toHexWithPrefix() == "0xebe8f22a55a9e26892a8545b93cbb2bfa4fd81c3184e50e5cf6276025bb42b93"
1 * ws.subscribe("newHeads") >> Flux.fromIterable([
headBlock
])
1 * ws.subscribe("newHeads") >> new WsSubscriptions.SubscribeData(
Flux.fromIterable([headBlock]), "id"
)
}
def "Restart ethereum ws head"() {
@@ -105,7 +108,11 @@ class EthereumWsHeadSpec extends Specification {
apiMock.answerOnce("eth_blockNumber", [], Mono.empty())
def ws = Mock(WsSubscriptions) {
2 * subscribe("newHeads") >>> [Flux.fromIterable([firstHeadBlock]), Flux.fromIterable([secondHeadBlock])]
1 * it.connectionInfoFlux() >> Flux.empty()
2 * subscribe("newHeads") >>> [
new WsSubscriptions.SubscribeData(Flux.fromIterable([firstHeadBlock]), "id"),
new WsSubscriptions.SubscribeData(Flux.fromIterable([secondHeadBlock]), "id")
]
}
def head = new EthereumWsHead("fake", new AlwaysForkChoice(), BlockValidator.ALWAYS_VALID, apiMock, ws, true)
@@ -122,4 +129,55 @@ class EthereumWsHeadSpec extends Specification {
.thenCancel()
.verify(Duration.ofSeconds(1))
}
def "Restart ethereum ws head immediately after reconnection"() {
setup:
def block = new BlockJson<TransactionRefJson>()
block.timestamp = Instant.now().truncatedTo(ChronoUnit.SECONDS)
block.number = 103
block.parentHash = parent
block.hash = BlockHash.from("0x3ec2ebf5d0ec474d0ac6bc50d2770d8409ad76e119968e7919f85d5ec8915200")
def secondBlock = new BlockJson<TransactionRefJson>()
secondBlock.parentHash = parent
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()
def connectionInfoSink = Sinks.many().multicast().directBestEffort()
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) {
1 * it.connectionInfoFlux() >> connectionInfoSink.asFlux()
2 * subscribe("newHeads") >>> [
new WsSubscriptions.SubscribeData(Flux.fromIterable([firstHeadBlock]), "id"),
new WsSubscriptions.SubscribeData(Flux.fromIterable([secondHeadBlock]), "id")
]
}
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 { connectionInfoSink.tryEmitNext(new WsConnection.ConnectionInfo("id", WsConnection.ConnectionState.DISCONNECTED)) }
.then { connectionInfoSink.tryEmitNext(new WsConnection.ConnectionInfo("id", WsConnection.ConnectionState.CONNECTED)) }
.expectNext(BlockContainer.from(secondBlock))
.thenCancel()
.verify(Duration.ofSeconds(1))
}
}

View File

@@ -16,6 +16,7 @@
package io.emeraldpay.dshackle.upstream.ethereum
import io.emeraldpay.dshackle.upstream.DefaultUpstream
import reactor.core.publisher.Flux
import spock.lang.Specification
import java.util.concurrent.ScheduledExecutorService
@@ -24,7 +25,9 @@ class WsConnectionMultiPoolSpec extends Specification {
def "create connection when less than required"() {
setup:
def conn = Mock(WsConnection)
def conn = Mock(WsConnection) {
1 * it.connectionInfoFlux() >> Flux.empty()
}
def up = Mock(DefaultUpstream)
def factory = Mock(EthereumWsConnectionFactory)
def pool = new WsConnectionMultiPool(factory, up, 3)
@@ -40,9 +43,15 @@ class WsConnectionMultiPoolSpec extends Specification {
def "create connection until target"() {
setup:
def conn1 = Mock(WsConnection)
def conn2 = Mock(WsConnection)
def conn3 = Mock(WsConnection)
def conn1 = Mock(WsConnection) {
1 * it.connectionInfoFlux() >> Flux.empty()
}
def conn2 = Mock(WsConnection) {
1 * it.connectionInfoFlux() >> Flux.empty()
}
def conn3 = Mock(WsConnection) {
1 * it.connectionInfoFlux() >> Flux.empty()
}
def up = Mock(DefaultUpstream)
def factory = Mock(EthereumWsConnectionFactory)
def pool = new WsConnectionMultiPool(factory, up, 3)
@@ -84,10 +93,18 @@ class WsConnectionMultiPoolSpec extends Specification {
def "recreate connection after failure"() {
setup:
def conn1 = Mock(WsConnection)
def conn2 = Mock(WsConnection)
def conn3 = Mock(WsConnection)
def conn4 = Mock(WsConnection)
def conn1 = Mock(WsConnection) {
1 * it.connectionInfoFlux() >> Flux.empty()
}
def conn2 = Mock(WsConnection) {
1 * it.connectionInfoFlux() >> Flux.empty()
}
def conn3 = Mock(WsConnection) {
1 * it.connectionInfoFlux() >> Flux.empty()
}
def conn4 = Mock(WsConnection) {
1 * it.connectionInfoFlux() >> Flux.empty()
}
def up = Mock(DefaultUpstream)
def factory = Mock(EthereumWsConnectionFactory)
def pool = new WsConnectionMultiPool(factory, up, 3)

View File

@@ -36,7 +36,9 @@ class WsSubscriptionsImplSpec extends Specification {
]
)
def conn = Mock(WsConnection)
def conn = Mock(WsConnection) {
1 * it.connectionId() >> "id"
}
def pool = Mock(WsConnectionPool) {
getConnection() >> conn
}
@@ -44,6 +46,7 @@ class WsSubscriptionsImplSpec extends Specification {
when:
def act = ws.subscribe("foo_bar")
.data
.map { new String(it) }
.take(3)
.collectList().block(Duration.ofSeconds(1))
@@ -71,7 +74,9 @@ class WsSubscriptionsImplSpec extends Specification {
]
)
def conn = Mock(WsConnection)
def conn = Mock(WsConnection) {
1 * it.connectionId() >> "id"
}
def pool = Mock(WsConnectionPool) {
getConnection() >> conn
}
@@ -79,6 +84,7 @@ class WsSubscriptionsImplSpec extends Specification {
when:
def act = ws.subscribe("foo_bar")
.data
.map { new String(it) }
.take(3)
.collectList().block(Duration.ofSeconds(1))

View File

@@ -40,7 +40,9 @@ class WebsocketPendingTxesSpec extends Specification {
.collectList().block(Duration.ofSeconds(1))
then:
1 * ws.subscribe("newPendingTransactions") >> Flux.fromIterable(responses)
1 * ws.subscribe("newPendingTransactions") >> new WsSubscriptions.SubscribeData(
Flux.fromIterable(responses), "id"
)
txes.collect {it.toHex() } == [
"0xa61bab14fc9720ea8725622688c2f964666d7c2afdae38af7dad53f12f242d5c",
"0x911548eb0f3bf353a54e03a3506c7c3e747470d6c201f03babbc07ff6e14cd6e",